Browse Source

feat: show loading indicator when loading data from remote (#3120)

* chore: show circle indicator if fetch the data from remote

* chore: fix the lb warnings

* chore: create sdk-build for macOS
Nathan.fooo 1 year ago
parent
commit
9a72f31d60
28 changed files with 198 additions and 531 deletions
  1. 0 8
      .github/workflows/release.yml
  2. 9 0
      frontend/appflowy_flutter/lib/plugins/database_view/application/database_controller.dart
  3. 12 0
      frontend/appflowy_flutter/lib/plugins/database_view/application/defines.dart
  4. 10 14
      frontend/appflowy_flutter/lib/plugins/database_view/board/application/board_bloc.dart
  5. 10 14
      frontend/appflowy_flutter/lib/plugins/database_view/calendar/application/calendar_bloc.dart
  6. 16 4
      frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_page.dart
  7. 6 13
      frontend/appflowy_flutter/lib/plugins/database_view/grid/application/grid_bloc.dart
  8. 23 14
      frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/toolbar/grid_setting_bar.dart
  9. 39 23
      frontend/appflowy_flutter/lib/plugins/database_view/tar_bar/tab_bar_view.dart
  10. 2 1
      frontend/appflowy_flutter/lib/plugins/document/document_page.dart
  11. 1 1
      frontend/appflowy_flutter/lib/user/application/user_service.dart
  12. 8 8
      frontend/rust-lib/flowy-document2/src/manager.rs
  13. 0 108
      frontend/rust-lib/flowy-server/docs/README.md
  14. BIN
      frontend/rust-lib/flowy-server/docs/architecture-Application.png
  15. 0 78
      frontend/rust-lib/flowy-server/docs/architecture.plantuml
  16. BIN
      frontend/rust-lib/flowy-server/docs/architecture.png
  17. BIN
      frontend/rust-lib/flowy-server/docs/schema-Triggers_in_Database.png
  18. 0 203
      frontend/rust-lib/flowy-server/docs/schema.plantuml
  19. BIN
      frontend/rust-lib/flowy-server/docs/schema.png
  20. 3 3
      frontend/rust-lib/flowy-server/src/supabase/api/collab_storage.rs
  21. 3 3
      frontend/rust-lib/flowy-server/src/supabase/api/database.rs
  22. 4 4
      frontend/rust-lib/flowy-server/src/supabase/api/document.rs
  23. 3 3
      frontend/rust-lib/flowy-server/src/supabase/api/folder.rs
  24. 17 7
      frontend/rust-lib/flowy-server/src/supabase/api/request.rs
  25. 13 13
      frontend/rust-lib/flowy-server/src/supabase/server.rs
  26. 5 5
      frontend/rust-lib/flowy-server/tests/supabase_test/util.rs
  27. 1 4
      frontend/rust-lib/flowy-user/src/services/user_session.rs
  28. 13 0
      frontend/scripts/makefile/desktop.toml

+ 0 - 8
.github/workflows/release.yml

@@ -20,14 +20,6 @@ jobs:
       - name: Checkout
         uses: actions/checkout@v2
 
-#      - name: Create .env file
-#        working-directory: frontend/appflowy_flutter
-#        run: |
-#          touch .env
-#          echo SUPABASE_URL=${{ secrets.HOST_URL }} >> .env
-#          echo SUPABASE_ANON_KEY=${{ secrets.HOST_ANON_KEY }} >> .env
-#          echo SUPABASE_JWT_SECRET=${{ secrets.HOST_JWT_SECRET }} >> .env
-
       - name: Build release notes
         run: |
           touch ${{ env.RELEASE_NOTES_PATH }}

+ 9 - 0
frontend/appflowy_flutter/lib/plugins/database_view/application/database_controller.dart

@@ -13,6 +13,7 @@ import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
 import 'package:collection/collection.dart';
 import 'dart:async';
 import 'package:dartz/dartz.dart';
+import 'package:flutter/material.dart';
 import 'database_view_service.dart';
 import 'defines.dart';
 import 'layout/layout_service.dart';
@@ -92,6 +93,8 @@ class DatabaseController {
   final DatabaseGroupListener _groupListener;
   final DatabaseLayoutSettingListener _layoutListener;
 
+  final ValueNotifier<bool> _isLoading = ValueNotifier(true);
+
   DatabaseController({required ViewPB view})
       : viewId = view.id,
         _databaseViewBackendSvc = DatabaseViewBackendService(viewId: view.id),
@@ -109,6 +112,12 @@ class DatabaseController {
     _listenOnLayoutChanged();
   }
 
+  void setIsLoading(bool isLoading) {
+    _isLoading.value = isLoading;
+  }
+
+  ValueNotifier<bool> get isLoading => _isLoading;
+
   void addListener({
     DatabaseCallbacks? onDatabaseChanged,
     DatabaseLayoutSettingCallbacks? onLayoutChanged,

+ 12 - 0
frontend/appflowy_flutter/lib/plugins/database_view/application/defines.dart

@@ -3,12 +3,16 @@ import 'dart:collection';
 import 'package:appflowy/plugins/database_view/grid/presentation/widgets/sort/sort_info.dart';
 import 'package:appflowy_backend/protobuf/flowy-database2/database_entities.pb.dart';
 import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
+import 'package:dartz/dartz.dart';
+import 'package:freezed_annotation/freezed_annotation.dart';
 
 import '../grid/presentation/widgets/filter/filter_info.dart';
 import 'field/field_controller.dart';
 import 'row/row_cache.dart';
 import 'row/row_service.dart';
 
+part 'defines.freezed.dart';
+
 typedef OnFieldsChanged = void Function(UnmodifiableListView<FieldInfo>);
 typedef OnFiltersChanged = void Function(List<FilterInfo>);
 typedef OnSortsChanged = void Function(List<SortInfo>);
@@ -27,3 +31,11 @@ typedef OnNumOfRowsChanged = void Function(
 );
 
 typedef OnError = void Function(FlowyError);
+
+@freezed
+class LoadingState with _$LoadingState {
+  const factory LoadingState.loading() = _Loading;
+  const factory LoadingState.finish(
+    Either<Unit, FlowyError> successOrFail,
+  ) = _Finish;
+}

+ 10 - 14
frontend/appflowy_flutter/lib/plugins/database_view/board/application/board_bloc.dart

@@ -1,6 +1,7 @@
 import 'dart:async';
 import 'dart:collection';
 
+import 'package:appflowy/plugins/database_view/application/defines.dart';
 import 'package:appflowy/plugins/database_view/application/row/row_service.dart';
 import 'package:appflowy_board/appflowy_board.dart';
 import 'package:dartz/dartz.dart';
@@ -254,11 +255,14 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
   Future<void> _openGrid(Emitter<BoardState> emit) async {
     final result = await databaseController.open();
     result.fold(
-      (grid) => emit(
-        state.copyWith(loadingState: GridLoadingState.finish(left(unit))),
-      ),
+      (grid) {
+        databaseController.setIsLoading(false);
+        emit(
+          state.copyWith(loadingState: LoadingState.finish(left(unit))),
+        );
+      },
       (err) => emit(
-        state.copyWith(loadingState: GridLoadingState.finish(right(err))),
+        state.copyWith(loadingState: LoadingState.finish(right(err))),
       ),
     );
   }
@@ -323,7 +327,7 @@ class BoardState with _$BoardState {
     required Option<DatabasePB> grid,
     required List<String> groupIds,
     required Option<BoardEditingRow> editingRow,
-    required GridLoadingState loadingState,
+    required LoadingState loadingState,
     required Option<FlowyError> noneOrError,
   }) = _BoardState;
 
@@ -333,18 +337,10 @@ class BoardState with _$BoardState {
         groupIds: [],
         editingRow: none(),
         noneOrError: none(),
-        loadingState: const _Loading(),
+        loadingState: const LoadingState.loading(),
       );
 }
 
-@freezed
-class GridLoadingState with _$GridLoadingState {
-  const factory GridLoadingState.loading() = _Loading;
-  const factory GridLoadingState.finish(
-    Either<Unit, FlowyError> successOrFail,
-  ) = _Finish;
-}
-
 class GridFieldEquatable extends Equatable {
   final UnmodifiableListView<FieldPB> _fields;
   const GridFieldEquatable(

+ 10 - 14
frontend/appflowy_flutter/lib/plugins/database_view/calendar/application/calendar_bloc.dart

@@ -1,4 +1,5 @@
 import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
+import 'package:appflowy/plugins/database_view/application/defines.dart';
 import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
 import 'package:appflowy/plugins/database_view/application/row/row_service.dart';
 import 'package:appflowy_backend/dispatch/dispatch.dart';
@@ -133,11 +134,14 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
   Future<void> _openDatabase(Emitter<CalendarState> emit) async {
     final result = await databaseController.open();
     result.fold(
-      (database) => emit(
-        state.copyWith(loadingState: DatabaseLoadingState.finish(left(unit))),
-      ),
+      (database) {
+        databaseController.setIsLoading(false);
+        emit(
+          state.copyWith(loadingState: LoadingState.finish(left(unit))),
+        );
+      },
       (err) => emit(
-        state.copyWith(loadingState: DatabaseLoadingState.finish(right(err))),
+        state.copyWith(loadingState: LoadingState.finish(right(err))),
       ),
     );
   }
@@ -425,7 +429,7 @@ class CalendarState with _$CalendarState {
     CalendarEventData<CalendarDayEvent>? updateEvent,
     required List<String> deleteEventIds,
     required Option<CalendarLayoutSettingPB> settings,
-    required DatabaseLoadingState loadingState,
+    required LoadingState loadingState,
     required Option<FlowyError> noneOrError,
   }) = _CalendarState;
 
@@ -436,18 +440,10 @@ class CalendarState with _$CalendarState {
         deleteEventIds: [],
         settings: none(),
         noneOrError: none(),
-        loadingState: const _Loading(),
+        loadingState: const LoadingState.loading(),
       );
 }
 
-@freezed
-class DatabaseLoadingState with _$DatabaseLoadingState {
-  const factory DatabaseLoadingState.loading() = _Loading;
-  const factory DatabaseLoadingState.finish(
-    Either<Unit, FlowyError> successOrFail,
-  ) = _Finish;
-}
-
 class CalendarEditingRow {
   RowPB row;
   int? index;

+ 16 - 4
frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_page.dart

@@ -159,10 +159,22 @@ class _CalendarPageState extends State<CalendarPage> {
           ],
           child: BlocBuilder<CalendarBloc, CalendarState>(
             builder: (context, state) {
-              return _buildCalendar(
-                context,
-                _eventController,
-                state.settings.foldLeft(0, (previous, a) => a.firstDayOfWeek),
+              return ValueListenableBuilder<bool>(
+                valueListenable: widget.databaseController.isLoading,
+                builder: (_, value, ___) {
+                  if (value) {
+                    return const Center(
+                      child: CircularProgressIndicator.adaptive(),
+                    );
+                  } else {
+                    return _buildCalendar(
+                      context,
+                      _eventController,
+                      state.settings
+                          .foldLeft(0, (previous, a) => a.firstDayOfWeek),
+                    );
+                  }
+                },
               );
             },
           ),

+ 6 - 13
frontend/appflowy_flutter/lib/plugins/database_view/grid/application/grid_bloc.dart

@@ -1,11 +1,11 @@
 import 'dart:async';
+import 'package:appflowy/plugins/database_view/application/defines.dart';
 import 'package:appflowy/plugins/database_view/application/row/row_cache.dart';
 import 'package:appflowy/plugins/database_view/application/row/row_service.dart';
 import 'package:appflowy/plugins/database_view/grid/presentation/widgets/filter/filter_info.dart';
 import 'package:appflowy/plugins/database_view/grid/presentation/widgets/sort/sort_info.dart';
 import 'package:dartz/dartz.dart';
 import 'package:equatable/equatable.dart';
-import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
 import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
 import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
@@ -134,12 +134,13 @@ class GridBloc extends Bloc<GridEvent, GridState> {
     final result = await databaseController.open();
     result.fold(
       (grid) {
+        databaseController.setIsLoading(false);
         emit(
-          state.copyWith(loadingState: GridLoadingState.finish(left(unit))),
+          state.copyWith(loadingState: LoadingState.finish(left(unit))),
         );
       },
       (err) => emit(
-        state.copyWith(loadingState: GridLoadingState.finish(right(err))),
+        state.copyWith(loadingState: LoadingState.finish(right(err))),
       ),
     );
   }
@@ -177,7 +178,7 @@ class GridState with _$GridState {
     required GridFieldEquatable fields,
     required List<RowInfo> rowInfos,
     required int rowCount,
-    required GridLoadingState loadingState,
+    required LoadingState loadingState,
     required bool reorderable,
     required ChangedReason reason,
     required List<SortInfo> sorts,
@@ -191,21 +192,13 @@ class GridState with _$GridState {
         grid: none(),
         viewId: viewId,
         reorderable: true,
-        loadingState: const _Loading(),
+        loadingState: const LoadingState.loading(),
         reason: const InitialListState(),
         filters: [],
         sorts: [],
       );
 }
 
-@freezed
-class GridLoadingState with _$GridLoadingState {
-  const factory GridLoadingState.loading() = _Loading;
-  const factory GridLoadingState.finish(
-    Either<Unit, FlowyError> successOrFail,
-  ) = _Finish;
-}
-
 class GridFieldEquatable extends Equatable {
   final List<FieldInfo> _fields;
   const GridFieldEquatable(

+ 23 - 14
frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/toolbar/grid_setting_bar.dart

@@ -47,20 +47,29 @@ class GridSettingBar extends StatelessWidget {
             listener: (context, state) => toggleExtension.toggle(),
           ),
         ],
-        child: SizedBox(
-          height: 40,
-          child: Row(
-            mainAxisAlignment: MainAxisAlignment.center,
-            children: [
-              SizedBox(width: GridSize.leadingHeaderPadding),
-              const Spacer(),
-              const FilterButton(),
-              const SortButton(),
-              SettingButton(
-                databaseController: controller,
-              ),
-            ],
-          ),
+        child: ValueListenableBuilder<bool>(
+          valueListenable: controller.isLoading,
+          builder: (context, value, child) {
+            if (value) {
+              return const SizedBox.shrink();
+            } else {
+              return SizedBox(
+                height: 40,
+                child: Row(
+                  mainAxisAlignment: MainAxisAlignment.center,
+                  children: [
+                    SizedBox(width: GridSize.leadingHeaderPadding),
+                    const Spacer(),
+                    const FilterButton(),
+                    const SortButton(),
+                    SettingButton(
+                      databaseController: controller,
+                    ),
+                  ],
+                ),
+              );
+            }
+          },
         ),
       ),
     );

+ 39 - 23
frontend/appflowy_flutter/lib/plugins/database_view/tar_bar/tab_bar_view.dart

@@ -93,30 +93,46 @@ class _DatabaseTabBarViewState extends State<DatabaseTabBarView> {
         ],
         child: Column(
           children: [
-            Row(
-              children: [
-                BlocBuilder<GridTabBarBloc, GridTabBarState>(
-                  builder: (context, state) {
-                    return const Flexible(
-                      child: Padding(
-                        padding: EdgeInsets.only(left: 50),
-                        child: DatabaseTabBar(),
-                      ),
-                    );
-                  },
-                ),
-                BlocBuilder<GridTabBarBloc, GridTabBarState>(
-                  builder: (context, state) {
-                    return SizedBox(
-                      width: 300,
-                      child: Padding(
-                        padding: const EdgeInsets.only(right: 50),
-                        child: pageSettingBarFromState(state),
-                      ),
-                    );
+            BlocBuilder<GridTabBarBloc, GridTabBarState>(
+              builder: (context, state) {
+                return ValueListenableBuilder<bool>(
+                  valueListenable: state
+                      .tabBarControllerByViewId[state.parentView.id]!
+                      .controller
+                      .isLoading,
+                  builder: (_, value, ___) {
+                    if (value) {
+                      return const SizedBox.shrink();
+                    } else {
+                      return Row(
+                        children: [
+                          BlocBuilder<GridTabBarBloc, GridTabBarState>(
+                            builder: (context, state) {
+                              return const Flexible(
+                                child: Padding(
+                                  padding: EdgeInsets.only(left: 50),
+                                  child: DatabaseTabBar(),
+                                ),
+                              );
+                            },
+                          ),
+                          BlocBuilder<GridTabBarBloc, GridTabBarState>(
+                            builder: (context, state) {
+                              return SizedBox(
+                                width: 300,
+                                child: Padding(
+                                  padding: const EdgeInsets.only(right: 50),
+                                  child: pageSettingBarFromState(state),
+                                ),
+                              );
+                            },
+                          ),
+                        ],
+                      );
+                    }
                   },
-                ),
-              ],
+                );
+              },
             ),
             BlocBuilder<GridTabBarBloc, GridTabBarState>(
               builder: (context, state) {

+ 2 - 1
frontend/appflowy_flutter/lib/plugins/document/document_page.dart

@@ -65,7 +65,8 @@ class _DocumentPageState extends State<DocumentPage> {
       child: BlocBuilder<DocumentBloc, DocumentState>(
         builder: (context, state) {
           return state.loadingState.when(
-            loading: () => const SizedBox.shrink(),
+            loading: () =>
+                const Center(child: CircularProgressIndicator.adaptive()),
             finish: (result) => result.fold(
               (error) {
                 Log.error(error);

+ 1 - 1
frontend/appflowy_flutter/lib/user/application/user_service.dart

@@ -18,7 +18,7 @@ class UserBackendService {
   static Future<Either<FlowyError, UserProfilePB>>
       getCurrentUserProfile() async {
     final result = await UserEventGetUserProfile().send().then((value) {
-      value.fold((l) => null, (r) => Log.error(r));
+      value.fold((l) => null, (r) => Log.info(r));
       return value;
     });
     return result.swap();

+ 8 - 8
frontend/rust-lib/flowy-document2/src/manager.rs

@@ -10,10 +10,10 @@ use collab_document::document_data::default_document_data;
 use collab_document::YrsDocAction;
 use parking_lot::RwLock;
 
-use crate::document::MutexDocument;
 use flowy_document_deps::cloud::DocumentCloudService;
 use flowy_error::{internal_error, FlowyError, FlowyResult};
 
+use crate::document::MutexDocument;
 use crate::entities::DocumentSnapshotPB;
 
 pub trait DocumentUser: Send + Sync {
@@ -77,13 +77,13 @@ impl DocumentManager {
     let mut updates = vec![];
     if !self.is_doc_exist(doc_id)? {
       // Try to get the document from the cloud service
-      if let Ok(document_updates) = self.cloud_service.get_document_updates(doc_id).await {
-        updates = document_updates;
-      } else {
-        return Err(
-          FlowyError::record_not_found().context(format!("document: {} is not exist", doc_id)),
-        );
-      };
+      match self.cloud_service.get_document_updates(doc_id).await {
+        Ok(document_updates) => updates = document_updates,
+        Err(e) => {
+          tracing::error!("Get document data failed: {:?}", e);
+          return Err(FlowyError::internal().context("Can't not read the document data"));
+        },
+      }
     }
 
     tracing::debug!("open_document: {:?}", doc_id);

+ 0 - 108
frontend/rust-lib/flowy-server/docs/README.md

@@ -1,108 +0,0 @@
-# AppFlowy Cloud Architecture
-
-AppFlowy supports multiple cloud solutions. Users can choose their preferred cloud provider, such as Supabase, Firebase,
-AWS, or our own AppFlowyCloud (Self-hosted server).
-
-![](architecture-Application.png)
-
-## Design
-
-AppFlowy use the traits [AppFlowyServer] to abstract the cloud provider. Each cloud provider implements the [AppFlowyServer]
-trait. As the image below shows. Users can choose their preferred cloud provider or simply use the default option, which is the LocalServer. When using the
-LocalServer, data is stored on the local file system. Users can migrate to a cloud provider if needed. For instance, one
-could migrate from LocalServer to AppFlowyCloud. This migration would create a new user in the cloud and transfer all the
-data from the local database to the cloud.
-
-![](architecture.png)
-
-## AppFlowy Cloud Implementation (WIP)
-
-### Restful API
-
-### Table schema
-
-## Supabase Implementation
-
-### Table schema
-![](./schema.png)
-
-1. `af_roles` table: This table contains a list of roles that are used in your application, such as 'Owner', 'Member', and 'Guest'.
-
-2. `af_permissions` table: This table stores permissions that are used in your application. Each permission has a name, a description, and an access level.
-
-3. `af_role_permissions` table: This is a many-to-many relation table between roles and permissions. It represents which permissions a role has.
-
-4. `af_user` table: This stores the details of users like uuid, email, uid, name, created_at. Here, uid is an auto-incrementing integer that uniquely identifies a user.
-
-5. `af_workspace` table: This table contains all the workspaces. Each workspace has an owner which is associated with the uid of a user in the `af_user` table.
-
-6. `af_workspace_member` table: This table maintains a list of all the members associated with a workspace and their roles.
-
-7. `af_collab` and `af_collab_member` tables: These tables store the collaborations and their members respectively. Each collaboration has an owner and a workspace associated with it.
-
-8. `af_collab_update`, `af_collab_update_document`, `af_collab_update_database`, `af_collab_update_w_database`, `af_collab_update_folder`, `af_database_row_update` tables: These tables are used for handling updates to collaborations.
-
-9. `af_collab_statistics`, `af_collab_snapshot`, `af_collab_state`: These tables and view are used for maintaining statistics and snapshots of collaborations.
-
-10. `af_user_profile_view` view: This view is used to get the latest workspace_id for each user.
-
-![](./schema-Triggers_in_Database.png)
-Here's a detailed description for each of these triggers:
-
-1. `create_af_workspace_trigger`:
-
-   This trigger is designed to automate the process of workspace creation in the `af_workspace` table after a new user is inserted into the `af_user` table. When a new user is added, this trigger fires and inserts a new record into the `af_workspace` table, setting the `owner_uid` to the UID of the new user.
-
-2. `manage_af_workspace_member_role_trigger`:
-
-   This trigger helps to manage the roles of workspace members. After an insert operation on the `af_workspace` table, this trigger automatically fires and creates a new record in the `af_workspace_member` table. The new record identifies the user as a member of the workspace with the role 'Owner'. This ensures that every new workspace has an owner.
-
-3. `insert_into_af_collab_trigger`:
-
-   The purpose of this trigger is to ensure consistency between the `af_collab_update` and `af_collab` tables. When an insert operation is about to be performed on the `af_collab_update` table, this trigger fires before the insert operation. It checks if a corresponding collaboration exists in the `af_collab` table using the oid and uid. If a corresponding collaboration does not exist, the trigger creates one, using the oid, uid, and current timestamp. This way, every collab update operation corresponds to a valid collaboration.
-
-4. `insert_into_af_collab_member_trigger`:
-
-   This trigger helps to manage the membership of users in collaborations. After a new collaboration is inserted into the `af_collab` table, this trigger fires. It checks if a corresponding collaboration member exists in the `af_collab_member` table. If a corresponding member does not exist, the trigger creates one, using the collaboration id and user id. This ensures that every collaboration has at least one member.
-
-5. `af_collab_snapshot_update_edit_count_trigger`:
-
-   This trigger is designed to keep track of the number of edits on each collaboration snapshot in the `af_collab_snapshot` table. When an update operation is performed on the `af_collab_snapshot` table, this trigger fires. It increments the `edit_count` of the corresponding record in the `af_collab_snapshot` table by one. This ensures that the application can keep track of how many times each collaboration snapshot has been edited.
-
-
-### Supabase configuration
-
-#### Test
-In order to run the test, you need to set up the .env.test file.
-```dotenv
-# Supabase configuration
-SUPABASE_URL="your-supabase-url"
-SUPABASE_ANON_KEY="your-supabase-anonymous-key"
-SUPABASE_KEY="your-supabase-key"
-SUPABASE_JWT_SECRET="your-supabase-jwt-secret"
-
-# Supabase Database configuration
-SUPABASE_DB="your-supabase-db-url"
-SUPABASE_DB_USER="your-db-username"
-SUPABASE_DB_PORT="your-db-port"
-SUPABASE_DB_PASSWORD="your-db-password"
-```
-
-1. `SUPABASE_URL`: This is the URL of your Supabase server instance. Your application will use this URL to interact with the Supabase service.
-
-2. `SUPABASE_ANON_KEY`: This is the anonymous API key from Supabase, used for operations that don't require user authentication. Operations performed with this key are done as the anonymous role in the database.
-
-3. `SUPABASE_KEY`: This is the API key with higher privileges from Supabase. It is generally used for server-side operations that require more permissions than an anonymous user.
-
-4. `SUPABASE_JWT_SECRET`: This is the secret used to verify JWT tokens generated by Supabase. JWT or JSON Web Token is a standard method for securely transferring data between parties as a JSON object.
-
-5. `SUPABASE_DB`: This is the URL for the database your Supabase server instance is using.
-
-6. `SUPABASE_DB_USER`: This is the username used to authenticate with the Supabase database, in this case, it's 'postgres', which is a common default for PostgreSQL.
-
-7. `SUPABASE_DB_PORT`: This is the port number where your Supabase database service is accessible. The default PostgreSQL port is 5432, and you are using this default port.
-
-8. `SUPABASE_DB_PASSWORD`: This is the password used to authenticate the `SUPABASE_DB_USER` with the Supabase database.
-
-For example, if you want to run the supabase tests located in flowy-test crate. You need to put the `.env.test` file under
-the flowy-test folder.

BIN
frontend/rust-lib/flowy-server/docs/architecture-Application.png


+ 0 - 78
frontend/rust-lib/flowy-server/docs/architecture.plantuml

@@ -1,78 +0,0 @@
-@startuml
-title "Application"
-left to right direction
-package "AppFlowy Application" {
-  [User]
-}
-
-cloud "Supabase Server" {
-  [RESTful Component]
-  [Realtime Component]
-  [Postgres DB]
-}
-
-database "LocalServer" {
-  [Local Server Component]
-}
-
-
-cloud "AppFlowy Cloud Server" {
-  [RESTful Component] as [AppFlowy RESTful Component]
-  [Realtime Component] as [AppFlowy Realtime Component]
-  [Postgres DB] as [AppFlowy Postgres DB]
-}
-
-User --> [AppFlowy Application]
-[AppFlowy Application] --> [Local Server Component] : Connect
-
-[AppFlowy Application] --> [RESTful Component] : RESTful API Communication
-[AppFlowy Application] <..> [Realtime Component] : WebSocket Communication
-
-[AppFlowy Application] --> [AppFlowy RESTful Component] : RESTful API Communication
-[AppFlowy Application] <..> [AppFlowy Realtime Component] : WebSocket Communication
-
-@enduml
-
-
-@startuml
-left to right direction
-
-interface AppFlowyServer {
-  + enable_sync(_enable: bool)
-  + user_service(): Arc<dyn UserService>
-  + folder_service(): Arc<dyn FolderCloudService>
-  + database_service(): Arc<dyn DatabaseCloudService>
-  + document_service(): Arc<dyn DocumentCloudService>
-  + collab_storage(): Option<Arc<dyn RemoteCollabStorage>>
-}
-
-class SupabaseServer {
-  + enable_sync(_enable: bool)
-  + user_service(): Arc<dyn UserService>
-  + folder_service(): Arc<dyn FolderCloudService>
-  + database_service(): Arc<dyn DatabaseCloudService>
-  + document_service(): Arc<dyn DocumentCloudService>
-  + collab_storage(): Option<Arc<dyn RemoteCollabStorage>>
-}
-
-class SelfHostServer {
-  + user_service(): Arc<dyn UserService>
-  + folder_service(): Arc<dyn FolderCloudService>
-  + database_service(): Arc<dyn DatabaseCloudService>
-  + document_service(): Arc<dyn DocumentCloudService>
-  + collab_storage(): Option<Arc<dyn RemoteCollabStorage>>
-}
-
-class LocalServer {
-  + user_service(): Arc<dyn UserService>
-  + folder_service(): Arc<dyn FolderCloudService>
-  + database_service(): Arc<dyn DatabaseCloudService>
-  + document_service(): Arc<dyn DocumentCloudService>
-  + collab_storage(): Option<Arc<dyn RemoteCollabStorage>>
-}
-
-SupabaseServer -u-|> AppFlowyServer
-SelfHostServer -u-|> AppFlowyServer
-LocalServer -u-|> AppFlowyServer
-
-@enduml

BIN
frontend/rust-lib/flowy-server/docs/architecture.png


BIN
frontend/rust-lib/flowy-server/docs/schema-Triggers_in_Database.png


+ 0 - 203
frontend/rust-lib/flowy-server/docs/schema.plantuml

@@ -1,203 +0,0 @@
-@startuml
-left to right direction
-
-entity "af_roles" as roles {
-  id : SERIAL (PK)
-  name : TEXT
-}
-
-entity "af_permissions" as permissions {
-  id : SERIAL (PK)
-  name : VARCHAR(255)
-  access_level : INTEGER
-  description : TEXT
-}
-
-entity "af_role_permissions" as role_permissions {
-  role_id : INT (FK af_roles.id)
-  permission_id : INT (FK af_permissions.id)
-  --
-  (role_id, permission_id) : PK
-}
-
-entity "af_user" as user {
-  uuid : UUID (PK)
-  email : TEXT
-  uid : BIGSERIAL
-  name : TEXT
-  created_at : TIMESTAMP WITH TIME ZONE
-}
-
-entity "af_workspace" as workspace {
-  workspace_id : UUID (PK)
-  database_storage_id : UUID
-  owner_uid : BIGINT (FK af_user.uid)
-  created_at : TIMESTAMP WITH TIME ZONE
-  workspace_type : INTEGER
-  workspace_name : TEXT
-}
-
-entity "af_workspace_member" as workspace_member {
-  uid : BIGINT
-  role_id : INT (FK af_roles.id)
-  workspace_id : UUID (FK af_workspace.workspace_id)
-  created_at : TIMESTAMP WITH TIME ZONE
-  updated_at : TIMESTAMP WITH TIME ZONE
-  --
-  (uid, workspace_id) : PK
-}
-
-entity "af_collab" as collab {
-  oid : TEXT (PK)
-  owner_uid : BIGINT
-  workspace_id : UUID (FK af_workspace.workspace_id)
-  access_level : INTEGER
-  created_at : TIMESTAMP WITH TIME ZONE
-}
-
-entity "af_collab_update" as collab_update {
-  oid : TEXT (FK af_collab.oid)
-  key : BIGSERIAL
-  value : BYTEA
-  value_size : INTEGER
-  partition_key : INTEGER
-  uid : BIGINT
-  md5 : TEXT
-  created_at : TIMESTAMP WITH TIME ZONE
-  workspace_id : UUID (FK af_workspace.workspace_id)
-  --
-  (oid, key, partition_key) : PK
-}
-
-
-entity "af_collab_update_document" as af_collab_update_document {
-  Inherits af_collab_update (partition_key = 0)
-}
-
-entity "af_collab_update_database" as af_collab_update_database {
-  Inherits af_collab_update (partition_key = 1)
-}
-
-entity "af_collab_update_w_database" as af_collab_update_w_database {
-  Inherits af_collab_update (partition_key = 2)
-}
-
-entity "af_collab_update_folder" as af_collab_update_folder {
-  Inherits af_collab_update (partition_key = 3)
-}
-
-af_collab_update_document -u-|> collab_update
-af_collab_update_database -u-|> collab_update
-af_collab_update_w_database -u-|> collab_update
-af_collab_update_folder -u-|> collab_update
-
-entity "af_database_row_update" as database_row_update {
-  oid : TEXT
-  key : BIGSERIAL
-  value : BYTEA
-  value_size : INTEGER
-  partition_key : INTEGER
-  uid : BIGINT
-  md5 : TEXT
-  workspace_id : UUID (FK af_workspace.workspace_id)
-  --
-  (oid, key) : PK
-}
-
-entity "af_collab_member" as collab_member {
-  uid : BIGINT (FK af_user.uid)
-  oid : TEXT (FK af_collab.oid)
-  role_id : INTEGER (FK af_roles.id)
-  --
-  (uid, oid) : PK
-}
-
-entity "af_collab_statistics" as collab_statistics {
-  oid : TEXT (PK)
-  edit_count : BIGINT
-}
-
-entity "af_collab_snapshot" as collab_snapshot {
-  sid : BIGSERIAL (PK)
-  oid : TEXT (FK af_collab.oid)
-  name : TEXT
-  blob : BYTEA
-  blob_size : INTEGER
-  edit_count : BIGINT
-  created_at : TIMESTAMP WITH TIME ZONE
-}
-
-
-roles <-- role_permissions : FK
-permissions <-u- role_permissions : FK
-user <-- collab : FK
-user <-- workspace : FK
-user <-- collab_member : FK
-roles <-- workspace_member : FK
-workspace <-- workspace_member : FK
-workspace <-- collab : FK
-workspace <-- database_row_update : FK
-collab <-- collab_update : FK
-collab <-- collab_snapshot: FK
-collab <-u- collab_member : FK
-collab <-- collab_statistics : PK
-roles <-- collab_member : FK
-
-
-@enduml
-
-@startuml
-title Triggers in Database Schema
-
-participant "af_user" as A
-participant "af_workspace" as B
-participant "af_workspace_member" as C
-participant "af_collab" as D
-participant "af_collab_update" as E
-participant "af_collab_member" as F
-participant "af_collab_statistics" as G
-participant "af_collab_snapshot" as H
-
-A -> B: create_af_workspace_trigger
-note right
-This trigger fires after an insert on af_user. It automatically creates a workspace
-with the uid of the new user as the owner_uid.
-end note
-
-B -> C: manage_af_workspace_member_role_trigger
-note right
-This trigger fires after an insert on af_workspace. It automatically
-creates a workspace member in the af_workspace_member table with the
-role 'Owner'.
-end note
-
-E -> D: insert_into_af_collab_trigger
-note right
-This trigger fires before an insert on af_collab_update.
-It checks if a corresponding collab exists in the af_collab table.
-If not, it creates one with the oid, uid, and current timestamp.
-end note
-
-D -> F: insert_into_af_collab_member_trigger
-note right
-This trigger fires after an insert on af_collab.
-It automatically adds the collab's owner to the af_collab_member
-table with the role 'Owner'.
-end note
-
-E -> G: af_collab_update_edit_count_trigger
-note right
- This trigger fires after an insert on af_collab_update.
- It increments the edit_count of the corresponding collab in
- the af_collab_statistics table.
-end note
-
-H -> G: af_collab_snapshot_update_edit_count_trigger
-note right
- This trigger fires after an insert on af_collab_snapshot.
- It sets the edit_count of the new snapshot to the current
- edit_count of the collab in the af_collab_statistics table.
-end note
-
-@enduml
-

BIN
frontend/rust-lib/flowy-server/docs/schema.png


+ 3 - 3
frontend/rust-lib/flowy-server/src/supabase/api/collab_storage.rs

@@ -21,16 +21,16 @@ use crate::supabase::api::util::{ExtendedResponse, InsertParamsBuilder};
 use crate::supabase::api::{PostgresWrapper, SupabaseServerService};
 use crate::supabase::define::*;
 
-pub struct RESTfulSupabaseCollabStorageImpl<T>(T);
+pub struct SupabaseCollabStorageImpl<T>(T);
 
-impl<T> RESTfulSupabaseCollabStorageImpl<T> {
+impl<T> SupabaseCollabStorageImpl<T> {
   pub fn new(server: T) -> Self {
     Self(server)
   }
 }
 
 #[async_trait]
-impl<T> RemoteCollabStorage for RESTfulSupabaseCollabStorageImpl<T>
+impl<T> RemoteCollabStorage for SupabaseCollabStorageImpl<T>
 where
   T: SupabaseServerService,
 {

+ 3 - 3
frontend/rust-lib/flowy-server/src/supabase/api/database.rs

@@ -12,17 +12,17 @@ use crate::supabase::api::request::{
 };
 use crate::supabase::api::SupabaseServerService;
 
-pub struct RESTfulSupabaseDatabaseServiceImpl<T> {
+pub struct SupabaseDatabaseServiceImpl<T> {
   server: T,
 }
 
-impl<T> RESTfulSupabaseDatabaseServiceImpl<T> {
+impl<T> SupabaseDatabaseServiceImpl<T> {
   pub fn new(server: T) -> Self {
     Self { server }
   }
 }
 
-impl<T> DatabaseCloudService for RESTfulSupabaseDatabaseServiceImpl<T>
+impl<T> DatabaseCloudService for SupabaseDatabaseServiceImpl<T>
 where
   T: SupabaseServerService,
 {

+ 4 - 4
frontend/rust-lib/flowy-server/src/supabase/api/document.rs

@@ -11,14 +11,14 @@ use lib_infra::future::FutureResult;
 use crate::supabase::api::request::{get_latest_snapshot_from_server, FetchObjectUpdateAction};
 use crate::supabase::api::SupabaseServerService;
 
-pub struct RESTfulSupabaseDocumentServiceImpl<T>(T);
-impl<T> RESTfulSupabaseDocumentServiceImpl<T> {
+pub struct SupabaseDocumentServiceImpl<T>(T);
+impl<T> SupabaseDocumentServiceImpl<T> {
   pub fn new(server: T) -> Self {
     Self(server)
   }
 }
 
-impl<T> DocumentCloudService for RESTfulSupabaseDocumentServiceImpl<T>
+impl<T> DocumentCloudService for SupabaseDocumentServiceImpl<T>
 where
   T: SupabaseServerService,
 {
@@ -31,7 +31,7 @@ where
         async move {
           let postgrest = try_get_postgrest?;
           let action = FetchObjectUpdateAction::new(document_id, CollabType::Document, postgrest);
-          action.run_with_fix_interval(5, 5).await
+          action.run_with_fix_interval(5, 10).await
         }
         .await,
       )

+ 3 - 3
frontend/rust-lib/flowy-server/src/supabase/api/folder.rs

@@ -19,15 +19,15 @@ use crate::supabase::api::util::{ExtendedResponse, InsertParamsBuilder};
 use crate::supabase::api::SupabaseServerService;
 use crate::supabase::define::*;
 
-pub struct RESTfulSupabaseFolderServiceImpl<T>(T);
+pub struct SupabaseFolderServiceImpl<T>(T);
 
-impl<T> RESTfulSupabaseFolderServiceImpl<T> {
+impl<T> SupabaseFolderServiceImpl<T> {
   pub fn new(server: T) -> Self {
     Self(server)
   }
 }
 
-impl<T> FolderCloudService for RESTfulSupabaseFolderServiceImpl<T>
+impl<T> FolderCloudService for SupabaseFolderServiceImpl<T>
 where
   T: SupabaseServerService,
 {

+ 17 - 7
frontend/rust-lib/flowy-server/src/supabase/api/request.rs

@@ -10,7 +10,7 @@ use chrono::{DateTime, Utc};
 use collab_plugins::cloud_storage::{CollabObject, CollabType, RemoteCollabSnapshot};
 use serde_json::Value;
 use tokio_retry::strategy::FixedInterval;
-use tokio_retry::{Action, Retry};
+use tokio_retry::{Action, Condition, RetryIf};
 
 use flowy_database_deps::cloud::{CollabObjectUpdate, CollabObjectUpdateByOid};
 use lib_infra::util::md5;
@@ -34,18 +34,20 @@ impl FetchObjectUpdateAction {
     }
   }
 
-  pub fn run(self) -> Retry<Take<FixedInterval>, FetchObjectUpdateAction> {
+  pub fn run(self) -> RetryIf<Take<FixedInterval>, FetchObjectUpdateAction, RetryCondition> {
+    let postgrest = self.postgrest.clone();
     let retry_strategy = FixedInterval::new(Duration::from_secs(5)).take(3);
-    Retry::spawn(retry_strategy, self)
+    RetryIf::spawn(retry_strategy, self, RetryCondition(postgrest))
   }
 
   pub fn run_with_fix_interval(
     self,
     secs: u64,
     times: usize,
-  ) -> Retry<Take<FixedInterval>, FetchObjectUpdateAction> {
+  ) -> RetryIf<Take<FixedInterval>, FetchObjectUpdateAction, RetryCondition> {
+    let postgrest = self.postgrest.clone();
     let retry_strategy = FixedInterval::new(Duration::from_secs(secs)).take(times);
-    Retry::spawn(retry_strategy, self)
+    RetryIf::spawn(retry_strategy, self, RetryCondition(postgrest))
   }
 }
 
@@ -89,9 +91,10 @@ impl BatchFetchObjectUpdateAction {
     }
   }
 
-  pub fn run(self) -> Retry<Take<FixedInterval>, BatchFetchObjectUpdateAction> {
+  pub fn run(self) -> RetryIf<Take<FixedInterval>, BatchFetchObjectUpdateAction, RetryCondition> {
+    let postgrest = self.postgrest.clone();
     let retry_strategy = FixedInterval::new(Duration::from_secs(5)).take(3);
-    Retry::spawn(retry_strategy, self)
+    RetryIf::spawn(retry_strategy, self, RetryCondition(postgrest))
   }
 }
 
@@ -302,3 +305,10 @@ fn decode_hex_string(s: &str) -> Option<Vec<u8>> {
   let s = s.strip_prefix("\\x")?;
   hex::decode(s).ok()
 }
+
+pub struct RetryCondition(Weak<PostgresWrapper>);
+impl Condition<anyhow::Error> for RetryCondition {
+  fn should_retry(&mut self, _error: &anyhow::Error) -> bool {
+    self.0.upgrade().is_some()
+  }
+}

+ 13 - 13
frontend/rust-lib/flowy-server/src/supabase/server.rs

@@ -10,9 +10,9 @@ use flowy_server_config::supabase_config::SupabaseConfiguration;
 use flowy_user_deps::cloud::UserService;
 
 use crate::supabase::api::{
-  RESTfulPostgresServer, RESTfulSupabaseCollabStorageImpl, RESTfulSupabaseDatabaseServiceImpl,
-  RESTfulSupabaseDocumentServiceImpl, RESTfulSupabaseFolderServiceImpl,
-  RESTfulSupabaseUserAuthServiceImpl, SupabaseServerServiceImpl,
+  RESTfulPostgresServer, RESTfulSupabaseUserAuthServiceImpl, SupabaseCollabStorageImpl,
+  SupabaseDatabaseServiceImpl, SupabaseDocumentServiceImpl, SupabaseFolderServiceImpl,
+  SupabaseServerServiceImpl,
 };
 use crate::AppFlowyServer;
 
@@ -96,25 +96,25 @@ impl AppFlowyServer for SupabaseServer {
   }
 
   fn folder_service(&self) -> Arc<dyn FolderCloudService> {
-    Arc::new(RESTfulSupabaseFolderServiceImpl::new(
-      SupabaseServerServiceImpl(self.restful_postgres.clone()),
-    ))
+    Arc::new(SupabaseFolderServiceImpl::new(SupabaseServerServiceImpl(
+      self.restful_postgres.clone(),
+    )))
   }
 
   fn database_service(&self) -> Arc<dyn DatabaseCloudService> {
-    Arc::new(RESTfulSupabaseDatabaseServiceImpl::new(
-      SupabaseServerServiceImpl(self.restful_postgres.clone()),
-    ))
+    Arc::new(SupabaseDatabaseServiceImpl::new(SupabaseServerServiceImpl(
+      self.restful_postgres.clone(),
+    )))
   }
 
   fn document_service(&self) -> Arc<dyn DocumentCloudService> {
-    Arc::new(RESTfulSupabaseDocumentServiceImpl::new(
-      SupabaseServerServiceImpl(self.restful_postgres.clone()),
-    ))
+    Arc::new(SupabaseDocumentServiceImpl::new(SupabaseServerServiceImpl(
+      self.restful_postgres.clone(),
+    )))
   }
 
   fn collab_storage(&self) -> Option<Arc<dyn RemoteCollabStorage>> {
-    Some(Arc::new(RESTfulSupabaseCollabStorageImpl::new(
+    Some(Arc::new(SupabaseCollabStorageImpl::new(
       SupabaseServerServiceImpl(self.restful_postgres.clone()),
     )))
   }

+ 5 - 5
frontend/rust-lib/flowy-server/tests/supabase_test/util.rs

@@ -7,8 +7,8 @@ use uuid::Uuid;
 use flowy_database_deps::cloud::DatabaseCloudService;
 use flowy_folder_deps::cloud::FolderCloudService;
 use flowy_server::supabase::api::{
-  RESTfulPostgresServer, RESTfulSupabaseCollabStorageImpl, RESTfulSupabaseDatabaseServiceImpl,
-  RESTfulSupabaseFolderServiceImpl, RESTfulSupabaseUserAuthServiceImpl, SupabaseServerServiceImpl,
+  RESTfulPostgresServer, RESTfulSupabaseUserAuthServiceImpl, SupabaseCollabStorageImpl,
+  SupabaseDatabaseServiceImpl, SupabaseFolderServiceImpl, SupabaseServerServiceImpl,
 };
 use flowy_server::supabase::define::{USER_EMAIL, USER_UUID};
 use flowy_server_config::supabase_config::SupabaseConfiguration;
@@ -25,7 +25,7 @@ pub fn get_supabase_config() -> Option<SupabaseConfiguration> {
 pub fn collab_service() -> Arc<dyn RemoteCollabStorage> {
   let config = SupabaseConfiguration::from_env().unwrap();
   let server = Arc::new(RESTfulPostgresServer::new(config));
-  Arc::new(RESTfulSupabaseCollabStorageImpl::new(
+  Arc::new(SupabaseCollabStorageImpl::new(
     SupabaseServerServiceImpl::new(server),
   ))
 }
@@ -33,7 +33,7 @@ pub fn collab_service() -> Arc<dyn RemoteCollabStorage> {
 pub fn database_service() -> Arc<dyn DatabaseCloudService> {
   let config = SupabaseConfiguration::from_env().unwrap();
   let server = Arc::new(RESTfulPostgresServer::new(config));
-  Arc::new(RESTfulSupabaseDatabaseServiceImpl::new(
+  Arc::new(SupabaseDatabaseServiceImpl::new(
     SupabaseServerServiceImpl::new(server),
   ))
 }
@@ -49,7 +49,7 @@ pub fn user_auth_service() -> Arc<dyn UserService> {
 pub fn folder_service() -> Arc<dyn FolderCloudService> {
   let config = SupabaseConfiguration::from_env().unwrap();
   let server = Arc::new(RESTfulPostgresServer::new(config));
-  Arc::new(RESTfulSupabaseFolderServiceImpl::new(
+  Arc::new(SupabaseFolderServiceImpl::new(
     SupabaseServerServiceImpl::new(server),
   ))
 }

+ 1 - 4
frontend/rust-lib/flowy-user/src/services/user_session.rs

@@ -584,10 +584,7 @@ impl UserSession {
     match KV::get_object::<Session>(&self.session_config.session_cache_key) {
       None => Err(FlowyError::new(
         ErrorCode::RecordNotFound,
-        format!(
-          "Can't find the value of {}, User is not logged in",
-          self.session_config.session_cache_key
-        ),
+        "User is not logged in",
       )),
       Some(session) => Ok(session),
     }

+ 13 - 0
frontend/scripts/makefile/desktop.toml

@@ -59,6 +59,19 @@ script = [
 ]
 script_runner = "@shell"
 
+[tasks.sdk-build.mac]
+private = true
+script = [
+  """
+    cd rust-lib/
+    rustup show
+    echo cargo build --package=dart-ffi --target ${RUST_COMPILE_TARGET} --features "${FLUTTER_DESKTOP_FEATURES}"
+    RUSTFLAGS="-C target-cpu=native -C link-arg=-mmacosx-version-min=11.0" cargo build --package=dart-ffi --target ${RUST_COMPILE_TARGET} --features "${FLUTTER_DESKTOP_FEATURES}"
+    cd ../
+  """,
+]
+script_runner = "@shell"
+
 [tasks.sdk-build-android]
 private = true
 script = [