فهرست منبع

refactor extract observable payload

appflowy 3 سال پیش
والد
کامیت
f18cc717ea
47فایلهای تغییر یافته به همراه1220 افزوده شده و 751 حذف شده
  1. 0 2
      app_flowy/lib/user/infrastructure/i_auth_impl.dart
  2. 10 10
      app_flowy/lib/welcome/presentation/welcome_screen.dart
  3. 1 0
      app_flowy/lib/workspace/application/menu/menu_bloc.dart
  4. 11 11
      app_flowy/lib/workspace/application/menu/menu_user_bloc.dart
  5. 110 0
      app_flowy/lib/workspace/application/workspace/welcome_bloc.dart
  6. 226 177
      app_flowy/lib/workspace/application/workspace/welcome_bloc.freezed.dart
  7. 0 91
      app_flowy/lib/workspace/application/workspace/workspace_list_bloc.dart
  8. 3 6
      app_flowy/lib/workspace/domain/i_user.dart
  9. 1 1
      app_flowy/lib/workspace/domain/i_view.dart
  10. 11 2
      app_flowy/lib/workspace/infrastructure/deps_resolver.dart
  11. 7 9
      app_flowy/lib/workspace/infrastructure/i_user_impl.dart
  12. 2 2
      app_flowy/lib/workspace/infrastructure/i_workspace_impl.dart
  13. 30 33
      app_flowy/lib/workspace/infrastructure/repos/app_repo.dart
  14. 35 0
      app_flowy/lib/workspace/infrastructure/repos/helper.dart
  15. 18 29
      app_flowy/lib/workspace/infrastructure/repos/user_repo.dart
  16. 23 21
      app_flowy/lib/workspace/infrastructure/repos/view_repo.dart
  17. 46 49
      app_flowy/lib/workspace/infrastructure/repos/workspace_repo.dart
  18. 17 0
      app_flowy/packages/flowy_sdk/lib/dispatch/code_gen.dart
  19. 50 23
      app_flowy/packages/flowy_sdk/lib/protobuf/flowy-observable/subject.pb.dart
  20. 6 4
      app_flowy/packages/flowy_sdk/lib/protobuf/flowy-observable/subject.pbjson.dart
  21. 2 0
      app_flowy/packages/flowy_sdk/lib/protobuf/flowy-workspace/event.pbenum.dart
  22. 2 1
      app_flowy/packages/flowy_sdk/lib/protobuf/flowy-workspace/event.pbjson.dart
  23. 67 4
      rust-lib/flowy-database/src/macros.rs
  24. 2 4
      rust-lib/flowy-document/src/sql_tables/doc/doc_sql.rs
  25. 8 4
      rust-lib/flowy-observable/src/entities/subject.rs
  26. 156 71
      rust-lib/flowy-observable/src/protobuf/model/subject.rs
  27. 3 2
      rust-lib/flowy-observable/src/protobuf/proto/subject.proto
  28. 12 2
      rust-lib/flowy-sdk/src/lib.rs
  29. 2 2
      rust-lib/flowy-user/src/services/user/user_session.rs
  30. 3 0
      rust-lib/flowy-workspace/Cargo.toml
  31. 16 13
      rust-lib/flowy-workspace/src/event.rs
  32. 1 2
      rust-lib/flowy-workspace/src/handlers/app_handler.rs
  33. 1 2
      rust-lib/flowy-workspace/src/handlers/view_handler.rs
  34. 11 1
      rust-lib/flowy-workspace/src/handlers/workspace_handler.rs
  35. 2 1
      rust-lib/flowy-workspace/src/module.rs
  36. 32 22
      rust-lib/flowy-workspace/src/observable/observable.rs
  37. 39 33
      rust-lib/flowy-workspace/src/protobuf/model/event.rs
  38. 1 0
      rust-lib/flowy-workspace/src/protobuf/proto/event.proto
  39. 30 10
      rust-lib/flowy-workspace/src/services/app_controller.rs
  40. 4 2
      rust-lib/flowy-workspace/src/services/helper.rs
  41. 33 9
      rust-lib/flowy-workspace/src/services/view_controller.rs
  42. 76 49
      rust-lib/flowy-workspace/src/services/workspace_controller.rs
  43. 29 10
      rust-lib/flowy-workspace/src/sql_tables/app/app_sql.rs
  44. 11 2
      rust-lib/flowy-workspace/src/sql_tables/app/app_table.rs
  45. 4 13
      rust-lib/flowy-workspace/src/sql_tables/view/view_sql.rs
  46. 58 22
      rust-lib/flowy-workspace/src/sql_tables/workspace/workspace_sql.rs
  47. 8 0
      rust-lib/flowy-workspace/src/sql_tables/workspace/workspace_table.rs

+ 0 - 2
app_flowy/lib/user/infrastructure/i_auth_impl.dart

@@ -1,8 +1,6 @@
 import 'package:app_flowy/startup/startup.dart';
 import 'package:app_flowy/user/presentation/sign_up_screen.dart';
 import 'package:app_flowy/welcome/domain/i_welcome.dart';
-import 'package:app_flowy/workspace/infrastructure/repos/user_repo.dart';
-import 'package:app_flowy/workspace/presentation/workspace/workspace_select_screen.dart';
 import 'package:dartz/dartz.dart';
 import 'package:flowy_infra_ui/widget/route/animation.dart';
 import 'package:flowy_sdk/protobuf/flowy-user/protobuf.dart';

+ 10 - 10
app_flowy/lib/welcome/presentation/welcome_screen.dart

@@ -1,4 +1,6 @@
-import 'package:app_flowy/workspace/application/workspace/workspace_list_bloc.dart';
+import 'package:app_flowy/startup/startup.dart';
+import 'package:app_flowy/workspace/application/workspace/welcome_bloc.dart';
+import 'package:app_flowy/workspace/domain/i_user.dart';
 import 'package:flowy_infra_ui/style_widget/scrolling/styled_list.dart';
 import 'package:flowy_infra_ui/style_widget/text_button.dart';
 import 'package:flowy_infra_ui/widget/error_page.dart';
@@ -17,9 +19,9 @@ class WelcomeScreen extends StatelessWidget {
   @override
   Widget build(BuildContext context) {
     return BlocProvider(
-      create: (_) =>
-          WorkspaceListBloc(repo)..add(const WorkspaceListEvent.initial()),
-      child: BlocBuilder<WorkspaceListBloc, WorkspaceListState>(
+      create: (_) => getIt<WelcomeBloc>(param1: repo.user)
+        ..add(const WelcomeEvent.initial()),
+      child: BlocBuilder<WelcomeBloc, WelcomeState>(
         builder: (context, state) {
           return Scaffold(
             body: Padding(
@@ -37,7 +39,7 @@ class WelcomeScreen extends StatelessWidget {
     );
   }
 
-  Widget _renderBody(WorkspaceListState state) {
+  Widget _renderBody(WelcomeState state) {
     final body = state.successOrFailure.fold(
       (_) => _renderList(state.workspaces),
       (error) => FlowyErrorPage(error.toString()),
@@ -54,8 +56,8 @@ class WelcomeScreen extends StatelessWidget {
         fontSize: 14,
         onPressed: () {
           context
-              .read<WorkspaceListBloc>()
-              .add(const WorkspaceListEvent.createWorkspace("workspace", ""));
+              .read<WelcomeBloc>()
+              .add(const WelcomeEvent.createWorkspace("workspace", ""));
         },
       ),
     );
@@ -77,9 +79,7 @@ class WelcomeScreen extends StatelessWidget {
   }
 
   void _handleOnPress(BuildContext context, Workspace workspace) {
-    context
-        .read<WorkspaceListBloc>()
-        .add(WorkspaceListEvent.openWorkspace(workspace));
+    context.read<WelcomeBloc>().add(WelcomeEvent.openWorkspace(workspace));
 
     Navigator.of(context).pop(workspace.id);
   }

+ 1 - 0
app_flowy/lib/workspace/application/menu/menu_bloc.dart

@@ -52,6 +52,7 @@ class MenuBloc extends Bloc<MenuEvent, MenuState> {
     );
   }
 
+  // ignore: unused_element
   Stream<MenuState> _fetchApps() async* {
     final appsOrFail = await workspace.getApps();
     yield appsOrFail.fold(

+ 11 - 11
app_flowy/lib/workspace/application/menu/menu_user_bloc.dart

@@ -16,17 +16,17 @@ class MenuUserBloc extends Bloc<MenuUserEvent, MenuUserState> {
   Stream<MenuUserState> mapEventToState(MenuUserEvent event) async* {
     yield* event.map(
       initial: (_) async* {
-        // fetch workspaces
-        iUserImpl.fetchWorkspaces().then((result) {
-          result.fold(
-            (workspaces) async* {
-              yield state.copyWith(workspaces: some(workspaces));
-            },
-            (error) async* {
-              yield state.copyWith(successOrFailure: right(error.msg));
-            },
-          );
-        });
+        // // fetch workspaces
+        // iUserImpl.fetchWorkspaces().then((result) {
+        //   result.fold(
+        //     (workspaces) async* {
+        //       yield state.copyWith(workspaces: some(workspaces));
+        //     },
+        //     (error) async* {
+        //       yield state.copyWith(successOrFailure: right(error.msg));
+        //     },
+        //   );
+        // });
       },
       fetchWorkspaces: (_FetchWorkspaces value) async* {},
     );

+ 110 - 0
app_flowy/lib/workspace/application/workspace/welcome_bloc.dart

@@ -0,0 +1,110 @@
+import 'package:app_flowy/workspace/domain/i_user.dart';
+import 'package:app_flowy/workspace/infrastructure/repos/user_repo.dart';
+import 'package:flowy_infra/flowy_logger.dart';
+import 'package:flowy_sdk/protobuf/flowy-workspace/errors.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-workspace/workspace_create.pb.dart';
+import 'package:freezed_annotation/freezed_annotation.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:dartz/dartz.dart';
+
+part 'welcome_bloc.freezed.dart';
+
+class WelcomeBloc extends Bloc<WelcomeEvent, WelcomeState> {
+  UserRepo repo;
+  IUserWorkspaceListWatch watcher;
+  WelcomeBloc({required this.repo, required this.watcher})
+      : super(WelcomeState.initial());
+
+  @override
+  Stream<WelcomeState> mapEventToState(
+    WelcomeEvent event,
+  ) async* {
+    yield* event.map(initial: (e) async* {
+      watcher.startWatching(
+        workspaceListUpdatedCallback: (workspacesOrFail) =>
+            _handleWorkspaceListUpdated(workspacesOrFail),
+      );
+      yield* _fetchWorkspaces();
+    }, openWorkspace: (e) async* {
+      yield* _openWorkspace(e.workspace);
+    }, createWorkspace: (e) async* {
+      yield* _createWorkspace(e.name, e.desc);
+    }, workspacesReveived: (e) async* {
+      yield e.workspacesOrFail.fold(
+        (workspaces) => state.copyWith(
+            workspaces: workspaces, successOrFailure: left(unit)),
+        (error) => state.copyWith(successOrFailure: right(error)),
+      );
+    });
+  }
+
+  Stream<WelcomeState> _fetchWorkspaces() async* {
+    final workspacesOrFailed = await repo.getWorkspaces();
+    yield workspacesOrFailed.fold(
+      (workspaces) =>
+          state.copyWith(workspaces: workspaces, successOrFailure: left(unit)),
+      (error) {
+        Log.error(error);
+        return state.copyWith(successOrFailure: right(error));
+      },
+    );
+  }
+
+  Stream<WelcomeState> _openWorkspace(Workspace workspace) async* {
+    final result = await repo.openWorkspace(workspace.id);
+    yield result.fold(
+      (workspaces) => state.copyWith(successOrFailure: left(unit)),
+      (error) {
+        Log.error(error);
+        return state.copyWith(successOrFailure: right(error));
+      },
+    );
+  }
+
+  Stream<WelcomeState> _createWorkspace(String name, String desc) async* {
+    final result = await repo.createWorkspace(name, desc);
+    yield result.fold(
+      (workspace) {
+        // add(const WelcomeEvent.fetchWorkspaces());
+        return state.copyWith(successOrFailure: left(unit));
+      },
+      (error) {
+        Log.error(error);
+        return state.copyWith(successOrFailure: right(error));
+      },
+    );
+  }
+
+  void _handleWorkspaceListUpdated(
+      Either<List<Workspace>, WorkspaceError> workspacesOrFail) {
+    add(WelcomeEvent.workspacesReveived(workspacesOrFail));
+  }
+}
+
+@freezed
+abstract class WelcomeEvent with _$WelcomeEvent {
+  const factory WelcomeEvent.initial() = Initial;
+  // const factory WelcomeEvent.fetchWorkspaces() = FetchWorkspace;
+  const factory WelcomeEvent.createWorkspace(String name, String desc) =
+      CreateWorkspace;
+  const factory WelcomeEvent.openWorkspace(Workspace workspace) = OpenWorkspace;
+
+  const factory WelcomeEvent.workspacesReveived(
+          Either<List<Workspace>, WorkspaceError> workspacesOrFail) =
+      WorkspacesReceived;
+}
+
+@freezed
+abstract class WelcomeState implements _$WelcomeState {
+  const factory WelcomeState({
+    required bool isLoading,
+    required List<Workspace> workspaces,
+    required Either<Unit, WorkspaceError> successOrFailure,
+  }) = _WelcomeState;
+
+  factory WelcomeState.initial() => WelcomeState(
+        isLoading: false,
+        workspaces: List.empty(),
+        successOrFailure: left(unit),
+      );
+}

+ 226 - 177
app_flowy/lib/workspace/application/workspace/workspace_list_bloc.freezed.dart → app_flowy/lib/workspace/application/workspace/welcome_bloc.freezed.dart

@@ -1,7 +1,7 @@
 // GENERATED CODE - DO NOT MODIFY BY HAND
 // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target
 
-part of 'workspace_list_bloc.dart';
+part of 'welcome_bloc.dart';
 
 // **************************************************************************
 // FreezedGenerator
@@ -13,17 +13,13 @@ final _privateConstructorUsedError = UnsupportedError(
     'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more informations: https://github.com/rrousselGit/freezed#custom-getters-and-methods');
 
 /// @nodoc
-class _$WorkspaceListEventTearOff {
-  const _$WorkspaceListEventTearOff();
+class _$WelcomeEventTearOff {
+  const _$WelcomeEventTearOff();
 
   Initial initial() {
     return const Initial();
   }
 
-  FetchWorkspace fetchWorkspaces() {
-    return const FetchWorkspace();
-  }
-
   CreateWorkspace createWorkspace(String name, String desc) {
     return CreateWorkspace(
       name,
@@ -36,64 +32,73 @@ class _$WorkspaceListEventTearOff {
       workspace,
     );
   }
+
+  WorkspacesReceived workspacesReveived(
+      Either<List<Workspace>, WorkspaceError> workspacesOrFail) {
+    return WorkspacesReceived(
+      workspacesOrFail,
+    );
+  }
 }
 
 /// @nodoc
-const $WorkspaceListEvent = _$WorkspaceListEventTearOff();
+const $WelcomeEvent = _$WelcomeEventTearOff();
 
 /// @nodoc
-mixin _$WorkspaceListEvent {
+mixin _$WelcomeEvent {
   @optionalTypeArgs
   TResult when<TResult extends Object?>({
     required TResult Function() initial,
-    required TResult Function() fetchWorkspaces,
     required TResult Function(String name, String desc) createWorkspace,
     required TResult Function(Workspace workspace) openWorkspace,
+    required TResult Function(
+            Either<List<Workspace>, WorkspaceError> workspacesOrFail)
+        workspacesReveived,
   }) =>
       throw _privateConstructorUsedError;
   @optionalTypeArgs
   TResult maybeWhen<TResult extends Object?>({
     TResult Function()? initial,
-    TResult Function()? fetchWorkspaces,
     TResult Function(String name, String desc)? createWorkspace,
     TResult Function(Workspace workspace)? openWorkspace,
+    TResult Function(Either<List<Workspace>, WorkspaceError> workspacesOrFail)?
+        workspacesReveived,
     required TResult orElse(),
   }) =>
       throw _privateConstructorUsedError;
   @optionalTypeArgs
   TResult map<TResult extends Object?>({
     required TResult Function(Initial value) initial,
-    required TResult Function(FetchWorkspace value) fetchWorkspaces,
     required TResult Function(CreateWorkspace value) createWorkspace,
     required TResult Function(OpenWorkspace value) openWorkspace,
+    required TResult Function(WorkspacesReceived value) workspacesReveived,
   }) =>
       throw _privateConstructorUsedError;
   @optionalTypeArgs
   TResult maybeMap<TResult extends Object?>({
     TResult Function(Initial value)? initial,
-    TResult Function(FetchWorkspace value)? fetchWorkspaces,
     TResult Function(CreateWorkspace value)? createWorkspace,
     TResult Function(OpenWorkspace value)? openWorkspace,
+    TResult Function(WorkspacesReceived value)? workspacesReveived,
     required TResult orElse(),
   }) =>
       throw _privateConstructorUsedError;
 }
 
 /// @nodoc
-abstract class $WorkspaceListEventCopyWith<$Res> {
-  factory $WorkspaceListEventCopyWith(
-          WorkspaceListEvent value, $Res Function(WorkspaceListEvent) then) =
-      _$WorkspaceListEventCopyWithImpl<$Res>;
+abstract class $WelcomeEventCopyWith<$Res> {
+  factory $WelcomeEventCopyWith(
+          WelcomeEvent value, $Res Function(WelcomeEvent) then) =
+      _$WelcomeEventCopyWithImpl<$Res>;
 }
 
 /// @nodoc
-class _$WorkspaceListEventCopyWithImpl<$Res>
-    implements $WorkspaceListEventCopyWith<$Res> {
-  _$WorkspaceListEventCopyWithImpl(this._value, this._then);
+class _$WelcomeEventCopyWithImpl<$Res> implements $WelcomeEventCopyWith<$Res> {
+  _$WelcomeEventCopyWithImpl(this._value, this._then);
 
-  final WorkspaceListEvent _value;
+  final WelcomeEvent _value;
   // ignore: unused_field
-  final $Res Function(WorkspaceListEvent) _then;
+  final $Res Function(WelcomeEvent) _then;
 }
 
 /// @nodoc
@@ -103,7 +108,7 @@ abstract class $InitialCopyWith<$Res> {
 }
 
 /// @nodoc
-class _$InitialCopyWithImpl<$Res> extends _$WorkspaceListEventCopyWithImpl<$Res>
+class _$InitialCopyWithImpl<$Res> extends _$WelcomeEventCopyWithImpl<$Res>
     implements $InitialCopyWith<$Res> {
   _$InitialCopyWithImpl(Initial _value, $Res Function(Initial) _then)
       : super(_value, (v) => _then(v as Initial));
@@ -119,7 +124,7 @@ class _$Initial implements Initial {
 
   @override
   String toString() {
-    return 'WorkspaceListEvent.initial()';
+    return 'WelcomeEvent.initial()';
   }
 
   @override
@@ -134,9 +139,11 @@ class _$Initial implements Initial {
   @optionalTypeArgs
   TResult when<TResult extends Object?>({
     required TResult Function() initial,
-    required TResult Function() fetchWorkspaces,
     required TResult Function(String name, String desc) createWorkspace,
     required TResult Function(Workspace workspace) openWorkspace,
+    required TResult Function(
+            Either<List<Workspace>, WorkspaceError> workspacesOrFail)
+        workspacesReveived,
   }) {
     return initial();
   }
@@ -145,9 +152,10 @@ class _$Initial implements Initial {
   @optionalTypeArgs
   TResult maybeWhen<TResult extends Object?>({
     TResult Function()? initial,
-    TResult Function()? fetchWorkspaces,
     TResult Function(String name, String desc)? createWorkspace,
     TResult Function(Workspace workspace)? openWorkspace,
+    TResult Function(Either<List<Workspace>, WorkspaceError> workspacesOrFail)?
+        workspacesReveived,
     required TResult orElse(),
   }) {
     if (initial != null) {
@@ -160,9 +168,9 @@ class _$Initial implements Initial {
   @optionalTypeArgs
   TResult map<TResult extends Object?>({
     required TResult Function(Initial value) initial,
-    required TResult Function(FetchWorkspace value) fetchWorkspaces,
     required TResult Function(CreateWorkspace value) createWorkspace,
     required TResult Function(OpenWorkspace value) openWorkspace,
+    required TResult Function(WorkspacesReceived value) workspacesReveived,
   }) {
     return initial(this);
   }
@@ -171,9 +179,9 @@ class _$Initial implements Initial {
   @optionalTypeArgs
   TResult maybeMap<TResult extends Object?>({
     TResult Function(Initial value)? initial,
-    TResult Function(FetchWorkspace value)? fetchWorkspaces,
     TResult Function(CreateWorkspace value)? createWorkspace,
     TResult Function(OpenWorkspace value)? openWorkspace,
+    TResult Function(WorkspacesReceived value)? workspacesReveived,
     required TResult orElse(),
   }) {
     if (initial != null) {
@@ -183,104 +191,10 @@ class _$Initial implements Initial {
   }
 }
 
-abstract class Initial implements WorkspaceListEvent {
+abstract class Initial implements WelcomeEvent {
   const factory Initial() = _$Initial;
 }
 
-/// @nodoc
-abstract class $FetchWorkspaceCopyWith<$Res> {
-  factory $FetchWorkspaceCopyWith(
-          FetchWorkspace value, $Res Function(FetchWorkspace) then) =
-      _$FetchWorkspaceCopyWithImpl<$Res>;
-}
-
-/// @nodoc
-class _$FetchWorkspaceCopyWithImpl<$Res>
-    extends _$WorkspaceListEventCopyWithImpl<$Res>
-    implements $FetchWorkspaceCopyWith<$Res> {
-  _$FetchWorkspaceCopyWithImpl(
-      FetchWorkspace _value, $Res Function(FetchWorkspace) _then)
-      : super(_value, (v) => _then(v as FetchWorkspace));
-
-  @override
-  FetchWorkspace get _value => super._value as FetchWorkspace;
-}
-
-/// @nodoc
-
-class _$FetchWorkspace implements FetchWorkspace {
-  const _$FetchWorkspace();
-
-  @override
-  String toString() {
-    return 'WorkspaceListEvent.fetchWorkspaces()';
-  }
-
-  @override
-  bool operator ==(dynamic other) {
-    return identical(this, other) || (other is FetchWorkspace);
-  }
-
-  @override
-  int get hashCode => runtimeType.hashCode;
-
-  @override
-  @optionalTypeArgs
-  TResult when<TResult extends Object?>({
-    required TResult Function() initial,
-    required TResult Function() fetchWorkspaces,
-    required TResult Function(String name, String desc) createWorkspace,
-    required TResult Function(Workspace workspace) openWorkspace,
-  }) {
-    return fetchWorkspaces();
-  }
-
-  @override
-  @optionalTypeArgs
-  TResult maybeWhen<TResult extends Object?>({
-    TResult Function()? initial,
-    TResult Function()? fetchWorkspaces,
-    TResult Function(String name, String desc)? createWorkspace,
-    TResult Function(Workspace workspace)? openWorkspace,
-    required TResult orElse(),
-  }) {
-    if (fetchWorkspaces != null) {
-      return fetchWorkspaces();
-    }
-    return orElse();
-  }
-
-  @override
-  @optionalTypeArgs
-  TResult map<TResult extends Object?>({
-    required TResult Function(Initial value) initial,
-    required TResult Function(FetchWorkspace value) fetchWorkspaces,
-    required TResult Function(CreateWorkspace value) createWorkspace,
-    required TResult Function(OpenWorkspace value) openWorkspace,
-  }) {
-    return fetchWorkspaces(this);
-  }
-
-  @override
-  @optionalTypeArgs
-  TResult maybeMap<TResult extends Object?>({
-    TResult Function(Initial value)? initial,
-    TResult Function(FetchWorkspace value)? fetchWorkspaces,
-    TResult Function(CreateWorkspace value)? createWorkspace,
-    TResult Function(OpenWorkspace value)? openWorkspace,
-    required TResult orElse(),
-  }) {
-    if (fetchWorkspaces != null) {
-      return fetchWorkspaces(this);
-    }
-    return orElse();
-  }
-}
-
-abstract class FetchWorkspace implements WorkspaceListEvent {
-  const factory FetchWorkspace() = _$FetchWorkspace;
-}
-
 /// @nodoc
 abstract class $CreateWorkspaceCopyWith<$Res> {
   factory $CreateWorkspaceCopyWith(
@@ -291,7 +205,7 @@ abstract class $CreateWorkspaceCopyWith<$Res> {
 
 /// @nodoc
 class _$CreateWorkspaceCopyWithImpl<$Res>
-    extends _$WorkspaceListEventCopyWithImpl<$Res>
+    extends _$WelcomeEventCopyWithImpl<$Res>
     implements $CreateWorkspaceCopyWith<$Res> {
   _$CreateWorkspaceCopyWithImpl(
       CreateWorkspace _value, $Res Function(CreateWorkspace) _then)
@@ -330,7 +244,7 @@ class _$CreateWorkspace implements CreateWorkspace {
 
   @override
   String toString() {
-    return 'WorkspaceListEvent.createWorkspace(name: $name, desc: $desc)';
+    return 'WelcomeEvent.createWorkspace(name: $name, desc: $desc)';
   }
 
   @override
@@ -358,9 +272,11 @@ class _$CreateWorkspace implements CreateWorkspace {
   @optionalTypeArgs
   TResult when<TResult extends Object?>({
     required TResult Function() initial,
-    required TResult Function() fetchWorkspaces,
     required TResult Function(String name, String desc) createWorkspace,
     required TResult Function(Workspace workspace) openWorkspace,
+    required TResult Function(
+            Either<List<Workspace>, WorkspaceError> workspacesOrFail)
+        workspacesReveived,
   }) {
     return createWorkspace(name, desc);
   }
@@ -369,9 +285,10 @@ class _$CreateWorkspace implements CreateWorkspace {
   @optionalTypeArgs
   TResult maybeWhen<TResult extends Object?>({
     TResult Function()? initial,
-    TResult Function()? fetchWorkspaces,
     TResult Function(String name, String desc)? createWorkspace,
     TResult Function(Workspace workspace)? openWorkspace,
+    TResult Function(Either<List<Workspace>, WorkspaceError> workspacesOrFail)?
+        workspacesReveived,
     required TResult orElse(),
   }) {
     if (createWorkspace != null) {
@@ -384,9 +301,9 @@ class _$CreateWorkspace implements CreateWorkspace {
   @optionalTypeArgs
   TResult map<TResult extends Object?>({
     required TResult Function(Initial value) initial,
-    required TResult Function(FetchWorkspace value) fetchWorkspaces,
     required TResult Function(CreateWorkspace value) createWorkspace,
     required TResult Function(OpenWorkspace value) openWorkspace,
+    required TResult Function(WorkspacesReceived value) workspacesReveived,
   }) {
     return createWorkspace(this);
   }
@@ -395,9 +312,9 @@ class _$CreateWorkspace implements CreateWorkspace {
   @optionalTypeArgs
   TResult maybeMap<TResult extends Object?>({
     TResult Function(Initial value)? initial,
-    TResult Function(FetchWorkspace value)? fetchWorkspaces,
     TResult Function(CreateWorkspace value)? createWorkspace,
     TResult Function(OpenWorkspace value)? openWorkspace,
+    TResult Function(WorkspacesReceived value)? workspacesReveived,
     required TResult orElse(),
   }) {
     if (createWorkspace != null) {
@@ -407,7 +324,7 @@ class _$CreateWorkspace implements CreateWorkspace {
   }
 }
 
-abstract class CreateWorkspace implements WorkspaceListEvent {
+abstract class CreateWorkspace implements WelcomeEvent {
   const factory CreateWorkspace(String name, String desc) = _$CreateWorkspace;
 
   String get name => throw _privateConstructorUsedError;
@@ -426,8 +343,7 @@ abstract class $OpenWorkspaceCopyWith<$Res> {
 }
 
 /// @nodoc
-class _$OpenWorkspaceCopyWithImpl<$Res>
-    extends _$WorkspaceListEventCopyWithImpl<$Res>
+class _$OpenWorkspaceCopyWithImpl<$Res> extends _$WelcomeEventCopyWithImpl<$Res>
     implements $OpenWorkspaceCopyWith<$Res> {
   _$OpenWorkspaceCopyWithImpl(
       OpenWorkspace _value, $Res Function(OpenWorkspace) _then)
@@ -459,7 +375,7 @@ class _$OpenWorkspace implements OpenWorkspace {
 
   @override
   String toString() {
-    return 'WorkspaceListEvent.openWorkspace(workspace: $workspace)';
+    return 'WelcomeEvent.openWorkspace(workspace: $workspace)';
   }
 
   @override
@@ -484,9 +400,11 @@ class _$OpenWorkspace implements OpenWorkspace {
   @optionalTypeArgs
   TResult when<TResult extends Object?>({
     required TResult Function() initial,
-    required TResult Function() fetchWorkspaces,
     required TResult Function(String name, String desc) createWorkspace,
     required TResult Function(Workspace workspace) openWorkspace,
+    required TResult Function(
+            Either<List<Workspace>, WorkspaceError> workspacesOrFail)
+        workspacesReveived,
   }) {
     return openWorkspace(workspace);
   }
@@ -495,9 +413,10 @@ class _$OpenWorkspace implements OpenWorkspace {
   @optionalTypeArgs
   TResult maybeWhen<TResult extends Object?>({
     TResult Function()? initial,
-    TResult Function()? fetchWorkspaces,
     TResult Function(String name, String desc)? createWorkspace,
     TResult Function(Workspace workspace)? openWorkspace,
+    TResult Function(Either<List<Workspace>, WorkspaceError> workspacesOrFail)?
+        workspacesReveived,
     required TResult orElse(),
   }) {
     if (openWorkspace != null) {
@@ -510,9 +429,9 @@ class _$OpenWorkspace implements OpenWorkspace {
   @optionalTypeArgs
   TResult map<TResult extends Object?>({
     required TResult Function(Initial value) initial,
-    required TResult Function(FetchWorkspace value) fetchWorkspaces,
     required TResult Function(CreateWorkspace value) createWorkspace,
     required TResult Function(OpenWorkspace value) openWorkspace,
+    required TResult Function(WorkspacesReceived value) workspacesReveived,
   }) {
     return openWorkspace(this);
   }
@@ -521,9 +440,9 @@ class _$OpenWorkspace implements OpenWorkspace {
   @optionalTypeArgs
   TResult maybeMap<TResult extends Object?>({
     TResult Function(Initial value)? initial,
-    TResult Function(FetchWorkspace value)? fetchWorkspaces,
     TResult Function(CreateWorkspace value)? createWorkspace,
     TResult Function(OpenWorkspace value)? openWorkspace,
+    TResult Function(WorkspacesReceived value)? workspacesReveived,
     required TResult orElse(),
   }) {
     if (openWorkspace != null) {
@@ -533,7 +452,7 @@ class _$OpenWorkspace implements OpenWorkspace {
   }
 }
 
-abstract class OpenWorkspace implements WorkspaceListEvent {
+abstract class OpenWorkspace implements WelcomeEvent {
   const factory OpenWorkspace(Workspace workspace) = _$OpenWorkspace;
 
   Workspace get workspace => throw _privateConstructorUsedError;
@@ -543,14 +462,146 @@ abstract class OpenWorkspace implements WorkspaceListEvent {
 }
 
 /// @nodoc
-class _$WorkspaceListStateTearOff {
-  const _$WorkspaceListStateTearOff();
+abstract class $WorkspacesReceivedCopyWith<$Res> {
+  factory $WorkspacesReceivedCopyWith(
+          WorkspacesReceived value, $Res Function(WorkspacesReceived) then) =
+      _$WorkspacesReceivedCopyWithImpl<$Res>;
+  $Res call({Either<List<Workspace>, WorkspaceError> workspacesOrFail});
+}
+
+/// @nodoc
+class _$WorkspacesReceivedCopyWithImpl<$Res>
+    extends _$WelcomeEventCopyWithImpl<$Res>
+    implements $WorkspacesReceivedCopyWith<$Res> {
+  _$WorkspacesReceivedCopyWithImpl(
+      WorkspacesReceived _value, $Res Function(WorkspacesReceived) _then)
+      : super(_value, (v) => _then(v as WorkspacesReceived));
+
+  @override
+  WorkspacesReceived get _value => super._value as WorkspacesReceived;
+
+  @override
+  $Res call({
+    Object? workspacesOrFail = freezed,
+  }) {
+    return _then(WorkspacesReceived(
+      workspacesOrFail == freezed
+          ? _value.workspacesOrFail
+          : workspacesOrFail // ignore: cast_nullable_to_non_nullable
+              as Either<List<Workspace>, WorkspaceError>,
+    ));
+  }
+}
+
+/// @nodoc
+
+class _$WorkspacesReceived implements WorkspacesReceived {
+  const _$WorkspacesReceived(this.workspacesOrFail);
+
+  @override
+  final Either<List<Workspace>, WorkspaceError> workspacesOrFail;
+
+  @override
+  String toString() {
+    return 'WelcomeEvent.workspacesReveived(workspacesOrFail: $workspacesOrFail)';
+  }
+
+  @override
+  bool operator ==(dynamic other) {
+    return identical(this, other) ||
+        (other is WorkspacesReceived &&
+            (identical(other.workspacesOrFail, workspacesOrFail) ||
+                const DeepCollectionEquality()
+                    .equals(other.workspacesOrFail, workspacesOrFail)));
+  }
+
+  @override
+  int get hashCode =>
+      runtimeType.hashCode ^
+      const DeepCollectionEquality().hash(workspacesOrFail);
+
+  @JsonKey(ignore: true)
+  @override
+  $WorkspacesReceivedCopyWith<WorkspacesReceived> get copyWith =>
+      _$WorkspacesReceivedCopyWithImpl<WorkspacesReceived>(this, _$identity);
+
+  @override
+  @optionalTypeArgs
+  TResult when<TResult extends Object?>({
+    required TResult Function() initial,
+    required TResult Function(String name, String desc) createWorkspace,
+    required TResult Function(Workspace workspace) openWorkspace,
+    required TResult Function(
+            Either<List<Workspace>, WorkspaceError> workspacesOrFail)
+        workspacesReveived,
+  }) {
+    return workspacesReveived(workspacesOrFail);
+  }
+
+  @override
+  @optionalTypeArgs
+  TResult maybeWhen<TResult extends Object?>({
+    TResult Function()? initial,
+    TResult Function(String name, String desc)? createWorkspace,
+    TResult Function(Workspace workspace)? openWorkspace,
+    TResult Function(Either<List<Workspace>, WorkspaceError> workspacesOrFail)?
+        workspacesReveived,
+    required TResult orElse(),
+  }) {
+    if (workspacesReveived != null) {
+      return workspacesReveived(workspacesOrFail);
+    }
+    return orElse();
+  }
+
+  @override
+  @optionalTypeArgs
+  TResult map<TResult extends Object?>({
+    required TResult Function(Initial value) initial,
+    required TResult Function(CreateWorkspace value) createWorkspace,
+    required TResult Function(OpenWorkspace value) openWorkspace,
+    required TResult Function(WorkspacesReceived value) workspacesReveived,
+  }) {
+    return workspacesReveived(this);
+  }
+
+  @override
+  @optionalTypeArgs
+  TResult maybeMap<TResult extends Object?>({
+    TResult Function(Initial value)? initial,
+    TResult Function(CreateWorkspace value)? createWorkspace,
+    TResult Function(OpenWorkspace value)? openWorkspace,
+    TResult Function(WorkspacesReceived value)? workspacesReveived,
+    required TResult orElse(),
+  }) {
+    if (workspacesReveived != null) {
+      return workspacesReveived(this);
+    }
+    return orElse();
+  }
+}
+
+abstract class WorkspacesReceived implements WelcomeEvent {
+  const factory WorkspacesReceived(
+          Either<List<Workspace>, WorkspaceError> workspacesOrFail) =
+      _$WorkspacesReceived;
+
+  Either<List<Workspace>, WorkspaceError> get workspacesOrFail =>
+      throw _privateConstructorUsedError;
+  @JsonKey(ignore: true)
+  $WorkspacesReceivedCopyWith<WorkspacesReceived> get copyWith =>
+      throw _privateConstructorUsedError;
+}
+
+/// @nodoc
+class _$WelcomeStateTearOff {
+  const _$WelcomeStateTearOff();
 
-  _WorkspaceListState call(
+  _WelcomeState call(
       {required bool isLoading,
       required List<Workspace> workspaces,
       required Either<Unit, WorkspaceError> successOrFailure}) {
-    return _WorkspaceListState(
+    return _WelcomeState(
       isLoading: isLoading,
       workspaces: workspaces,
       successOrFailure: successOrFailure,
@@ -559,25 +610,25 @@ class _$WorkspaceListStateTearOff {
 }
 
 /// @nodoc
-const $WorkspaceListState = _$WorkspaceListStateTearOff();
+const $WelcomeState = _$WelcomeStateTearOff();
 
 /// @nodoc
-mixin _$WorkspaceListState {
+mixin _$WelcomeState {
   bool get isLoading => throw _privateConstructorUsedError;
   List<Workspace> get workspaces => throw _privateConstructorUsedError;
   Either<Unit, WorkspaceError> get successOrFailure =>
       throw _privateConstructorUsedError;
 
   @JsonKey(ignore: true)
-  $WorkspaceListStateCopyWith<WorkspaceListState> get copyWith =>
+  $WelcomeStateCopyWith<WelcomeState> get copyWith =>
       throw _privateConstructorUsedError;
 }
 
 /// @nodoc
-abstract class $WorkspaceListStateCopyWith<$Res> {
-  factory $WorkspaceListStateCopyWith(
-          WorkspaceListState value, $Res Function(WorkspaceListState) then) =
-      _$WorkspaceListStateCopyWithImpl<$Res>;
+abstract class $WelcomeStateCopyWith<$Res> {
+  factory $WelcomeStateCopyWith(
+          WelcomeState value, $Res Function(WelcomeState) then) =
+      _$WelcomeStateCopyWithImpl<$Res>;
   $Res call(
       {bool isLoading,
       List<Workspace> workspaces,
@@ -585,13 +636,12 @@ abstract class $WorkspaceListStateCopyWith<$Res> {
 }
 
 /// @nodoc
-class _$WorkspaceListStateCopyWithImpl<$Res>
-    implements $WorkspaceListStateCopyWith<$Res> {
-  _$WorkspaceListStateCopyWithImpl(this._value, this._then);
+class _$WelcomeStateCopyWithImpl<$Res> implements $WelcomeStateCopyWith<$Res> {
+  _$WelcomeStateCopyWithImpl(this._value, this._then);
 
-  final WorkspaceListState _value;
+  final WelcomeState _value;
   // ignore: unused_field
-  final $Res Function(WorkspaceListState) _then;
+  final $Res Function(WelcomeState) _then;
 
   @override
   $Res call({
@@ -617,11 +667,11 @@ class _$WorkspaceListStateCopyWithImpl<$Res>
 }
 
 /// @nodoc
-abstract class _$WorkspaceListStateCopyWith<$Res>
-    implements $WorkspaceListStateCopyWith<$Res> {
-  factory _$WorkspaceListStateCopyWith(
-          _WorkspaceListState value, $Res Function(_WorkspaceListState) then) =
-      __$WorkspaceListStateCopyWithImpl<$Res>;
+abstract class _$WelcomeStateCopyWith<$Res>
+    implements $WelcomeStateCopyWith<$Res> {
+  factory _$WelcomeStateCopyWith(
+          _WelcomeState value, $Res Function(_WelcomeState) then) =
+      __$WelcomeStateCopyWithImpl<$Res>;
   @override
   $Res call(
       {bool isLoading,
@@ -630,15 +680,14 @@ abstract class _$WorkspaceListStateCopyWith<$Res>
 }
 
 /// @nodoc
-class __$WorkspaceListStateCopyWithImpl<$Res>
-    extends _$WorkspaceListStateCopyWithImpl<$Res>
-    implements _$WorkspaceListStateCopyWith<$Res> {
-  __$WorkspaceListStateCopyWithImpl(
-      _WorkspaceListState _value, $Res Function(_WorkspaceListState) _then)
-      : super(_value, (v) => _then(v as _WorkspaceListState));
+class __$WelcomeStateCopyWithImpl<$Res> extends _$WelcomeStateCopyWithImpl<$Res>
+    implements _$WelcomeStateCopyWith<$Res> {
+  __$WelcomeStateCopyWithImpl(
+      _WelcomeState _value, $Res Function(_WelcomeState) _then)
+      : super(_value, (v) => _then(v as _WelcomeState));
 
   @override
-  _WorkspaceListState get _value => super._value as _WorkspaceListState;
+  _WelcomeState get _value => super._value as _WelcomeState;
 
   @override
   $Res call({
@@ -646,7 +695,7 @@ class __$WorkspaceListStateCopyWithImpl<$Res>
     Object? workspaces = freezed,
     Object? successOrFailure = freezed,
   }) {
-    return _then(_WorkspaceListState(
+    return _then(_WelcomeState(
       isLoading: isLoading == freezed
           ? _value.isLoading
           : isLoading // ignore: cast_nullable_to_non_nullable
@@ -665,8 +714,8 @@ class __$WorkspaceListStateCopyWithImpl<$Res>
 
 /// @nodoc
 
-class _$_WorkspaceListState implements _WorkspaceListState {
-  const _$_WorkspaceListState(
+class _$_WelcomeState implements _WelcomeState {
+  const _$_WelcomeState(
       {required this.isLoading,
       required this.workspaces,
       required this.successOrFailure});
@@ -680,13 +729,13 @@ class _$_WorkspaceListState implements _WorkspaceListState {
 
   @override
   String toString() {
-    return 'WorkspaceListState(isLoading: $isLoading, workspaces: $workspaces, successOrFailure: $successOrFailure)';
+    return 'WelcomeState(isLoading: $isLoading, workspaces: $workspaces, successOrFailure: $successOrFailure)';
   }
 
   @override
   bool operator ==(dynamic other) {
     return identical(this, other) ||
-        (other is _WorkspaceListState &&
+        (other is _WelcomeState &&
             (identical(other.isLoading, isLoading) ||
                 const DeepCollectionEquality()
                     .equals(other.isLoading, isLoading)) &&
@@ -707,16 +756,16 @@ class _$_WorkspaceListState implements _WorkspaceListState {
 
   @JsonKey(ignore: true)
   @override
-  _$WorkspaceListStateCopyWith<_WorkspaceListState> get copyWith =>
-      __$WorkspaceListStateCopyWithImpl<_WorkspaceListState>(this, _$identity);
+  _$WelcomeStateCopyWith<_WelcomeState> get copyWith =>
+      __$WelcomeStateCopyWithImpl<_WelcomeState>(this, _$identity);
 }
 
-abstract class _WorkspaceListState implements WorkspaceListState {
-  const factory _WorkspaceListState(
+abstract class _WelcomeState implements WelcomeState {
+  const factory _WelcomeState(
           {required bool isLoading,
           required List<Workspace> workspaces,
           required Either<Unit, WorkspaceError> successOrFailure}) =
-      _$_WorkspaceListState;
+      _$_WelcomeState;
 
   @override
   bool get isLoading => throw _privateConstructorUsedError;
@@ -727,6 +776,6 @@ abstract class _WorkspaceListState implements WorkspaceListState {
       throw _privateConstructorUsedError;
   @override
   @JsonKey(ignore: true)
-  _$WorkspaceListStateCopyWith<_WorkspaceListState> get copyWith =>
+  _$WelcomeStateCopyWith<_WelcomeState> get copyWith =>
       throw _privateConstructorUsedError;
 }

+ 0 - 91
app_flowy/lib/workspace/application/workspace/workspace_list_bloc.dart

@@ -1,91 +0,0 @@
-import 'package:app_flowy/workspace/infrastructure/repos/user_repo.dart';
-import 'package:flowy_infra/flowy_logger.dart';
-import 'package:flowy_sdk/protobuf/flowy-workspace/errors.pb.dart';
-import 'package:flowy_sdk/protobuf/flowy-workspace/workspace_create.pb.dart';
-import 'package:freezed_annotation/freezed_annotation.dart';
-import 'package:flutter_bloc/flutter_bloc.dart';
-import 'package:dartz/dartz.dart';
-
-part 'workspace_list_bloc.freezed.dart';
-
-class WorkspaceListBloc extends Bloc<WorkspaceListEvent, WorkspaceListState> {
-  UserRepo repo;
-  WorkspaceListBloc(this.repo) : super(WorkspaceListState.initial());
-
-  @override
-  Stream<WorkspaceListState> mapEventToState(
-    WorkspaceListEvent event,
-  ) async* {
-    yield* event.map(initial: (e) async* {
-      yield* _fetchWorkspaces();
-    }, openWorkspace: (e) async* {
-      yield* _openWorkspace(e.workspace);
-    }, createWorkspace: (e) async* {
-      yield* _createWorkspace(e.name, e.desc);
-    }, fetchWorkspaces: (e) async* {
-      yield* _fetchWorkspaces();
-    });
-  }
-
-  Stream<WorkspaceListState> _fetchWorkspaces() async* {
-    final workspacesOrFailed = await repo.fetchWorkspaces();
-    yield workspacesOrFailed.fold(
-      (workspaces) =>
-          state.copyWith(workspaces: workspaces, successOrFailure: left(unit)),
-      (error) {
-        Log.error(error);
-        return state.copyWith(successOrFailure: right(error));
-      },
-    );
-  }
-
-  Stream<WorkspaceListState> _openWorkspace(Workspace workspace) async* {
-    final result = await repo.openWorkspace(workspace.id);
-    yield result.fold(
-      (workspaces) => state.copyWith(successOrFailure: left(unit)),
-      (error) {
-        Log.error(error);
-        return state.copyWith(successOrFailure: right(error));
-      },
-    );
-  }
-
-  Stream<WorkspaceListState> _createWorkspace(String name, String desc) async* {
-    final result = await repo.createWorkspace(name, desc);
-    yield result.fold(
-      (workspace) {
-        add(const WorkspaceListEvent.fetchWorkspaces());
-        return state.copyWith(successOrFailure: left(unit));
-      },
-      (error) {
-        Log.error(error);
-        return state.copyWith(successOrFailure: right(error));
-      },
-    );
-  }
-}
-
-@freezed
-abstract class WorkspaceListEvent with _$WorkspaceListEvent {
-  const factory WorkspaceListEvent.initial() = Initial;
-  const factory WorkspaceListEvent.fetchWorkspaces() = FetchWorkspace;
-  const factory WorkspaceListEvent.createWorkspace(String name, String desc) =
-      CreateWorkspace;
-  const factory WorkspaceListEvent.openWorkspace(Workspace workspace) =
-      OpenWorkspace;
-}
-
-@freezed
-abstract class WorkspaceListState implements _$WorkspaceListState {
-  const factory WorkspaceListState({
-    required bool isLoading,
-    required List<Workspace> workspaces,
-    required Either<Unit, WorkspaceError> successOrFailure,
-  }) = _WorkspaceListState;
-
-  factory WorkspaceListState.initial() => WorkspaceListState(
-        isLoading: false,
-        workspaces: List.empty(),
-        successOrFailure: left(unit),
-      );
-}

+ 3 - 6
app_flowy/lib/workspace/domain/i_user.dart

@@ -8,9 +8,7 @@ export 'package:flowy_sdk/protobuf/flowy-workspace/workspace_create.pb.dart';
 export 'package:flowy_sdk/protobuf/flowy-user/errors.pb.dart';
 export 'package:flowy_sdk/protobuf/flowy-user/user_profile.pb.dart';
 
-typedef UserCreateWorkspaceCallback = void Function(
-    Either<List<Workspace>, WorkspaceError> workspacesOrFailed);
-typedef UserDeleteWorkspaceCallback = void Function(
+typedef WorkspaceListUpdatedCallback = void Function(
     Either<List<Workspace>, WorkspaceError> workspacesOrFailed);
 
 abstract class IUser {
@@ -21,10 +19,9 @@ abstract class IUser {
   Future<Either<Unit, UserError>> signOut();
 }
 
-abstract class IUserWatch {
+abstract class IUserWorkspaceListWatch {
   void startWatching(
-      {UserCreateWorkspaceCallback? createWorkspaceCallback,
-      UserDeleteWorkspaceCallback? deleteWorkspaceCallback});
+      {WorkspaceListUpdatedCallback? workspaceListUpdatedCallback});
 
   Future<void> stopWatching();
 }

+ 1 - 1
app_flowy/lib/workspace/domain/i_view.dart

@@ -2,7 +2,7 @@ import 'package:flowy_sdk/protobuf/flowy-workspace/errors.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-workspace/view_create.pb.dart';
 import 'package:dartz/dartz.dart';
 
-typedef ViewUpdatedCallback = void Function(View view);
+typedef ViewUpdatedCallback = void Function(Either<View, WorkspaceError>);
 
 abstract class IView {
   Future<Either<View, WorkspaceError>> readView();

+ 11 - 2
app_flowy/lib/workspace/infrastructure/deps_resolver.dart

@@ -7,6 +7,7 @@ import 'package:app_flowy/workspace/application/menu/menu_watch.dart';
 import 'package:app_flowy/workspace/application/view/doc_watch_bloc.dart';
 import 'package:app_flowy/workspace/application/view/view_bloc.dart';
 import 'package:app_flowy/workspace/application/view/view_list_bloc.dart';
+import 'package:app_flowy/workspace/application/workspace/welcome_bloc.dart';
 import 'package:app_flowy/workspace/domain/i_doc.dart';
 import 'package:app_flowy/workspace/domain/i_view.dart';
 import 'package:app_flowy/workspace/domain/page_stack/page_stack.dart';
@@ -57,8 +58,9 @@ class HomeDepsResolver {
     // User
     getIt.registerFactoryParam<IUser, UserProfile, void>(
         (user, _) => IUserImpl(repo: UserRepo(user: user)));
-    getIt.registerFactoryParam<IUserWatch, UserProfile, void>(
-        (user, _) => IUserWatchImpl(repo: UserWatchRepo(user: user)));
+    getIt.registerFactoryParam<IUserWorkspaceListWatch, UserProfile, void>(
+        (user, _) =>
+            IUserWorkspaceListWatchImpl(repo: UserWatchRepo(user: user)));
 
     //Menu Bloc
     getIt.registerFactoryParam<MenuBloc, UserProfile, String>(
@@ -93,6 +95,13 @@ class HomeDepsResolver {
     getIt.registerFactoryParam<ViewListBloc, List<View>, void>(
         (views, _) => ViewListBloc(views: views));
 
+    getIt.registerFactoryParam<WelcomeBloc, UserProfile, void>(
+      (user, _) => WelcomeBloc(
+        repo: UserRepo(user: user),
+        watcher: getIt<IUserWorkspaceListWatch>(param1: user),
+      ),
+    );
+
     // getIt.registerFactoryParam<ViewBloc, String, void>(
     //     (viewId, _) => ViewBloc(iViewImpl: getIt<IView>(param1: viewId)));
   }

+ 7 - 9
app_flowy/lib/workspace/infrastructure/i_user_impl.dart

@@ -31,22 +31,20 @@ class IUserImpl extends IUser {
 
   @override
   Future<Either<List<Workspace>, WorkspaceError>> fetchWorkspaces() {
-    return repo.fetchWorkspaces();
+    return repo.getWorkspaces();
   }
 }
 
-class IUserWatchImpl extends IUserWatch {
+class IUserWorkspaceListWatchImpl extends IUserWorkspaceListWatch {
   UserWatchRepo repo;
-  IUserWatchImpl({
+  IUserWorkspaceListWatchImpl({
     required this.repo,
   });
   @override
-  void startWatching(
-      {UserCreateWorkspaceCallback? createWorkspaceCallback,
-      UserDeleteWorkspaceCallback? deleteWorkspaceCallback}) {
-    repo.startWatching(
-        createWorkspace: createWorkspaceCallback,
-        deleteWorkspace: deleteWorkspaceCallback);
+  void startWatching({
+    WorkspaceListUpdatedCallback? workspaceListUpdatedCallback,
+  }) {
+    repo.startWatching(workspaceListUpdated: workspaceListUpdatedCallback);
   }
 
   @override

+ 2 - 2
app_flowy/lib/workspace/infrastructure/i_workspace_impl.dart

@@ -20,9 +20,9 @@ class IWorkspaceImpl extends IWorkspace {
 
   @override
   Future<Either<List<App>, WorkspaceError>> getApps() {
-    return repo.getWorkspace().then((result) {
+    return repo.getApps().then((result) {
       return result.fold(
-        (workspace) => left(workspace.apps.items),
+        (apps) => left(apps),
         (error) => right(error),
       );
     });

+ 30 - 33
app_flowy/lib/workspace/infrastructure/repos/app_repo.dart

@@ -1,4 +1,5 @@
 import 'dart:async';
+import 'dart:typed_data';
 import 'package:app_flowy/workspace/domain/i_app.dart';
 import 'package:dartz/dartz.dart';
 import 'package:flowy_infra/flowy_logger.dart';
@@ -11,6 +12,7 @@ import 'package:flowy_sdk/protobuf/flowy-workspace/observable.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-workspace/view_create.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-workspace/view_create.pbenum.dart';
 import 'package:flowy_sdk/rust_stream.dart';
+import 'helper.dart';
 
 class AppRepository {
   String appId;
@@ -56,13 +58,12 @@ class AppWatchRepository {
   AppCreateViewCallback? _createView;
   AppDeleteViewCallback? _deleteView;
   AppUpdatedCallback? _update;
+  late ObservableExtractor _extractor;
   String appId;
-  late AppRepository _repo;
+
   AppWatchRepository({
     required this.appId,
-  }) {
-    _repo = AppRepository(appId: appId);
-  }
+  });
 
   void startWatching(
       {AppCreateViewCallback? createView,
@@ -71,52 +72,48 @@ class AppWatchRepository {
     _createView = createView;
     _deleteView = deleteView;
     _update = update;
-    _subscription = RustStreamReceiver.listen((observable) {
-      if (observable.subjectId != appId) {
-        return;
-      }
-
-      final ty = WorkspaceObservable.valueOf(observable.ty);
-      if (ty != null) {
-        _handleObservableType(ty);
-      }
-    });
+    _extractor = ObservableExtractor(
+      id: appId,
+      callback: (ty, result) => _handleObservableType(ty, result),
+    );
+    _subscription =
+        RustStreamReceiver.listen((observable) => _extractor.parse(observable));
   }
 
-  void _handleObservableType(WorkspaceObservable ty) {
+  void _handleObservableType(
+      WorkspaceObservable ty, Either<Uint8List, WorkspaceError> result) {
     switch (ty) {
       case WorkspaceObservable.AppCreateView:
-        if (_createView == null) {
-          return;
-        }
-        _repo.getViews().then((result) {
+        if (_createView != null) {
           result.fold(
-            (views) => _createView!(left(views)),
+            (payload) {
+              final repeatedView = RepeatedView.fromBuffer(payload);
+              _createView!(left(repeatedView.items));
+            },
             (error) => _createView!(right(error)),
           );
-        });
+        }
         break;
       case WorkspaceObservable.AppDeleteView:
-        if (_deleteView == null) {
-          return;
-        }
-        _repo.getViews().then((result) {
+        if (_deleteView != null) {
           result.fold(
-            (views) => _deleteView!(left(views)),
+            (payload) => _deleteView!(
+              left(RepeatedView.fromBuffer(payload).items),
+            ),
             (error) => _deleteView!(right(error)),
           );
-        });
+        }
         break;
       case WorkspaceObservable.AppUpdated:
-        if (_update == null) {
-          return;
-        }
-        _repo.getAppDesc().then((result) {
+        if (_update != null) {
           result.fold(
-            (app) => _update!(app.name, app.desc),
+            (payload) {
+              final app = App.fromBuffer(payload);
+              _update!(app.name, app.desc);
+            },
             (error) => Log.error(error),
           );
-        });
+        }
         break;
       default:
         break;

+ 35 - 0
app_flowy/lib/workspace/infrastructure/repos/helper.dart

@@ -0,0 +1,35 @@
+import 'dart:typed_data';
+import 'package:flowy_sdk/protobuf/flowy-observable/protobuf.dart';
+import 'package:flowy_sdk/protobuf/flowy-workspace/errors.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-workspace/observable.pb.dart';
+import 'package:dartz/dartz.dart';
+
+class ObservableExtractor {
+  String id;
+  void Function(WorkspaceObservable, Either<Uint8List, WorkspaceError>)
+      callback;
+
+  ObservableExtractor({required this.id, required this.callback});
+
+  void parse(ObservableSubject subject) {
+    if (subject.id != id) {
+      return;
+    }
+
+    final ty = WorkspaceObservable.valueOf(subject.ty);
+    if (ty == null) {
+      return;
+    }
+
+    if (subject.hasPayload()) {
+      final bytes = Uint8List.fromList(subject.error);
+      callback(ty, left(bytes));
+    } else if (subject.hasError()) {
+      final bytes = Uint8List.fromList(subject.error);
+      final error = WorkspaceError.fromBuffer(bytes);
+      callback(ty, right(error));
+    } else {
+      // do nothing
+    }
+  }
+}

+ 18 - 29
app_flowy/lib/workspace/infrastructure/repos/user_repo.dart

@@ -1,4 +1,5 @@
 import 'dart:async';
+import 'dart:typed_data';
 
 import 'package:dartz/dartz.dart';
 import 'package:flowy_sdk/dispatch/dispatch.dart';
@@ -32,7 +33,7 @@ class UserRepo {
     return UserEventSignOut().send();
   }
 
-  Future<Either<List<Workspace>, WorkspaceError>> fetchWorkspaces() {
+  Future<Either<List<Workspace>, WorkspaceError>> getWorkspaces() {
     final request = QueryWorkspaceRequest.create();
 
     return WorkspaceEventReadWorkspaces(request).send().then((result) {
@@ -69,8 +70,7 @@ class UserRepo {
 
 class UserWatchRepo {
   StreamSubscription<ObservableSubject>? _subscription;
-  UserCreateWorkspaceCallback? _createWorkspace;
-  UserDeleteWorkspaceCallback? _deleteWorkspace;
+  WorkspaceListUpdatedCallback? _workspaceListUpdated;
   late UserRepo _repo;
   UserWatchRepo({
     required UserProfile user,
@@ -78,19 +78,16 @@ class UserWatchRepo {
     _repo = UserRepo(user: user);
   }
 
-  void startWatching(
-      {UserCreateWorkspaceCallback? createWorkspace,
-      UserDeleteWorkspaceCallback? deleteWorkspace}) {
-    _createWorkspace = createWorkspace;
-    _deleteWorkspace = deleteWorkspace;
+  void startWatching({WorkspaceListUpdatedCallback? workspaceListUpdated}) {
+    _workspaceListUpdated = workspaceListUpdated;
     _subscription = RustStreamReceiver.listen((observable) {
-      if (observable.subjectId != _repo.user.id) {
+      if (observable.id != _repo.user.id) {
         return;
       }
 
       final ty = WorkspaceObservable.valueOf(observable.ty);
       if (ty != null) {
-        _handleObservableType(ty);
+        _handleObservableType(ty, Uint8List.fromList(observable.payload));
       }
     });
   }
@@ -99,31 +96,23 @@ class UserWatchRepo {
     await _subscription?.cancel();
   }
 
-  void _handleObservableType(WorkspaceObservable ty) {
+  void _handleObservableType(WorkspaceObservable ty, Uint8List payload) {
+    if (_workspaceListUpdated == null) {
+      return;
+    }
+
     switch (ty) {
       case WorkspaceObservable.UserCreateWorkspace:
-        if (_createWorkspace == null) {
-          return;
-        }
-        _repo.fetchWorkspaces().then((result) {
-          result.fold(
-            (workspaces) => _createWorkspace!(left(workspaces)),
-            (error) => _createWorkspace!(right(error)),
-          );
-        });
-        break;
       case WorkspaceObservable.UserDeleteWorkspace:
-        if (_deleteWorkspace == null) {
+      case WorkspaceObservable.WorkspaceListUpdated:
+        if (_workspaceListUpdated == null) {
           return;
         }
-        _repo.fetchWorkspaces().then((result) {
-          result.fold(
-            (workspaces) => _deleteWorkspace!(left(workspaces)),
-            (error) => _deleteWorkspace!(right(error)),
-          );
-        });
-        break;
 
+        final workspaces = RepeatedWorkspace.fromBuffer(payload);
+        _workspaceListUpdated!(left(workspaces.items));
+
+        break;
       default:
         break;
     }

+ 23 - 21
app_flowy/lib/workspace/infrastructure/repos/view_repo.dart

@@ -1,4 +1,5 @@
 import 'dart:async';
+import 'dart:typed_data';
 
 import 'package:dartz/dartz.dart';
 import 'package:flowy_infra/flowy_logger.dart';
@@ -12,6 +13,8 @@ import 'package:flowy_sdk/rust_stream.dart';
 
 import 'package:app_flowy/workspace/domain/i_view.dart';
 
+import 'helper.dart';
+
 class ViewRepository {
   View view;
   ViewRepository({
@@ -27,42 +30,41 @@ class ViewRepository {
 class ViewWatchRepository {
   StreamSubscription<ObservableSubject>? _subscription;
   ViewUpdatedCallback? _update;
+  late ObservableExtractor _extractor;
   View view;
-  late ViewRepository _repo;
+
   ViewWatchRepository({
     required this.view,
-  }) {
-    _repo = ViewRepository(view: view);
-  }
+  });
 
   void startWatching({
     ViewUpdatedCallback? update,
   }) {
     _update = update;
-    _subscription = RustStreamReceiver.listen((observable) {
-      if (observable.subjectId != view.id) {
-        return;
-      }
+    _extractor = ObservableExtractor(
+      id: view.id,
+      callback: (ty, result) {
+        _handleObservableType(ty, result);
+      },
+    );
 
-      final ty = WorkspaceObservable.valueOf(observable.ty);
-      if (ty != null) {
-        _handleObservableType(ty);
-      }
-    });
+    _subscription =
+        RustStreamReceiver.listen((observable) => _extractor.parse(observable));
   }
 
-  void _handleObservableType(WorkspaceObservable ty) {
+  void _handleObservableType(
+      WorkspaceObservable ty, Either<Uint8List, WorkspaceError> result) {
     switch (ty) {
       case WorkspaceObservable.ViewUpdated:
-        if (_update == null) {
-          return;
-        }
-        _repo.readView().then((result) {
+        if (_update != null) {
           result.fold(
-            (view) => _update!(view),
-            (error) => Log.error(error),
+            (payload) {
+              final view = View.fromBuffer(payload);
+              _update!(left(view));
+            },
+            (error) => _update!(right(error)),
           );
-        });
+        }
         break;
       default:
         break;

+ 46 - 49
app_flowy/lib/workspace/infrastructure/repos/workspace_repo.dart

@@ -1,4 +1,5 @@
 import 'dart:async';
+import 'dart:typed_data';
 
 import 'package:dartz/dartz.dart';
 import 'package:flowy_infra/flowy_logger.dart';
@@ -14,6 +15,8 @@ import 'package:flowy_sdk/rust_stream.dart';
 
 import 'package:app_flowy/workspace/domain/i_workspace.dart';
 
+import 'helper.dart';
+
 class WorkspaceRepo {
   UserProfile user;
   String workspaceId;
@@ -23,25 +26,15 @@ class WorkspaceRepo {
   });
 
   Future<Either<App, WorkspaceError>> createApp(String appName, String desc) {
-    return WorkspaceEventReadCurWorkspace().send().then((result) {
-      return result.fold(
-        (workspace) {
-          final request = CreateAppRequest.create()
-            ..name = appName
-            ..workspaceId = workspace.id
-            ..desc = desc;
-          return WorkspaceEventCreateApp(request).send();
-        },
-        (error) {
-          return right(error);
-        },
-      );
-    });
+    final request = CreateAppRequest.create()
+      ..name = appName
+      ..workspaceId = workspaceId
+      ..desc = desc;
+    return WorkspaceEventCreateApp(request).send();
   }
 
   Future<Either<Workspace, WorkspaceError>> getWorkspace() {
     final request = QueryWorkspaceRequest.create()..workspaceId = workspaceId;
-
     return WorkspaceEventReadWorkspaces(request).send().then((result) {
       return result.fold(
         (workspaces) {
@@ -57,6 +50,16 @@ class WorkspaceRepo {
       );
     });
   }
+
+  Future<Either<List<App>, WorkspaceError>> getApps() {
+    final request = QueryWorkspaceRequest.create()..workspaceId = workspaceId;
+    return WorkspaceEventReadWorkspaceApps(request).send().then((result) {
+      return result.fold(
+        (apps) => left(apps.items),
+        (error) => right(error),
+      );
+    });
+  }
 }
 
 class WorkspaceWatchRepo {
@@ -64,16 +67,14 @@ class WorkspaceWatchRepo {
   WorkspaceCreateAppCallback? _createApp;
   WorkspaceDeleteAppCallback? _deleteApp;
   WorkspaceUpdatedCallback? _update;
+  late ObservableExtractor _extractor;
   final UserProfile user;
   final String workspaceId;
-  late WorkspaceRepo _repo;
 
   WorkspaceWatchRepo({
     required this.user,
     required this.workspaceId,
-  }) {
-    _repo = WorkspaceRepo(user: user, workspaceId: workspaceId);
-  }
+  });
 
   void startWatching({
     WorkspaceCreateAppCallback? createApp,
@@ -84,54 +85,50 @@ class WorkspaceWatchRepo {
     _deleteApp = deleteApp;
     _update = update;
 
-    _subscription = RustStreamReceiver.listen((observable) {
-      if (observable.subjectId != workspaceId) {
-        return;
-      }
+    _extractor = ObservableExtractor(
+      id: workspaceId,
+      callback: (ty, result) {
+        _handleObservableType(ty, result);
+      },
+    );
 
-      final ty = WorkspaceObservable.valueOf(observable.ty);
-      if (ty != null) {
-        _handleObservableType(ty);
-      }
-    });
+    _subscription =
+        RustStreamReceiver.listen((observable) => _extractor.parse(observable));
   }
 
-  void _handleObservableType(WorkspaceObservable ty) {
+  void _handleObservableType(
+      WorkspaceObservable ty, Either<Uint8List, WorkspaceError> result) {
     switch (ty) {
       case WorkspaceObservable.WorkspaceUpdated:
-        if (_update == null) {
-          return;
-        }
-        _repo.getWorkspace().then((result) {
+        if (_update != null) {
           result.fold(
-            (workspace) => _update!(workspace.name, workspace.desc),
+            (payload) {
+              final workspace = Workspace.fromBuffer(payload);
+              _update!(workspace.name, workspace.desc);
+            },
             (error) => Log.error(error),
           );
-        });
+        }
         break;
       case WorkspaceObservable.WorkspaceCreateApp:
-        if (_createApp == null) {
-          return;
-        }
-
-        _repo.getWorkspace().then((result) {
+        if (_createApp != null) {
           result.fold(
-            (workspace) => _createApp!(left(workspace.apps.items)),
+            (payload) => _createApp!(
+              left(RepeatedApp.fromBuffer(payload).items),
+            ),
             (error) => _createApp!(right(error)),
           );
-        });
-
+        }
         break;
       case WorkspaceObservable.WorkspaceDeleteApp:
-        if (_deleteApp == null) {
-          return;
-        }
-        _repo.getWorkspace().then((result) {
+        if (_deleteApp != null) {
           result.fold(
-            (workspace) => _deleteApp!(left(workspace.apps.items)),
+            (payload) => _deleteApp!(
+              left(RepeatedApp.fromBuffer(payload).items),
+            ),
             (error) => _deleteApp!(right(error)),
           );
-        });
+        }
         break;
       default:
         break;

+ 17 - 0
app_flowy/packages/flowy_sdk/lib/dispatch/code_gen.dart

@@ -84,6 +84,23 @@ class WorkspaceEventOpenWorkspace {
     }
 }
 
+class WorkspaceEventReadWorkspaceApps {
+     QueryWorkspaceRequest request;
+     WorkspaceEventReadWorkspaceApps(this.request);
+
+    Future<Either<RepeatedApp, WorkspaceError>> send() {
+    final request = FFIRequest.create()
+          ..event = WorkspaceEvent.ReadWorkspaceApps.toString()
+          ..payload = requestToBytes(this.request);
+
+    return Dispatch.asyncRequest(request)
+        .then((bytesResult) => bytesResult.fold(
+           (okBytes) => left(RepeatedApp.fromBuffer(okBytes)),
+           (errBytes) => right(WorkspaceError.fromBuffer(errBytes)),
+        ));
+    }
+}
+
 class WorkspaceEventCreateApp {
      CreateAppRequest request;
      WorkspaceEventCreateApp(this.request);

+ 50 - 23
app_flowy/packages/flowy_sdk/lib/protobuf/flowy-observable/subject.pb.dart

@@ -9,22 +9,33 @@ import 'dart:core' as $core;
 
 import 'package:protobuf/protobuf.dart' as $pb;
 
-enum ObservableSubject_OneOfSubjectPayload {
-  subjectPayload, 
+enum ObservableSubject_OneOfPayload {
+  payload, 
+  notSet
+}
+
+enum ObservableSubject_OneOfError {
+  error, 
   notSet
 }
 
 class ObservableSubject extends $pb.GeneratedMessage {
-  static const $core.Map<$core.int, ObservableSubject_OneOfSubjectPayload> _ObservableSubject_OneOfSubjectPayloadByTag = {
-    4 : ObservableSubject_OneOfSubjectPayload.subjectPayload,
-    0 : ObservableSubject_OneOfSubjectPayload.notSet
+  static const $core.Map<$core.int, ObservableSubject_OneOfPayload> _ObservableSubject_OneOfPayloadByTag = {
+    4 : ObservableSubject_OneOfPayload.payload,
+    0 : ObservableSubject_OneOfPayload.notSet
+  };
+  static const $core.Map<$core.int, ObservableSubject_OneOfError> _ObservableSubject_OneOfErrorByTag = {
+    5 : ObservableSubject_OneOfError.error,
+    0 : ObservableSubject_OneOfError.notSet
   };
   static final $pb.BuilderInfo _i = $pb.BuilderInfo(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'ObservableSubject', createEmptyInstance: create)
     ..oo(0, [4])
+    ..oo(1, [5])
     ..aOS(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'category')
     ..a<$core.int>(2, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'ty', $pb.PbFieldType.O3)
-    ..aOS(3, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'subjectId')
-    ..a<$core.List<$core.int>>(4, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'subjectPayload', $pb.PbFieldType.OY)
+    ..aOS(3, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'id')
+    ..a<$core.List<$core.int>>(4, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'payload', $pb.PbFieldType.OY)
+    ..a<$core.List<$core.int>>(5, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'error', $pb.PbFieldType.OY)
     ..hasRequiredFields = false
   ;
 
@@ -32,8 +43,9 @@ class ObservableSubject extends $pb.GeneratedMessage {
   factory ObservableSubject({
     $core.String? category,
     $core.int? ty,
-    $core.String? subjectId,
-    $core.List<$core.int>? subjectPayload,
+    $core.String? id,
+    $core.List<$core.int>? payload,
+    $core.List<$core.int>? error,
   }) {
     final _result = create();
     if (category != null) {
@@ -42,11 +54,14 @@ class ObservableSubject extends $pb.GeneratedMessage {
     if (ty != null) {
       _result.ty = ty;
     }
-    if (subjectId != null) {
-      _result.subjectId = subjectId;
+    if (id != null) {
+      _result.id = id;
     }
-    if (subjectPayload != null) {
-      _result.subjectPayload = subjectPayload;
+    if (payload != null) {
+      _result.payload = payload;
+    }
+    if (error != null) {
+      _result.error = error;
     }
     return _result;
   }
@@ -71,8 +86,11 @@ class ObservableSubject extends $pb.GeneratedMessage {
   static ObservableSubject getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<ObservableSubject>(create);
   static ObservableSubject? _defaultInstance;
 
-  ObservableSubject_OneOfSubjectPayload whichOneOfSubjectPayload() => _ObservableSubject_OneOfSubjectPayloadByTag[$_whichOneof(0)]!;
-  void clearOneOfSubjectPayload() => clearField($_whichOneof(0));
+  ObservableSubject_OneOfPayload whichOneOfPayload() => _ObservableSubject_OneOfPayloadByTag[$_whichOneof(0)]!;
+  void clearOneOfPayload() => clearField($_whichOneof(0));
+
+  ObservableSubject_OneOfError whichOneOfError() => _ObservableSubject_OneOfErrorByTag[$_whichOneof(1)]!;
+  void clearOneOfError() => clearField($_whichOneof(1));
 
   @$pb.TagNumber(1)
   $core.String get category => $_getSZ(0);
@@ -93,21 +111,30 @@ class ObservableSubject extends $pb.GeneratedMessage {
   void clearTy() => clearField(2);
 
   @$pb.TagNumber(3)
-  $core.String get subjectId => $_getSZ(2);
+  $core.String get id => $_getSZ(2);
   @$pb.TagNumber(3)
-  set subjectId($core.String v) { $_setString(2, v); }
+  set id($core.String v) { $_setString(2, v); }
   @$pb.TagNumber(3)
-  $core.bool hasSubjectId() => $_has(2);
+  $core.bool hasId() => $_has(2);
   @$pb.TagNumber(3)
-  void clearSubjectId() => clearField(3);
+  void clearId() => clearField(3);
 
   @$pb.TagNumber(4)
-  $core.List<$core.int> get subjectPayload => $_getN(3);
+  $core.List<$core.int> get payload => $_getN(3);
   @$pb.TagNumber(4)
-  set subjectPayload($core.List<$core.int> v) { $_setBytes(3, v); }
+  set payload($core.List<$core.int> v) { $_setBytes(3, v); }
   @$pb.TagNumber(4)
-  $core.bool hasSubjectPayload() => $_has(3);
+  $core.bool hasPayload() => $_has(3);
   @$pb.TagNumber(4)
-  void clearSubjectPayload() => clearField(4);
+  void clearPayload() => clearField(4);
+
+  @$pb.TagNumber(5)
+  $core.List<$core.int> get error => $_getN(4);
+  @$pb.TagNumber(5)
+  set error($core.List<$core.int> v) { $_setBytes(4, v); }
+  @$pb.TagNumber(5)
+  $core.bool hasError() => $_has(4);
+  @$pb.TagNumber(5)
+  void clearError() => clearField(5);
 }
 

+ 6 - 4
app_flowy/packages/flowy_sdk/lib/protobuf/flowy-observable/subject.pbjson.dart

@@ -14,13 +14,15 @@ const ObservableSubject$json = const {
   '2': const [
     const {'1': 'category', '3': 1, '4': 1, '5': 9, '10': 'category'},
     const {'1': 'ty', '3': 2, '4': 1, '5': 5, '10': 'ty'},
-    const {'1': 'subject_id', '3': 3, '4': 1, '5': 9, '10': 'subjectId'},
-    const {'1': 'subject_payload', '3': 4, '4': 1, '5': 12, '9': 0, '10': 'subjectPayload'},
+    const {'1': 'id', '3': 3, '4': 1, '5': 9, '10': 'id'},
+    const {'1': 'payload', '3': 4, '4': 1, '5': 12, '9': 0, '10': 'payload'},
+    const {'1': 'error', '3': 5, '4': 1, '5': 12, '9': 1, '10': 'error'},
   ],
   '8': const [
-    const {'1': 'one_of_subject_payload'},
+    const {'1': 'one_of_payload'},
+    const {'1': 'one_of_error'},
   ],
 };
 
 /// Descriptor for `ObservableSubject`. Decode as a `google.protobuf.DescriptorProto`.
-final $typed_data.Uint8List observableSubjectDescriptor = $convert.base64Decode('ChFPYnNlcnZhYmxlU3ViamVjdBIaCghjYXRlZ29yeRgBIAEoCVIIY2F0ZWdvcnkSDgoCdHkYAiABKAVSAnR5Eh0KCnN1YmplY3RfaWQYAyABKAlSCXN1YmplY3RJZBIpCg9zdWJqZWN0X3BheWxvYWQYBCABKAxIAFIOc3ViamVjdFBheWxvYWRCGAoWb25lX29mX3N1YmplY3RfcGF5bG9hZA==');
+final $typed_data.Uint8List observableSubjectDescriptor = $convert.base64Decode('ChFPYnNlcnZhYmxlU3ViamVjdBIaCghjYXRlZ29yeRgBIAEoCVIIY2F0ZWdvcnkSDgoCdHkYAiABKAVSAnR5Eg4KAmlkGAMgASgJUgJpZBIaCgdwYXlsb2FkGAQgASgMSABSB3BheWxvYWQSFgoFZXJyb3IYBSABKAxIAVIFZXJyb3JCEAoOb25lX29mX3BheWxvYWRCDgoMb25lX29mX2Vycm9y');

+ 2 - 0
app_flowy/packages/flowy_sdk/lib/protobuf/flowy-workspace/event.pbenum.dart

@@ -15,6 +15,7 @@ class WorkspaceEvent extends $pb.ProtobufEnum {
   static const WorkspaceEvent ReadWorkspaces = WorkspaceEvent._(2, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'ReadWorkspaces');
   static const WorkspaceEvent DeleteWorkspace = WorkspaceEvent._(3, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'DeleteWorkspace');
   static const WorkspaceEvent OpenWorkspace = WorkspaceEvent._(4, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'OpenWorkspace');
+  static const WorkspaceEvent ReadWorkspaceApps = WorkspaceEvent._(5, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'ReadWorkspaceApps');
   static const WorkspaceEvent CreateApp = WorkspaceEvent._(101, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'CreateApp');
   static const WorkspaceEvent DeleteApp = WorkspaceEvent._(102, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'DeleteApp');
   static const WorkspaceEvent ReadApp = WorkspaceEvent._(103, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'ReadApp');
@@ -30,6 +31,7 @@ class WorkspaceEvent extends $pb.ProtobufEnum {
     ReadWorkspaces,
     DeleteWorkspace,
     OpenWorkspace,
+    ReadWorkspaceApps,
     CreateApp,
     DeleteApp,
     ReadApp,

+ 2 - 1
app_flowy/packages/flowy_sdk/lib/protobuf/flowy-workspace/event.pbjson.dart

@@ -17,6 +17,7 @@ const WorkspaceEvent$json = const {
     const {'1': 'ReadWorkspaces', '2': 2},
     const {'1': 'DeleteWorkspace', '2': 3},
     const {'1': 'OpenWorkspace', '2': 4},
+    const {'1': 'ReadWorkspaceApps', '2': 5},
     const {'1': 'CreateApp', '2': 101},
     const {'1': 'DeleteApp', '2': 102},
     const {'1': 'ReadApp', '2': 103},
@@ -29,4 +30,4 @@ const WorkspaceEvent$json = const {
 };
 
 /// Descriptor for `WorkspaceEvent`. Decode as a `google.protobuf.EnumDescriptorProto`.
-final $typed_data.Uint8List workspaceEventDescriptor = $convert.base64Decode('Cg5Xb3Jrc3BhY2VFdmVudBITCg9DcmVhdGVXb3Jrc3BhY2UQABIUChBSZWFkQ3VyV29ya3NwYWNlEAESEgoOUmVhZFdvcmtzcGFjZXMQAhITCg9EZWxldGVXb3Jrc3BhY2UQAxIRCg1PcGVuV29ya3NwYWNlEAQSDQoJQ3JlYXRlQXBwEGUSDQoJRGVsZXRlQXBwEGYSCwoHUmVhZEFwcBBnEg0KCVVwZGF0ZUFwcBBoEg8KCkNyZWF0ZVZpZXcQyQESDQoIUmVhZFZpZXcQygESDwoKVXBkYXRlVmlldxDLARIPCgpEZWxldGVWaWV3EMwB');
+final $typed_data.Uint8List workspaceEventDescriptor = $convert.base64Decode('Cg5Xb3Jrc3BhY2VFdmVudBITCg9DcmVhdGVXb3Jrc3BhY2UQABIUChBSZWFkQ3VyV29ya3NwYWNlEAESEgoOUmVhZFdvcmtzcGFjZXMQAhITCg9EZWxldGVXb3Jrc3BhY2UQAxIRCg1PcGVuV29ya3NwYWNlEAQSFQoRUmVhZFdvcmtzcGFjZUFwcHMQBRINCglDcmVhdGVBcHAQZRINCglEZWxldGVBcHAQZhILCgdSZWFkQXBwEGcSDQoJVXBkYXRlQXBwEGgSDwoKQ3JlYXRlVmlldxDJARINCghSZWFkVmlldxDKARIPCgpVcGRhdGVWaWV3EMsBEg8KCkRlbGV0ZVZpZXcQzAE=');

+ 67 - 4
rust-lib/flowy-database/src/macros.rs

@@ -1,14 +1,77 @@
+#[rustfmt::skip]
+/*
+diesel master support on_conflict on sqlite but not 1.4.7 version. Workaround for this
+
+match dsl::workspace_table
+    .filter(workspace_table::id.eq(table.id.clone()))
+    .count()
+    .get_result(conn)
+    .unwrap_or(0)
+{
+    0 => diesel::insert_into(workspace_table::table).values(table)
+                    .on_conflict(workspace_table::id)
+                    .do_update()
+                    .set(WorkspaceTableChangeset::from_table(workspace_table))
+                    .execute(conn)?,
+    _ => {
+        let changeset = WorkspaceTableChangeset::from_table(table);
+        let filter = dsl::workspace_table.filter(workspace_table::id.eq(changeset.id.clone()));
+        diesel::update(filter).set(changeset).execute(conn)?;
+    },
+}
+
+is equivalent to:
+
+match diesel_record_count!(workspace_table, &table.id, conn) {
+    0 => diesel_insert_table!(workspace_table, table, conn),
+    _ => diesel_update_table!(workspace_table, WorkspaceTableChangeset::from_table(table), &*conn),
+}
+*/
+
 #[macro_export]
-macro_rules! diesel_update_table {
+macro_rules! diesel_insert_table {
+    (
+        $table_name:ident,
+        $table:expr,
+        $connection:expr
+    ) => {
+        {
+        let _ = diesel::insert_into($table_name::table)
+                    .values($table.clone())
+                    // .on_conflict($table_name::dsl::id)
+                    // .do_update()
+                    // .set(WorkspaceTableChangeset::from_table(workspace_table))
+                    .execute($connection)?;
+        }
+    };
+}
+
+#[macro_export]
+macro_rules! diesel_record_count {
     (
         $table_name:ident,
-        $changeset:ident,
+        $id:expr,
         $connection:expr
     ) => {
+        $table_name::dsl::$table_name
+            .filter($table_name::dsl::id.eq($id.clone()))
+            .count()
+            .get_result($connection)
+            .unwrap_or(0);
+    };
+}
+
+#[macro_export]
+macro_rules! diesel_update_table {
+    (
+        $table_name:ident,
+        $changeset:expr,
+        $connection:expr
+    ) => {{
         let filter = $table_name::dsl::$table_name.filter($table_name::dsl::id.eq($changeset.id.clone()));
-        let affected_row = diesel::update(filter).set($changeset).execute(&*$connection)?;
+        let affected_row = diesel::update(filter).set($changeset).execute($connection)?;
         debug_assert_eq!(affected_row, 1);
-    };
+    }};
 }
 
 #[macro_export]

+ 2 - 4
rust-lib/flowy-document/src/sql_tables/doc/doc_sql.rs

@@ -16,15 +16,13 @@ pub struct DocTableSql {
 impl DocTableSql {
     pub(crate) fn create_doc_table(&self, doc_table: DocTable) -> Result<(), DocError> {
         let conn = self.database.db_connection()?;
-        let _ = diesel::insert_into(doc_table::table)
-            .values(doc_table)
-            .execute(&*conn)?;
+        let _ = diesel::insert_into(doc_table::table).values(doc_table).execute(&*conn)?;
         Ok(())
     }
 
     pub(crate) fn update_doc_table(&self, changeset: DocTableChangeset) -> Result<(), DocError> {
         let conn = self.database.db_connection()?;
-        diesel_update_table!(doc_table, changeset, conn);
+        diesel_update_table!(doc_table, changeset, &*conn);
         Ok(())
     }
 

+ 8 - 4
rust-lib/flowy-observable/src/entities/subject.rs

@@ -9,10 +9,13 @@ pub struct ObservableSubject {
     pub ty: i32,
 
     #[pb(index = 3)]
-    pub subject_id: String,
+    pub id: String,
 
     #[pb(index = 4, one_of)]
-    pub subject_payload: Option<Vec<u8>>,
+    pub payload: Option<Vec<u8>>,
+
+    #[pb(index = 5, one_of)]
+    pub error: Option<Vec<u8>>,
 }
 
 impl std::default::Default for ObservableSubject {
@@ -20,8 +23,9 @@ impl std::default::Default for ObservableSubject {
         Self {
             category: "".to_string(),
             ty: 0,
-            subject_id: "".to_string(),
-            subject_payload: None,
+            id: "".to_string(),
+            payload: None,
+            error: None,
         }
     }
 }

+ 156 - 71
rust-lib/flowy-observable/src/protobuf/model/subject.rs

@@ -28,9 +28,10 @@ pub struct ObservableSubject {
     // message fields
     pub category: ::std::string::String,
     pub ty: i32,
-    pub subject_id: ::std::string::String,
+    pub id: ::std::string::String,
     // message oneof groups
-    pub one_of_subject_payload: ::std::option::Option<ObservableSubject_oneof_one_of_subject_payload>,
+    pub one_of_payload: ::std::option::Option<ObservableSubject_oneof_one_of_payload>,
+    pub one_of_error: ::std::option::Option<ObservableSubject_oneof_one_of_error>,
     // special fields
     pub unknown_fields: ::protobuf::UnknownFields,
     pub cached_size: ::protobuf::CachedSize,
@@ -43,8 +44,13 @@ impl<'a> ::std::default::Default for &'a ObservableSubject {
 }
 
 #[derive(Clone,PartialEq,Debug)]
-pub enum ObservableSubject_oneof_one_of_subject_payload {
-    subject_payload(::std::vec::Vec<u8>),
+pub enum ObservableSubject_oneof_one_of_payload {
+    payload(::std::vec::Vec<u8>),
+}
+
+#[derive(Clone,PartialEq,Debug)]
+pub enum ObservableSubject_oneof_one_of_error {
+    error(::std::vec::Vec<u8>),
 }
 
 impl ObservableSubject {
@@ -93,74 +99,123 @@ impl ObservableSubject {
         self.ty = v;
     }
 
-    // string subject_id = 3;
+    // string id = 3;
 
 
-    pub fn get_subject_id(&self) -> &str {
-        &self.subject_id
+    pub fn get_id(&self) -> &str {
+        &self.id
     }
-    pub fn clear_subject_id(&mut self) {
-        self.subject_id.clear();
+    pub fn clear_id(&mut self) {
+        self.id.clear();
     }
 
     // Param is passed by value, moved
-    pub fn set_subject_id(&mut self, v: ::std::string::String) {
-        self.subject_id = v;
+    pub fn set_id(&mut self, v: ::std::string::String) {
+        self.id = v;
     }
 
     // Mutable pointer to the field.
     // If field is not initialized, it is initialized with default value first.
-    pub fn mut_subject_id(&mut self) -> &mut ::std::string::String {
-        &mut self.subject_id
+    pub fn mut_id(&mut self) -> &mut ::std::string::String {
+        &mut self.id
+    }
+
+    // Take field
+    pub fn take_id(&mut self) -> ::std::string::String {
+        ::std::mem::replace(&mut self.id, ::std::string::String::new())
+    }
+
+    // bytes payload = 4;
+
+
+    pub fn get_payload(&self) -> &[u8] {
+        match self.one_of_payload {
+            ::std::option::Option::Some(ObservableSubject_oneof_one_of_payload::payload(ref v)) => v,
+            _ => &[],
+        }
+    }
+    pub fn clear_payload(&mut self) {
+        self.one_of_payload = ::std::option::Option::None;
+    }
+
+    pub fn has_payload(&self) -> bool {
+        match self.one_of_payload {
+            ::std::option::Option::Some(ObservableSubject_oneof_one_of_payload::payload(..)) => true,
+            _ => false,
+        }
+    }
+
+    // Param is passed by value, moved
+    pub fn set_payload(&mut self, v: ::std::vec::Vec<u8>) {
+        self.one_of_payload = ::std::option::Option::Some(ObservableSubject_oneof_one_of_payload::payload(v))
+    }
+
+    // Mutable pointer to the field.
+    pub fn mut_payload(&mut self) -> &mut ::std::vec::Vec<u8> {
+        if let ::std::option::Option::Some(ObservableSubject_oneof_one_of_payload::payload(_)) = self.one_of_payload {
+        } else {
+            self.one_of_payload = ::std::option::Option::Some(ObservableSubject_oneof_one_of_payload::payload(::std::vec::Vec::new()));
+        }
+        match self.one_of_payload {
+            ::std::option::Option::Some(ObservableSubject_oneof_one_of_payload::payload(ref mut v)) => v,
+            _ => panic!(),
+        }
     }
 
     // Take field
-    pub fn take_subject_id(&mut self) -> ::std::string::String {
-        ::std::mem::replace(&mut self.subject_id, ::std::string::String::new())
+    pub fn take_payload(&mut self) -> ::std::vec::Vec<u8> {
+        if self.has_payload() {
+            match self.one_of_payload.take() {
+                ::std::option::Option::Some(ObservableSubject_oneof_one_of_payload::payload(v)) => v,
+                _ => panic!(),
+            }
+        } else {
+            ::std::vec::Vec::new()
+        }
     }
 
-    // bytes subject_payload = 4;
+    // bytes error = 5;
 
 
-    pub fn get_subject_payload(&self) -> &[u8] {
-        match self.one_of_subject_payload {
-            ::std::option::Option::Some(ObservableSubject_oneof_one_of_subject_payload::subject_payload(ref v)) => v,
+    pub fn get_error(&self) -> &[u8] {
+        match self.one_of_error {
+            ::std::option::Option::Some(ObservableSubject_oneof_one_of_error::error(ref v)) => v,
             _ => &[],
         }
     }
-    pub fn clear_subject_payload(&mut self) {
-        self.one_of_subject_payload = ::std::option::Option::None;
+    pub fn clear_error(&mut self) {
+        self.one_of_error = ::std::option::Option::None;
     }
 
-    pub fn has_subject_payload(&self) -> bool {
-        match self.one_of_subject_payload {
-            ::std::option::Option::Some(ObservableSubject_oneof_one_of_subject_payload::subject_payload(..)) => true,
+    pub fn has_error(&self) -> bool {
+        match self.one_of_error {
+            ::std::option::Option::Some(ObservableSubject_oneof_one_of_error::error(..)) => true,
             _ => false,
         }
     }
 
     // Param is passed by value, moved
-    pub fn set_subject_payload(&mut self, v: ::std::vec::Vec<u8>) {
-        self.one_of_subject_payload = ::std::option::Option::Some(ObservableSubject_oneof_one_of_subject_payload::subject_payload(v))
+    pub fn set_error(&mut self, v: ::std::vec::Vec<u8>) {
+        self.one_of_error = ::std::option::Option::Some(ObservableSubject_oneof_one_of_error::error(v))
     }
 
     // Mutable pointer to the field.
-    pub fn mut_subject_payload(&mut self) -> &mut ::std::vec::Vec<u8> {
-        if let ::std::option::Option::Some(ObservableSubject_oneof_one_of_subject_payload::subject_payload(_)) = self.one_of_subject_payload {
+    pub fn mut_error(&mut self) -> &mut ::std::vec::Vec<u8> {
+        if let ::std::option::Option::Some(ObservableSubject_oneof_one_of_error::error(_)) = self.one_of_error {
         } else {
-            self.one_of_subject_payload = ::std::option::Option::Some(ObservableSubject_oneof_one_of_subject_payload::subject_payload(::std::vec::Vec::new()));
+            self.one_of_error = ::std::option::Option::Some(ObservableSubject_oneof_one_of_error::error(::std::vec::Vec::new()));
         }
-        match self.one_of_subject_payload {
-            ::std::option::Option::Some(ObservableSubject_oneof_one_of_subject_payload::subject_payload(ref mut v)) => v,
+        match self.one_of_error {
+            ::std::option::Option::Some(ObservableSubject_oneof_one_of_error::error(ref mut v)) => v,
             _ => panic!(),
         }
     }
 
     // Take field
-    pub fn take_subject_payload(&mut self) -> ::std::vec::Vec<u8> {
-        if self.has_subject_payload() {
-            match self.one_of_subject_payload.take() {
-                ::std::option::Option::Some(ObservableSubject_oneof_one_of_subject_payload::subject_payload(v)) => v,
+    pub fn take_error(&mut self) -> ::std::vec::Vec<u8> {
+        if self.has_error() {
+            match self.one_of_error.take() {
+                ::std::option::Option::Some(ObservableSubject_oneof_one_of_error::error(v)) => v,
                 _ => panic!(),
             }
         } else {
@@ -189,13 +244,19 @@ impl ::protobuf::Message for ObservableSubject {
                     self.ty = tmp;
                 },
                 3 => {
-                    ::protobuf::rt::read_singular_proto3_string_into(wire_type, is, &mut self.subject_id)?;
+                    ::protobuf::rt::read_singular_proto3_string_into(wire_type, is, &mut self.id)?;
                 },
                 4 => {
                     if wire_type != ::protobuf::wire_format::WireTypeLengthDelimited {
                         return ::std::result::Result::Err(::protobuf::rt::unexpected_wire_type(wire_type));
                     }
-                    self.one_of_subject_payload = ::std::option::Option::Some(ObservableSubject_oneof_one_of_subject_payload::subject_payload(is.read_bytes()?));
+                    self.one_of_payload = ::std::option::Option::Some(ObservableSubject_oneof_one_of_payload::payload(is.read_bytes()?));
+                },
+                5 => {
+                    if wire_type != ::protobuf::wire_format::WireTypeLengthDelimited {
+                        return ::std::result::Result::Err(::protobuf::rt::unexpected_wire_type(wire_type));
+                    }
+                    self.one_of_error = ::std::option::Option::Some(ObservableSubject_oneof_one_of_error::error(is.read_bytes()?));
                 },
                 _ => {
                     ::protobuf::rt::read_unknown_or_skip_group(field_number, wire_type, is, self.mut_unknown_fields())?;
@@ -215,16 +276,23 @@ impl ::protobuf::Message for ObservableSubject {
         if self.ty != 0 {
             my_size += ::protobuf::rt::value_size(2, self.ty, ::protobuf::wire_format::WireTypeVarint);
         }
-        if !self.subject_id.is_empty() {
-            my_size += ::protobuf::rt::string_size(3, &self.subject_id);
+        if !self.id.is_empty() {
+            my_size += ::protobuf::rt::string_size(3, &self.id);
         }
-        if let ::std::option::Option::Some(ref v) = self.one_of_subject_payload {
+        if let ::std::option::Option::Some(ref v) = self.one_of_payload {
             match v {
-                &ObservableSubject_oneof_one_of_subject_payload::subject_payload(ref v) => {
+                &ObservableSubject_oneof_one_of_payload::payload(ref v) => {
                     my_size += ::protobuf::rt::bytes_size(4, &v);
                 },
             };
         }
+        if let ::std::option::Option::Some(ref v) = self.one_of_error {
+            match v {
+                &ObservableSubject_oneof_one_of_error::error(ref v) => {
+                    my_size += ::protobuf::rt::bytes_size(5, &v);
+                },
+            };
+        }
         my_size += ::protobuf::rt::unknown_fields_size(self.get_unknown_fields());
         self.cached_size.set(my_size);
         my_size
@@ -237,16 +305,23 @@ impl ::protobuf::Message for ObservableSubject {
         if self.ty != 0 {
             os.write_int32(2, self.ty)?;
         }
-        if !self.subject_id.is_empty() {
-            os.write_string(3, &self.subject_id)?;
+        if !self.id.is_empty() {
+            os.write_string(3, &self.id)?;
         }
-        if let ::std::option::Option::Some(ref v) = self.one_of_subject_payload {
+        if let ::std::option::Option::Some(ref v) = self.one_of_payload {
             match v {
-                &ObservableSubject_oneof_one_of_subject_payload::subject_payload(ref v) => {
+                &ObservableSubject_oneof_one_of_payload::payload(ref v) => {
                     os.write_bytes(4, v)?;
                 },
             };
         }
+        if let ::std::option::Option::Some(ref v) = self.one_of_error {
+            match v {
+                &ObservableSubject_oneof_one_of_error::error(ref v) => {
+                    os.write_bytes(5, v)?;
+                },
+            };
+        }
         os.write_unknown_fields(self.get_unknown_fields())?;
         ::std::result::Result::Ok(())
     }
@@ -296,14 +371,19 @@ impl ::protobuf::Message for ObservableSubject {
                 |m: &mut ObservableSubject| { &mut m.ty },
             ));
             fields.push(::protobuf::reflect::accessor::make_simple_field_accessor::<_, ::protobuf::types::ProtobufTypeString>(
-                "subject_id",
-                |m: &ObservableSubject| { &m.subject_id },
-                |m: &mut ObservableSubject| { &mut m.subject_id },
+                "id",
+                |m: &ObservableSubject| { &m.id },
+                |m: &mut ObservableSubject| { &mut m.id },
+            ));
+            fields.push(::protobuf::reflect::accessor::make_singular_bytes_accessor::<_>(
+                "payload",
+                ObservableSubject::has_payload,
+                ObservableSubject::get_payload,
             ));
             fields.push(::protobuf::reflect::accessor::make_singular_bytes_accessor::<_>(
-                "subject_payload",
-                ObservableSubject::has_subject_payload,
-                ObservableSubject::get_subject_payload,
+                "error",
+                ObservableSubject::has_error,
+                ObservableSubject::get_error,
             ));
             ::protobuf::reflect::MessageDescriptor::new_pb_name::<ObservableSubject>(
                 "ObservableSubject",
@@ -323,8 +403,9 @@ impl ::protobuf::Clear for ObservableSubject {
     fn clear(&mut self) {
         self.category.clear();
         self.ty = 0;
-        self.subject_id.clear();
-        self.one_of_subject_payload = ::std::option::Option::None;
+        self.id.clear();
+        self.one_of_payload = ::std::option::Option::None;
+        self.one_of_error = ::std::option::Option::None;
         self.unknown_fields.clear();
     }
 }
@@ -342,25 +423,29 @@ impl ::protobuf::reflect::ProtobufValue for ObservableSubject {
 }
 
 static file_descriptor_proto_data: &'static [u8] = b"\
-    \n\rsubject.proto\"\xa3\x01\n\x11ObservableSubject\x12\x1a\n\x08category\
+    \n\rsubject.proto\"\xa5\x01\n\x11ObservableSubject\x12\x1a\n\x08category\
     \x18\x01\x20\x01(\tR\x08category\x12\x0e\n\x02ty\x18\x02\x20\x01(\x05R\
-    \x02ty\x12\x1d\n\nsubject_id\x18\x03\x20\x01(\tR\tsubjectId\x12)\n\x0fsu\
-    bject_payload\x18\x04\x20\x01(\x0cH\0R\x0esubjectPayloadB\x18\n\x16one_o\
-    f_subject_payloadJ\xa1\x02\n\x06\x12\x04\0\0\x07\x01\n\x08\n\x01\x0c\x12\
-    \x03\0\0\x12\n\n\n\x02\x04\0\x12\x04\x02\0\x07\x01\n\n\n\x03\x04\0\x01\
-    \x12\x03\x02\x08\x19\n\x0b\n\x04\x04\0\x02\0\x12\x03\x03\x04\x18\n\x0c\n\
-    \x05\x04\0\x02\0\x05\x12\x03\x03\x04\n\n\x0c\n\x05\x04\0\x02\0\x01\x12\
-    \x03\x03\x0b\x13\n\x0c\n\x05\x04\0\x02\0\x03\x12\x03\x03\x16\x17\n\x0b\n\
-    \x04\x04\0\x02\x01\x12\x03\x04\x04\x11\n\x0c\n\x05\x04\0\x02\x01\x05\x12\
-    \x03\x04\x04\t\n\x0c\n\x05\x04\0\x02\x01\x01\x12\x03\x04\n\x0c\n\x0c\n\
-    \x05\x04\0\x02\x01\x03\x12\x03\x04\x0f\x10\n\x0b\n\x04\x04\0\x02\x02\x12\
-    \x03\x05\x04\x1a\n\x0c\n\x05\x04\0\x02\x02\x05\x12\x03\x05\x04\n\n\x0c\n\
-    \x05\x04\0\x02\x02\x01\x12\x03\x05\x0b\x15\n\x0c\n\x05\x04\0\x02\x02\x03\
-    \x12\x03\x05\x18\x19\n\x0b\n\x04\x04\0\x08\0\x12\x03\x06\x04?\n\x0c\n\
-    \x05\x04\0\x08\0\x01\x12\x03\x06\n\x20\n\x0b\n\x04\x04\0\x02\x03\x12\x03\
-    \x06#=\n\x0c\n\x05\x04\0\x02\x03\x05\x12\x03\x06#(\n\x0c\n\x05\x04\0\x02\
-    \x03\x01\x12\x03\x06)8\n\x0c\n\x05\x04\0\x02\x03\x03\x12\x03\x06;<b\x06p\
-    roto3\
+    \x02ty\x12\x0e\n\x02id\x18\x03\x20\x01(\tR\x02id\x12\x1a\n\x07payload\
+    \x18\x04\x20\x01(\x0cH\0R\x07payload\x12\x16\n\x05error\x18\x05\x20\x01(\
+    \x0cH\x01R\x05errorB\x10\n\x0eone_of_payloadB\x0e\n\x0cone_of_errorJ\xf3\
+    \x02\n\x06\x12\x04\0\0\x08\x01\n\x08\n\x01\x0c\x12\x03\0\0\x12\n\n\n\x02\
+    \x04\0\x12\x04\x02\0\x08\x01\n\n\n\x03\x04\0\x01\x12\x03\x02\x08\x19\n\
+    \x0b\n\x04\x04\0\x02\0\x12\x03\x03\x04\x18\n\x0c\n\x05\x04\0\x02\0\x05\
+    \x12\x03\x03\x04\n\n\x0c\n\x05\x04\0\x02\0\x01\x12\x03\x03\x0b\x13\n\x0c\
+    \n\x05\x04\0\x02\0\x03\x12\x03\x03\x16\x17\n\x0b\n\x04\x04\0\x02\x01\x12\
+    \x03\x04\x04\x11\n\x0c\n\x05\x04\0\x02\x01\x05\x12\x03\x04\x04\t\n\x0c\n\
+    \x05\x04\0\x02\x01\x01\x12\x03\x04\n\x0c\n\x0c\n\x05\x04\0\x02\x01\x03\
+    \x12\x03\x04\x0f\x10\n\x0b\n\x04\x04\0\x02\x02\x12\x03\x05\x04\x12\n\x0c\
+    \n\x05\x04\0\x02\x02\x05\x12\x03\x05\x04\n\n\x0c\n\x05\x04\0\x02\x02\x01\
+    \x12\x03\x05\x0b\r\n\x0c\n\x05\x04\0\x02\x02\x03\x12\x03\x05\x10\x11\n\
+    \x0b\n\x04\x04\0\x08\0\x12\x03\x06\x04/\n\x0c\n\x05\x04\0\x08\0\x01\x12\
+    \x03\x06\n\x18\n\x0b\n\x04\x04\0\x02\x03\x12\x03\x06\x1b-\n\x0c\n\x05\
+    \x04\0\x02\x03\x05\x12\x03\x06\x1b\x20\n\x0c\n\x05\x04\0\x02\x03\x01\x12\
+    \x03\x06!(\n\x0c\n\x05\x04\0\x02\x03\x03\x12\x03\x06+,\n\x0b\n\x04\x04\0\
+    \x08\x01\x12\x03\x07\x04+\n\x0c\n\x05\x04\0\x08\x01\x01\x12\x03\x07\n\
+    \x16\n\x0b\n\x04\x04\0\x02\x04\x12\x03\x07\x19)\n\x0c\n\x05\x04\0\x02\
+    \x04\x05\x12\x03\x07\x19\x1e\n\x0c\n\x05\x04\0\x02\x04\x01\x12\x03\x07\
+    \x1f$\n\x0c\n\x05\x04\0\x02\x04\x03\x12\x03\x07'(b\x06proto3\
 ";
 
 static file_descriptor_proto_lazy: ::protobuf::rt::LazyV2<::protobuf::descriptor::FileDescriptorProto> = ::protobuf::rt::LazyV2::INIT;

+ 3 - 2
rust-lib/flowy-observable/src/protobuf/proto/subject.proto

@@ -3,6 +3,7 @@ syntax = "proto3";
 message ObservableSubject {
     string category = 1;
     int32 ty = 2;
-    string subject_id = 3;
-    oneof one_of_subject_payload { bytes subject_payload = 4; };
+    string id = 3;
+    oneof one_of_payload { bytes payload = 4; };
+    oneof one_of_error { bytes error = 5; };
 }

+ 12 - 2
rust-lib/flowy-sdk/src/lib.rs

@@ -22,16 +22,26 @@ impl FlowySDKConfig {
     pub fn new(root: &str) -> Self {
         FlowySDKConfig {
             root: root.to_owned(),
-            log_filter: std::env::var("RUST_LOG").unwrap_or("info".to_owned()),
+            log_filter: crate_log_filter(None),
         }
     }
 
     pub fn log_filter(mut self, filter: &str) -> Self {
-        self.log_filter = filter.to_owned();
+        self.log_filter = crate_log_filter(Some(filter.to_owned()));
         self
     }
 }
 
+fn crate_log_filter(level: Option<String>) -> String {
+    let level = level.unwrap_or(std::env::var("RUST_LOG").unwrap_or("info".to_owned()));
+    let mut filters = vec![];
+    filters.push(format!("flowy_sdk={}", level));
+    filters.push(format!("flowy_workspace={}", level));
+    filters.push(format!("flowy_user={}", level));
+    filters.push(format!("info"));
+    filters.join(",")
+}
+
 #[derive(Clone)]
 pub struct FlowySDK {
     config: FlowySDKConfig,

+ 2 - 2
rust-lib/flowy-user/src/services/user/user_session.rs

@@ -111,7 +111,7 @@ impl UserSession {
     pub async fn update_user(&self, params: UpdateUserParams) -> Result<(), UserError> {
         let session = self.get_session()?;
         let changeset = UserTableChangeset::new(params.clone());
-        diesel_update_table!(user_table, changeset, self.db_conn()?);
+        diesel_update_table!(user_table, changeset, &*self.db_conn()?);
 
         let _ = self.update_user_on_server(&session.token, params).await?;
         Ok(())
@@ -231,7 +231,7 @@ impl UserSession {
 pub async fn update_user(_server: Server, pool: Arc<ConnectionPool>, params: UpdateUserParams) -> Result<(), UserError> {
     let changeset = UserTableChangeset::new(params);
     let conn = pool.get()?;
-    diesel_update_table!(user_table, changeset, conn);
+    diesel_update_table!(user_table, changeset, &*conn);
     Ok(())
 }
 

+ 3 - 0
rust-lib/flowy-workspace/Cargo.toml

@@ -19,6 +19,9 @@ protobuf = {version = "2.18.0"}
 log = "0.4.14"
 diesel = {version = "1.4.7", features = ["sqlite"]}
 diesel_derives = {version = "1.4.1", features = ["sqlite"]}
+#diesel = { git = "https://github.com/diesel-rs/diesel.git", branch = "master", features = ["sqlite"] }
+#diesel_derives = { git = "https://github.com/diesel-rs/diesel.git", branch = "master",features = ["sqlite"] }
+
 futures-core = { version = "0.3", default-features = false }
 pin-project = "1.0.0"
 strum = "0.21"

+ 16 - 13
rust-lib/flowy-workspace/src/event.rs

@@ -5,41 +5,44 @@ use strum_macros::Display;
 #[event_err = "WorkspaceError"]
 pub enum WorkspaceEvent {
     #[event(input = "CreateWorkspaceRequest", output = "Workspace")]
-    CreateWorkspace  = 0,
+    CreateWorkspace   = 0,
 
     #[event(output = "Workspace")]
-    ReadCurWorkspace = 1,
+    ReadCurWorkspace  = 1,
 
     #[event(input = "QueryWorkspaceRequest", output = "RepeatedWorkspace")]
-    ReadWorkspaces   = 2,
+    ReadWorkspaces    = 2,
 
     #[event(input = "DeleteWorkspaceRequest")]
-    DeleteWorkspace  = 3,
+    DeleteWorkspace   = 3,
 
     #[event(input = "QueryWorkspaceRequest", output = "Workspace")]
-    OpenWorkspace    = 4,
+    OpenWorkspace     = 4,
+
+    #[event(input = "QueryWorkspaceRequest", output = "RepeatedApp")]
+    ReadWorkspaceApps = 5,
 
     #[event(input = "CreateAppRequest", output = "App")]
-    CreateApp        = 101,
+    CreateApp         = 101,
 
     #[event(input = "DeleteAppRequest")]
-    DeleteApp        = 102,
+    DeleteApp         = 102,
 
     #[event(input = "QueryAppRequest", output = "App")]
-    ReadApp          = 103,
+    ReadApp           = 103,
 
     #[event(input = "UpdateAppRequest")]
-    UpdateApp        = 104,
+    UpdateApp         = 104,
 
     #[event(input = "CreateViewRequest", output = "View")]
-    CreateView       = 201,
+    CreateView        = 201,
 
     #[event(input = "QueryViewRequest", output = "View")]
-    ReadView         = 202,
+    ReadView          = 202,
 
     #[event(input = "UpdateViewRequest")]
-    UpdateView       = 203,
+    UpdateView        = 203,
 
     #[event(input = "DeleteViewRequest")]
-    DeleteView       = 204,
+    DeleteView        = 204,
 }

+ 1 - 2
rust-lib/flowy-workspace/src/handlers/app_handler.rs

@@ -55,8 +55,7 @@ pub(crate) async fn read_app_handler(
 
     // The View's belonging is the view indexed by the belong_to_id for now
     if params.read_belongings {
-        let views = view_controller.read_views_belong_to(&params.app_id).await?;
-        app.belongings = RepeatedView { items: views };
+        app.belongings = view_controller.read_views_belong_to(&params.app_id).await?;
     }
 
     data_result(app)

+ 1 - 2
rust-lib/flowy-workspace/src/handlers/view_handler.rs

@@ -36,8 +36,7 @@ pub(crate) async fn read_view_handler(
     let mut view = controller.read_view(params.clone()).await?;
 
     if params.read_belongings {
-        let views = controller.read_views_belong_to(&params.view_id).await?;
-        view.belongings = RepeatedView { items: views }
+        view.belongings = controller.read_views_belong_to(&params.view_id).await?;
     }
 
     data_result(view)

+ 11 - 1
rust-lib/flowy-workspace/src/handlers/workspace_handler.rs

@@ -1,4 +1,8 @@
-use crate::{entities::workspace::*, errors::WorkspaceError, services::WorkspaceController};
+use crate::{
+    entities::{app::RepeatedApp, workspace::*},
+    errors::WorkspaceError,
+    services::WorkspaceController,
+};
 use flowy_dispatch::prelude::{data_result, Data, DataResult, Unit};
 use std::{convert::TryInto, sync::Arc};
 
@@ -19,6 +23,12 @@ pub(crate) async fn read_cur_workspace_handler(controller: Unit<Arc<WorkspaceCon
     data_result(workspace)
 }
 
+#[tracing::instrument(skip(controller), err)]
+pub(crate) async fn read_workspace_apps_handler(controller: Unit<Arc<WorkspaceController>>) -> DataResult<RepeatedApp, WorkspaceError> {
+    let repeated_app = controller.read_workspace_apps().await?;
+    data_result(repeated_app)
+}
+
 #[tracing::instrument(skip(data, controller), err)]
 pub(crate) async fn read_workspaces_handler(
     data: Data<QueryWorkspaceRequest>,

+ 2 - 1
rust-lib/flowy-workspace/src/module.rs

@@ -52,7 +52,8 @@ pub fn create(user: Arc<dyn WorkspaceUser>, database: Arc<dyn WorkspaceDatabase>
         .event(WorkspaceEvent::CreateWorkspace, create_workspace_handler)
         .event(WorkspaceEvent::ReadCurWorkspace, read_cur_workspace_handler)
         .event(WorkspaceEvent::ReadWorkspaces, read_workspaces_handler)
-        .event(WorkspaceEvent::OpenWorkspace, open_workspace_handler);
+        .event(WorkspaceEvent::OpenWorkspace, open_workspace_handler)
+        .event(WorkspaceEvent::ReadWorkspaceApps, read_workspace_apps_handler);
 
     module = module
         .event(WorkspaceEvent::CreateApp, create_app_handler)

+ 32 - 22
rust-lib/flowy-workspace/src/observable/observable.rs

@@ -28,22 +28,23 @@ impl std::default::Default for WorkspaceObservable {
     fn default() -> Self { WorkspaceObservable::Unknown }
 }
 
-pub(crate) struct ObservableSender {
-    ty: WorkspaceObservable,
-    subject_id: String,
+pub(crate) struct ObservableBuilder {
+    id: String,
     payload: Option<Bytes>,
+    error: Option<Bytes>,
+    ty: WorkspaceObservable,
 }
 
-impl ObservableSender {
-    pub(crate) fn new(subject_id: &str, ty: WorkspaceObservable) -> Self {
+impl ObservableBuilder {
+    pub(crate) fn new(id: &str, ty: WorkspaceObservable) -> Self {
         Self {
-            subject_id: subject_id.to_owned(),
+            id: id.to_owned(),
             ty,
             payload: None,
+            error: None,
         }
     }
 
-    #[allow(dead_code)]
     pub(crate) fn payload<T>(mut self, payload: T) -> Self
     where
         T: ToBytes,
@@ -58,10 +59,28 @@ impl ObservableSender {
         self
     }
 
-    pub(crate) fn send(self) {
-        log::trace!("Workspace observable id: {}, ty: {:?}", self.subject_id, self.ty);
+    pub(crate) fn error<T>(mut self, error: T) -> Self
+    where
+        T: ToBytes,
+    {
+        match error.into_bytes() {
+            Ok(bytes) => self.error = Some(bytes),
+            Err(e) => {
+                log::error!("Set observable error failed: {:?}", e);
+            },
+        }
+        self
+    }
+
+    pub(crate) fn build(self) {
+        log::trace!("Workspace observable id: {}, ty: {:?}", self.id, self.ty);
 
-        let subject_payload = match self.payload {
+        let payload = match self.payload {
+            None => None,
+            Some(bytes) => Some(bytes.to_vec()),
+        };
+
+        let error = match self.error {
             None => None,
             Some(bytes) => Some(bytes.to_vec()),
         };
@@ -69,8 +88,9 @@ impl ObservableSender {
         let subject = ObservableSubject {
             category: OBSERVABLE_CATEGORY.to_string(),
             ty: self.ty as i32,
-            subject_id: self.subject_id,
-            subject_payload,
+            id: self.id,
+            payload,
+            error,
         };
         match RustStreamSender::post(subject) {
             Ok(_) => {},
@@ -78,13 +98,3 @@ impl ObservableSender {
         }
     }
 }
-
-pub(crate) fn send_observable(id: &str, ty: WorkspaceObservable) { ObservableSender::new(id, ty).send(); }
-
-#[allow(dead_code)]
-pub(crate) fn send_observable_with_payload<T>(id: &str, ty: WorkspaceObservable, payload: T)
-where
-    T: ToBytes,
-{
-    ObservableSender::new(id, ty).payload(payload).send();
-}

+ 39 - 33
rust-lib/flowy-workspace/src/protobuf/model/event.rs

@@ -30,6 +30,7 @@ pub enum WorkspaceEvent {
     ReadWorkspaces = 2,
     DeleteWorkspace = 3,
     OpenWorkspace = 4,
+    ReadWorkspaceApps = 5,
     CreateApp = 101,
     DeleteApp = 102,
     ReadApp = 103,
@@ -52,6 +53,7 @@ impl ::protobuf::ProtobufEnum for WorkspaceEvent {
             2 => ::std::option::Option::Some(WorkspaceEvent::ReadWorkspaces),
             3 => ::std::option::Option::Some(WorkspaceEvent::DeleteWorkspace),
             4 => ::std::option::Option::Some(WorkspaceEvent::OpenWorkspace),
+            5 => ::std::option::Option::Some(WorkspaceEvent::ReadWorkspaceApps),
             101 => ::std::option::Option::Some(WorkspaceEvent::CreateApp),
             102 => ::std::option::Option::Some(WorkspaceEvent::DeleteApp),
             103 => ::std::option::Option::Some(WorkspaceEvent::ReadApp),
@@ -71,6 +73,7 @@ impl ::protobuf::ProtobufEnum for WorkspaceEvent {
             WorkspaceEvent::ReadWorkspaces,
             WorkspaceEvent::DeleteWorkspace,
             WorkspaceEvent::OpenWorkspace,
+            WorkspaceEvent::ReadWorkspaceApps,
             WorkspaceEvent::CreateApp,
             WorkspaceEvent::DeleteApp,
             WorkspaceEvent::ReadApp,
@@ -107,41 +110,44 @@ impl ::protobuf::reflect::ProtobufValue for WorkspaceEvent {
 }
 
 static file_descriptor_proto_data: &'static [u8] = b"\
-    \n\x0bevent.proto*\xf3\x01\n\x0eWorkspaceEvent\x12\x13\n\x0fCreateWorksp\
+    \n\x0bevent.proto*\x8a\x02\n\x0eWorkspaceEvent\x12\x13\n\x0fCreateWorksp\
     ace\x10\0\x12\x14\n\x10ReadCurWorkspace\x10\x01\x12\x12\n\x0eReadWorkspa\
     ces\x10\x02\x12\x13\n\x0fDeleteWorkspace\x10\x03\x12\x11\n\rOpenWorkspac\
-    e\x10\x04\x12\r\n\tCreateApp\x10e\x12\r\n\tDeleteApp\x10f\x12\x0b\n\x07R\
-    eadApp\x10g\x12\r\n\tUpdateApp\x10h\x12\x0f\n\nCreateView\x10\xc9\x01\
-    \x12\r\n\x08ReadView\x10\xca\x01\x12\x0f\n\nUpdateView\x10\xcb\x01\x12\
-    \x0f\n\nDeleteView\x10\xcc\x01J\xbf\x04\n\x06\x12\x04\0\0\x10\x01\n\x08\
-    \n\x01\x0c\x12\x03\0\0\x12\n\n\n\x02\x05\0\x12\x04\x02\0\x10\x01\n\n\n\
-    \x03\x05\0\x01\x12\x03\x02\x05\x13\n\x0b\n\x04\x05\0\x02\0\x12\x03\x03\
-    \x04\x18\n\x0c\n\x05\x05\0\x02\0\x01\x12\x03\x03\x04\x13\n\x0c\n\x05\x05\
-    \0\x02\0\x02\x12\x03\x03\x16\x17\n\x0b\n\x04\x05\0\x02\x01\x12\x03\x04\
-    \x04\x19\n\x0c\n\x05\x05\0\x02\x01\x01\x12\x03\x04\x04\x14\n\x0c\n\x05\
-    \x05\0\x02\x01\x02\x12\x03\x04\x17\x18\n\x0b\n\x04\x05\0\x02\x02\x12\x03\
-    \x05\x04\x17\n\x0c\n\x05\x05\0\x02\x02\x01\x12\x03\x05\x04\x12\n\x0c\n\
-    \x05\x05\0\x02\x02\x02\x12\x03\x05\x15\x16\n\x0b\n\x04\x05\0\x02\x03\x12\
-    \x03\x06\x04\x18\n\x0c\n\x05\x05\0\x02\x03\x01\x12\x03\x06\x04\x13\n\x0c\
-    \n\x05\x05\0\x02\x03\x02\x12\x03\x06\x16\x17\n\x0b\n\x04\x05\0\x02\x04\
-    \x12\x03\x07\x04\x16\n\x0c\n\x05\x05\0\x02\x04\x01\x12\x03\x07\x04\x11\n\
-    \x0c\n\x05\x05\0\x02\x04\x02\x12\x03\x07\x14\x15\n\x0b\n\x04\x05\0\x02\
-    \x05\x12\x03\x08\x04\x14\n\x0c\n\x05\x05\0\x02\x05\x01\x12\x03\x08\x04\r\
-    \n\x0c\n\x05\x05\0\x02\x05\x02\x12\x03\x08\x10\x13\n\x0b\n\x04\x05\0\x02\
-    \x06\x12\x03\t\x04\x14\n\x0c\n\x05\x05\0\x02\x06\x01\x12\x03\t\x04\r\n\
-    \x0c\n\x05\x05\0\x02\x06\x02\x12\x03\t\x10\x13\n\x0b\n\x04\x05\0\x02\x07\
-    \x12\x03\n\x04\x12\n\x0c\n\x05\x05\0\x02\x07\x01\x12\x03\n\x04\x0b\n\x0c\
-    \n\x05\x05\0\x02\x07\x02\x12\x03\n\x0e\x11\n\x0b\n\x04\x05\0\x02\x08\x12\
-    \x03\x0b\x04\x14\n\x0c\n\x05\x05\0\x02\x08\x01\x12\x03\x0b\x04\r\n\x0c\n\
-    \x05\x05\0\x02\x08\x02\x12\x03\x0b\x10\x13\n\x0b\n\x04\x05\0\x02\t\x12\
-    \x03\x0c\x04\x15\n\x0c\n\x05\x05\0\x02\t\x01\x12\x03\x0c\x04\x0e\n\x0c\n\
-    \x05\x05\0\x02\t\x02\x12\x03\x0c\x11\x14\n\x0b\n\x04\x05\0\x02\n\x12\x03\
-    \r\x04\x13\n\x0c\n\x05\x05\0\x02\n\x01\x12\x03\r\x04\x0c\n\x0c\n\x05\x05\
-    \0\x02\n\x02\x12\x03\r\x0f\x12\n\x0b\n\x04\x05\0\x02\x0b\x12\x03\x0e\x04\
-    \x15\n\x0c\n\x05\x05\0\x02\x0b\x01\x12\x03\x0e\x04\x0e\n\x0c\n\x05\x05\0\
-    \x02\x0b\x02\x12\x03\x0e\x11\x14\n\x0b\n\x04\x05\0\x02\x0c\x12\x03\x0f\
-    \x04\x15\n\x0c\n\x05\x05\0\x02\x0c\x01\x12\x03\x0f\x04\x0e\n\x0c\n\x05\
-    \x05\0\x02\x0c\x02\x12\x03\x0f\x11\x14b\x06proto3\
+    e\x10\x04\x12\x15\n\x11ReadWorkspaceApps\x10\x05\x12\r\n\tCreateApp\x10e\
+    \x12\r\n\tDeleteApp\x10f\x12\x0b\n\x07ReadApp\x10g\x12\r\n\tUpdateApp\
+    \x10h\x12\x0f\n\nCreateView\x10\xc9\x01\x12\r\n\x08ReadView\x10\xca\x01\
+    \x12\x0f\n\nUpdateView\x10\xcb\x01\x12\x0f\n\nDeleteView\x10\xcc\x01J\
+    \xe8\x04\n\x06\x12\x04\0\0\x11\x01\n\x08\n\x01\x0c\x12\x03\0\0\x12\n\n\n\
+    \x02\x05\0\x12\x04\x02\0\x11\x01\n\n\n\x03\x05\0\x01\x12\x03\x02\x05\x13\
+    \n\x0b\n\x04\x05\0\x02\0\x12\x03\x03\x04\x18\n\x0c\n\x05\x05\0\x02\0\x01\
+    \x12\x03\x03\x04\x13\n\x0c\n\x05\x05\0\x02\0\x02\x12\x03\x03\x16\x17\n\
+    \x0b\n\x04\x05\0\x02\x01\x12\x03\x04\x04\x19\n\x0c\n\x05\x05\0\x02\x01\
+    \x01\x12\x03\x04\x04\x14\n\x0c\n\x05\x05\0\x02\x01\x02\x12\x03\x04\x17\
+    \x18\n\x0b\n\x04\x05\0\x02\x02\x12\x03\x05\x04\x17\n\x0c\n\x05\x05\0\x02\
+    \x02\x01\x12\x03\x05\x04\x12\n\x0c\n\x05\x05\0\x02\x02\x02\x12\x03\x05\
+    \x15\x16\n\x0b\n\x04\x05\0\x02\x03\x12\x03\x06\x04\x18\n\x0c\n\x05\x05\0\
+    \x02\x03\x01\x12\x03\x06\x04\x13\n\x0c\n\x05\x05\0\x02\x03\x02\x12\x03\
+    \x06\x16\x17\n\x0b\n\x04\x05\0\x02\x04\x12\x03\x07\x04\x16\n\x0c\n\x05\
+    \x05\0\x02\x04\x01\x12\x03\x07\x04\x11\n\x0c\n\x05\x05\0\x02\x04\x02\x12\
+    \x03\x07\x14\x15\n\x0b\n\x04\x05\0\x02\x05\x12\x03\x08\x04\x1a\n\x0c\n\
+    \x05\x05\0\x02\x05\x01\x12\x03\x08\x04\x15\n\x0c\n\x05\x05\0\x02\x05\x02\
+    \x12\x03\x08\x18\x19\n\x0b\n\x04\x05\0\x02\x06\x12\x03\t\x04\x14\n\x0c\n\
+    \x05\x05\0\x02\x06\x01\x12\x03\t\x04\r\n\x0c\n\x05\x05\0\x02\x06\x02\x12\
+    \x03\t\x10\x13\n\x0b\n\x04\x05\0\x02\x07\x12\x03\n\x04\x14\n\x0c\n\x05\
+    \x05\0\x02\x07\x01\x12\x03\n\x04\r\n\x0c\n\x05\x05\0\x02\x07\x02\x12\x03\
+    \n\x10\x13\n\x0b\n\x04\x05\0\x02\x08\x12\x03\x0b\x04\x12\n\x0c\n\x05\x05\
+    \0\x02\x08\x01\x12\x03\x0b\x04\x0b\n\x0c\n\x05\x05\0\x02\x08\x02\x12\x03\
+    \x0b\x0e\x11\n\x0b\n\x04\x05\0\x02\t\x12\x03\x0c\x04\x14\n\x0c\n\x05\x05\
+    \0\x02\t\x01\x12\x03\x0c\x04\r\n\x0c\n\x05\x05\0\x02\t\x02\x12\x03\x0c\
+    \x10\x13\n\x0b\n\x04\x05\0\x02\n\x12\x03\r\x04\x15\n\x0c\n\x05\x05\0\x02\
+    \n\x01\x12\x03\r\x04\x0e\n\x0c\n\x05\x05\0\x02\n\x02\x12\x03\r\x11\x14\n\
+    \x0b\n\x04\x05\0\x02\x0b\x12\x03\x0e\x04\x13\n\x0c\n\x05\x05\0\x02\x0b\
+    \x01\x12\x03\x0e\x04\x0c\n\x0c\n\x05\x05\0\x02\x0b\x02\x12\x03\x0e\x0f\
+    \x12\n\x0b\n\x04\x05\0\x02\x0c\x12\x03\x0f\x04\x15\n\x0c\n\x05\x05\0\x02\
+    \x0c\x01\x12\x03\x0f\x04\x0e\n\x0c\n\x05\x05\0\x02\x0c\x02\x12\x03\x0f\
+    \x11\x14\n\x0b\n\x04\x05\0\x02\r\x12\x03\x10\x04\x15\n\x0c\n\x05\x05\0\
+    \x02\r\x01\x12\x03\x10\x04\x0e\n\x0c\n\x05\x05\0\x02\r\x02\x12\x03\x10\
+    \x11\x14b\x06proto3\
 ";
 
 static file_descriptor_proto_lazy: ::protobuf::rt::LazyV2<::protobuf::descriptor::FileDescriptorProto> = ::protobuf::rt::LazyV2::INIT;

+ 1 - 0
rust-lib/flowy-workspace/src/protobuf/proto/event.proto

@@ -6,6 +6,7 @@ enum WorkspaceEvent {
     ReadWorkspaces = 2;
     DeleteWorkspace = 3;
     OpenWorkspace = 4;
+    ReadWorkspaceApps = 5;
     CreateApp = 101;
     DeleteApp = 102;
     ReadApp = 103;

+ 30 - 10
rust-lib/flowy-workspace/src/services/app_controller.rs

@@ -7,6 +7,7 @@ use crate::{
     sql_tables::app::{AppTable, AppTableChangeset, AppTableSql},
 };
 
+use crate::entities::view::RepeatedView;
 use std::sync::Arc;
 
 pub(crate) struct AppController {
@@ -24,7 +25,7 @@ impl AppController {
         view_controller: Arc<ViewController>,
         server: Server,
     ) -> Self {
-        let sql = Arc::new(AppTableSql { database });
+        let sql = Arc::new(AppTableSql::new(database));
         Self {
             user,
             sql,
@@ -38,7 +39,12 @@ impl AppController {
         let app = self.create_app_on_server(params).await?;
         let app_table = AppTable::new(app.clone());
         let _ = self.sql.create_app(app_table)?;
-        send_observable(&app.workspace_id, WorkspaceObservable::WorkspaceCreateApp);
+
+        // Opti: transaction
+        let apps = self.read_local_apps(&app.workspace_id)?;
+        ObservableBuilder::new(&app.workspace_id, WorkspaceObservable::WorkspaceCreateApp)
+            .payload(apps)
+            .build();
         Ok(app)
     }
 
@@ -51,16 +57,31 @@ impl AppController {
     pub(crate) async fn delete_app(&self, app_id: &str) -> Result<(), WorkspaceError> {
         let app = self.sql.delete_app(app_id)?;
         let _ = self.delete_app_on_server(app_id).await?;
-        send_observable(&app.workspace_id, WorkspaceObservable::WorkspaceDeleteApp);
+
+        // Opti: transaction
+        let apps = self.read_local_apps(&app.workspace_id)?;
+        ObservableBuilder::new(&app.workspace_id, WorkspaceObservable::WorkspaceDeleteApp)
+            .payload(apps)
+            .build();
         Ok(())
     }
 
+    fn read_local_apps(&self, workspace_id: &str) -> Result<RepeatedApp, WorkspaceError> {
+        let app_tables = self.sql.read_apps(workspace_id, false)?;
+        let apps = app_tables.into_iter().map(|table| table.into()).collect::<Vec<App>>();
+        Ok(RepeatedApp { items: apps })
+    }
+
     pub(crate) async fn update_app(&self, params: UpdateAppParams) -> Result<(), WorkspaceError> {
         let changeset = AppTableChangeset::new(params.clone());
         let app_id = changeset.id.clone();
         let _ = self.sql.update_app(changeset)?;
         let _ = self.update_app_on_server(params).await?;
-        send_observable(&app_id, WorkspaceObservable::AppUpdated);
+
+        let app: App = self.sql.read_app(&app_id, false)?.into();
+        ObservableBuilder::new(&app_id, WorkspaceObservable::AppUpdated)
+            .payload(app)
+            .build();
         Ok(())
     }
 }
@@ -114,12 +135,11 @@ impl AppController {
         let token = self.user.token()?;
         let server = self.server.clone();
         spawn(async move {
-            match server.read_app(&token, params).await {
-                Ok(_) => {},
-                Err(e) => {
-                    // TODO: retry?
-                    log::error!("Read app failed: {:?}", e);
-                },
+            // Opti: retry?
+            let app = server.read_app(&token, params).await.unwrap();
+            match app {
+                None => {},
+                Some(_) => {},
             }
         });
         Ok(())

+ 4 - 2
rust-lib/flowy-workspace/src/services/helper.rs

@@ -1,7 +1,9 @@
-pub fn spawn<F>(f: F)
+use tokio::task::JoinHandle;
+
+pub fn spawn<F>(f: F) -> JoinHandle<F::Output>
 where
     F: std::future::Future + Send + 'static,
     F::Output: Send + 'static,
 {
-    let _ = tokio::spawn(f);
+    tokio::spawn(f)
 }

+ 33 - 9
rust-lib/flowy-workspace/src/services/view_controller.rs

@@ -2,14 +2,15 @@ use crate::{
     entities::view::{CreateViewParams, UpdateViewParams, View},
     errors::WorkspaceError,
     module::WorkspaceDatabase,
-    observable::{send_observable, WorkspaceObservable},
+    observable::WorkspaceObservable,
     services::{helper::spawn, server::Server},
     sql_tables::view::{ViewTable, ViewTableChangeset, ViewTableSql},
 };
 
 use crate::{
-    entities::view::{DeleteViewParams, QueryViewParams},
+    entities::view::{DeleteViewParams, QueryViewParams, RepeatedView},
     module::WorkspaceUser,
+    observable::ObservableBuilder,
 };
 use std::sync::Arc;
 
@@ -30,7 +31,10 @@ impl ViewController {
         let view_table = ViewTable::new(view.clone());
         let _ = self.sql.create_view(view_table)?;
 
-        send_observable(&view.belong_to_id, WorkspaceObservable::AppCreateView);
+        let repeated_view = self.read_local_views_belong_to(&view.belong_to_id)?;
+        ObservableBuilder::new(&view.belong_to_id, WorkspaceObservable::AppCreateView)
+            .payload(repeated_view)
+            .build();
         Ok(view)
     }
 
@@ -42,14 +46,19 @@ impl ViewController {
     }
 
     pub(crate) async fn delete_view(&self, view_id: &str) -> Result<(), WorkspaceError> {
-        let view = self.sql.delete_view(view_id)?;
+        let view_table = self.sql.delete_view(view_id)?;
         let _ = self.delete_view_on_server(view_id).await?;
-        send_observable(&view.belong_to_id, WorkspaceObservable::AppDeleteView);
+
+        let repeated_view = self.read_local_views_belong_to(&view_table.belong_to_id)?;
+        ObservableBuilder::new(&view_table.belong_to_id, WorkspaceObservable::AppDeleteView)
+            .payload(repeated_view)
+            .build();
         Ok(())
     }
 
     // belong_to_id will be the app_id or view_id.
-    pub(crate) async fn read_views_belong_to(&self, belong_to_id: &str) -> Result<Vec<View>, WorkspaceError> {
+    pub(crate) async fn read_views_belong_to(&self, belong_to_id: &str) -> Result<RepeatedView, WorkspaceError> {
+        // TODO: read from server
         let views = self
             .sql
             .read_views_belong_to(belong_to_id)?
@@ -57,16 +66,19 @@ impl ViewController {
             .map(|view_table| view_table.into())
             .collect::<Vec<View>>();
 
-        Ok(views)
+        Ok(RepeatedView { items: views })
     }
 
     pub(crate) async fn update_view(&self, params: UpdateViewParams) -> Result<(), WorkspaceError> {
         let changeset = ViewTableChangeset::new(params.clone());
         let view_id = changeset.id.clone();
         let _ = self.sql.update_view(changeset)?;
-
         let _ = self.update_view_on_server(params).await?;
-        send_observable(&view_id, WorkspaceObservable::ViewUpdated);
+
+        let view: View = self.sql.read_view(&view_id, false)?.into();
+        ObservableBuilder::new(&view_id, WorkspaceObservable::ViewUpdated)
+            .payload(view)
+            .build();
         Ok(())
     }
 }
@@ -129,4 +141,16 @@ impl ViewController {
         });
         Ok(())
     }
+
+    // belong_to_id will be the app_id or view_id.
+    fn read_local_views_belong_to(&self, belong_to_id: &str) -> Result<RepeatedView, WorkspaceError> {
+        let views = self
+            .sql
+            .read_views_belong_to(belong_to_id)?
+            .into_iter()
+            .map(|view_table| view_table.into())
+            .collect::<Vec<View>>();
+
+        Ok(RepeatedView { items: views })
+    }
 }

+ 76 - 49
rust-lib/flowy-workspace/src/services/workspace_controller.rs

@@ -2,13 +2,14 @@ use crate::{
     entities::{app::App, workspace::*},
     errors::*,
     module::{WorkspaceDatabase, WorkspaceUser},
-    observable::{send_observable, WorkspaceObservable},
+    observable::WorkspaceObservable,
     services::{helper::spawn, server::Server, AppController},
     sql_tables::workspace::{WorkspaceSql, WorkspaceTable, WorkspaceTableChangeset},
 };
 
 use flowy_infra::kv::KV;
 
+use crate::{entities::app::RepeatedApp, observable::ObservableBuilder};
 use std::sync::Arc;
 
 pub(crate) struct WorkspaceController {
@@ -25,7 +26,7 @@ impl WorkspaceController {
         app_controller: Arc<AppController>,
         server: Server,
     ) -> Self {
-        let sql = Arc::new(WorkspaceSql { database });
+        let sql = Arc::new(WorkspaceSql::new(database));
         Self {
             user,
             sql,
@@ -39,7 +40,12 @@ impl WorkspaceController {
         let user_id = self.user.user_id()?;
         let workspace_table = WorkspaceTable::new(workspace.clone(), &user_id);
         let _ = self.sql.create_workspace(workspace_table)?;
-        send_observable(&user_id, WorkspaceObservable::UserCreateWorkspace);
+
+        // Opti: read all local workspaces may cause performance issues
+        let repeated_workspace = self.read_local_workspaces(None, &user_id)?;
+        ObservableBuilder::new(&user_id, WorkspaceObservable::UserCreateWorkspace)
+            .payload(repeated_workspace)
+            .build();
         Ok(workspace)
     }
 
@@ -48,7 +54,13 @@ impl WorkspaceController {
         let workspace_id = changeset.id.clone();
         let _ = self.sql.update_workspace(changeset)?;
         let _ = self.update_workspace_on_server(params).await?;
-        send_observable(&workspace_id, WorkspaceObservable::WorkspaceUpdated);
+
+        // Opti: transaction
+        let user_id = self.user.user_id()?;
+        let workspace = self.read_local_workspace(workspace_id.clone(), &user_id)?;
+        ObservableBuilder::new(&workspace_id, WorkspaceObservable::WorkspaceUpdated)
+            .payload(workspace)
+            .build();
         Ok(())
     }
 
@@ -56,23 +68,21 @@ impl WorkspaceController {
         let user_id = self.user.user_id()?;
         let _ = self.sql.delete_workspace(workspace_id)?;
         let _ = self.delete_workspace_on_server(workspace_id).await?;
-        send_observable(&user_id, WorkspaceObservable::UserDeleteWorkspace);
+
+        // Opti: read all local workspaces may cause performance issues
+        let repeated_workspace = self.read_local_workspaces(None, &user_id)?;
+        ObservableBuilder::new(&user_id, WorkspaceObservable::UserDeleteWorkspace)
+            .payload(repeated_workspace)
+            .build();
         Ok(())
     }
 
     pub(crate) async fn open_workspace(&self, params: QueryWorkspaceParams) -> Result<Workspace, WorkspaceError> {
         let user_id = self.user.user_id()?;
         if let Some(workspace_id) = params.workspace_id.clone() {
-            self.read_workspaces_on_server(params.clone());
-            let result = self.read_workspace_table(Some(workspace_id), user_id)?;
-            match result.first() {
-                None => Err(ErrorBuilder::new(ErrorCode::RecordNotFound).build()),
-                Some(workspace_table) => {
-                    let workspace: Workspace = workspace_table.clone().into();
-                    set_current_workspace(&workspace.id);
-                    Ok(workspace)
-                },
-            }
+            let workspace = self.read_local_workspace(workspace_id, &user_id)?;
+            set_current_workspace(&workspace.id);
+            Ok(workspace)
         } else {
             return Err(ErrorBuilder::new(ErrorCode::WorkspaceIdInvalid)
                 .msg("Opened workspace id should not be empty")
@@ -82,25 +92,44 @@ impl WorkspaceController {
 
     pub(crate) async fn read_workspaces(&self, params: QueryWorkspaceParams) -> Result<RepeatedWorkspace, WorkspaceError> {
         let user_id = self.user.user_id()?;
-        let workspace_tables = self.read_workspace_table(params.workspace_id.clone(), user_id.clone())?;
+        let workspaces = self.read_local_workspaces(params.workspace_id.clone(), &user_id)?;
+        let _ = self.read_workspaces_on_server(user_id, params).await?;
+        Ok(workspaces)
+    }
+
+    pub(crate) async fn read_cur_workspace(&self) -> Result<Workspace, WorkspaceError> {
+        let workspace_id = get_current_workspace()?;
+        let user_id = self.user.user_id()?;
+        let workspace = self.read_local_workspace(workspace_id, &user_id)?;
+        Ok(workspace)
+    }
+
+    pub(crate) async fn read_workspace_apps(&self) -> Result<RepeatedApp, WorkspaceError> {
+        let workspace_id = get_current_workspace()?;
+        let apps = self.read_local_apps(&workspace_id)?;
+        // TODO: read from server
+        Ok(RepeatedApp { items: apps })
+    }
+
+    #[tracing::instrument(level = "debug", skip(self), err)]
+    fn read_local_workspaces(&self, workspace_id: Option<String>, user_id: &str) -> Result<RepeatedWorkspace, WorkspaceError> {
+        let sql = self.sql.clone();
+        let workspace_id = workspace_id.to_owned();
+        let workspace_tables = sql.read_workspaces(workspace_id, user_id)?;
+
         let mut workspaces = vec![];
         for table in workspace_tables {
-            let apps = self.read_apps(&table.id).await?;
+            let apps = self.read_local_apps(&table.id)?;
             let mut workspace: Workspace = table.into();
             workspace.apps.items = apps;
             workspaces.push(workspace);
         }
-
-        let _ = self.read_workspaces_on_server(user_id, params).await?;
         Ok(RepeatedWorkspace { items: workspaces })
     }
 
-    pub(crate) async fn read_cur_workspace(&self) -> Result<Workspace, WorkspaceError> {
-        let params = QueryWorkspaceParams {
-            workspace_id: Some(get_current_workspace()?),
-        };
-        let mut repeated_workspace = self.read_workspaces(params).await?;
-
+    fn read_local_workspace(&self, workspace_id: String, user_id: &str) -> Result<Workspace, WorkspaceError> {
+        // Opti: fetch single workspace from local db
+        let mut repeated_workspace = self.read_local_workspaces(Some(workspace_id), user_id)?;
         if repeated_workspace.is_empty() {
             return Err(ErrorBuilder::new(ErrorCode::RecordNotFound).build());
         }
@@ -110,13 +139,8 @@ impl WorkspaceController {
         Ok(workspace)
     }
 
-    pub(crate) async fn read_cur_apps(&self) -> Result<Vec<App>, WorkspaceError> {
-        let workspace_id = get_current_workspace()?;
-        let apps = self.read_apps(&workspace_id).await?;
-        Ok(apps)
-    }
-
-    pub(crate) async fn read_apps(&self, workspace_id: &str) -> Result<Vec<App>, WorkspaceError> {
+    #[tracing::instrument(level = "debug", skip(self), err)]
+    fn read_local_apps(&self, workspace_id: &str) -> Result<Vec<App>, WorkspaceError> {
         let apps = self
             .sql
             .read_apps_belong_to_workspace(workspace_id)?
@@ -126,13 +150,6 @@ impl WorkspaceController {
 
         Ok(apps)
     }
-
-    fn read_workspace_table(&self, workspace_id: Option<String>, user_id: String) -> Result<Vec<WorkspaceTable>, WorkspaceError> {
-        let sql = self.sql.clone();
-        let workspace_id = workspace_id.to_owned();
-        let workspace = sql.read_workspaces(workspace_id, &user_id)?;
-        Ok(workspace)
-    }
 }
 
 impl WorkspaceController {
@@ -185,17 +202,27 @@ impl WorkspaceController {
     #[tracing::instrument(skip(self), err)]
     async fn read_workspaces_on_server(&self, user_id: String, params: QueryWorkspaceParams) -> Result<(), WorkspaceError> {
         let (token, server) = self.token_with_server()?;
+        let sql = self.sql.clone();
+        let conn = self.sql.get_db_conn()?;
         spawn(async move {
-            match server.read_workspace(&token, params).await {
-                Ok(workspaces) => {
-                    log::debug!("Workspace list: {:?}", workspaces);
-                    send_observable(&user_id, WorkspaceObservable::UserCreateWorkspace);
-                },
-                Err(e) => {
-                    // TODO: retry?
-                    log::error!("Delete workspace failed: {:?}", e);
-                },
-            }
+            // Opti: retry?
+            let workspaces = server.read_workspace(&token, params).await?;
+            let _ = (&*conn).immediate_transaction::<_, WorkspaceError, _>(|| {
+                for workspace in &workspaces.items {
+                    let mut m_workspace = workspace.clone();
+                    let repeated_app = m_workspace.apps.take_items();
+                    let workspace_table = WorkspaceTable::new(m_workspace, &user_id);
+                    log::debug!("Save workspace: {} to disk", &workspace.id);
+                    let _ = sql.create_workspace_with(workspace_table, &*conn)?;
+                    log::debug!("Save workspace: {} apps to disk", &workspace.id);
+                    let _ = sql.create_apps(repeated_app, &*conn)?;
+                }
+                Ok(())
+            })?;
+            ObservableBuilder::new(&user_id, WorkspaceObservable::WorkspaceListUpdated)
+                .payload(workspaces)
+                .build();
+            Result::<(), WorkspaceError>::Ok(())
         });
 
         Ok(())

+ 29 - 10
rust-lib/flowy-workspace/src/sql_tables/app/app_sql.rs

@@ -6,33 +6,43 @@ use crate::{
 use flowy_database::{
     prelude::*,
     schema::{app_table, app_table::dsl},
+    SqliteConnection,
 };
 use std::sync::Arc;
 
 pub struct AppTableSql {
-    pub database: Arc<dyn WorkspaceDatabase>,
+    database: Arc<dyn WorkspaceDatabase>,
+}
+
+impl AppTableSql {
+    pub fn new(database: Arc<dyn WorkspaceDatabase>) -> Self { Self { database } }
 }
 
 impl AppTableSql {
     pub(crate) fn create_app(&self, app_table: AppTable) -> Result<(), WorkspaceError> {
         let conn = self.database.db_connection()?;
-        let _ = diesel::insert_into(app_table::table)
-            .values(app_table)
-            .execute(&*conn)?;
+        let _ = self.create_app_with(app_table, &*conn)?;
+        Ok(())
+    }
+
+    pub(crate) fn create_app_with(&self, app_table: AppTable, conn: &SqliteConnection) -> Result<(), WorkspaceError> {
+        match diesel_record_count!(app_table, &app_table.id, conn) {
+            0 => diesel_insert_table!(app_table, &app_table, conn),
+            _ => {
+                let changeset = AppTableChangeset::from_table(app_table);
+                diesel_update_table!(app_table, changeset, conn)
+            },
+        }
         Ok(())
     }
 
     pub(crate) fn update_app(&self, changeset: AppTableChangeset) -> Result<(), WorkspaceError> {
         let conn = self.database.db_connection()?;
-        diesel_update_table!(app_table, changeset, conn);
+        diesel_update_table!(app_table, changeset, &*conn);
         Ok(())
     }
 
-    pub(crate) fn read_app(
-        &self,
-        app_id: &str,
-        is_trash: bool,
-    ) -> Result<AppTable, WorkspaceError> {
+    pub(crate) fn read_app(&self, app_id: &str, is_trash: bool) -> Result<AppTable, WorkspaceError> {
         let app_table = dsl::app_table
             .filter(app_table::id.eq(app_id))
             .filter(app_table::is_trash.eq(is_trash))
@@ -41,6 +51,15 @@ impl AppTableSql {
         Ok(app_table)
     }
 
+    pub(crate) fn read_apps(&self, workspace_id: &str, is_trash: bool) -> Result<Vec<AppTable>, WorkspaceError> {
+        let app_table = dsl::app_table
+            .filter(app_table::workspace_id.eq(workspace_id))
+            .filter(app_table::is_trash.eq(is_trash))
+            .load::<AppTable>(&*(self.database.db_connection()?))?;
+
+        Ok(app_table)
+    }
+
     pub(crate) fn delete_app(&self, app_id: &str) -> Result<AppTable, WorkspaceError> {
         let conn = self.database.db_connection()?;
         // TODO: group into sql transaction

+ 11 - 2
rust-lib/flowy-workspace/src/sql_tables/app/app_table.rs

@@ -75,7 +75,7 @@ impl_sql_binary_expression!(ColorStyleCol);
 
 #[derive(AsChangeset, Identifiable, Default, Debug)]
 #[table_name = "app_table"]
-pub struct AppTableChangeset {
+pub(crate) struct AppTableChangeset {
     pub id: String,
     pub name: Option<String>,
     pub desc: Option<String>,
@@ -83,7 +83,7 @@ pub struct AppTableChangeset {
 }
 
 impl AppTableChangeset {
-    pub fn new(params: UpdateAppParams) -> Self {
+    pub(crate) fn new(params: UpdateAppParams) -> Self {
         AppTableChangeset {
             id: params.app_id,
             name: params.name,
@@ -91,6 +91,15 @@ impl AppTableChangeset {
             is_trash: params.is_trash,
         }
     }
+
+    pub(crate) fn from_table(table: AppTable) -> Self {
+        AppTableChangeset {
+            id: table.id,
+            name: Some(table.name),
+            desc: Some(table.desc),
+            is_trash: Some(table.is_trash),
+        }
+    }
 }
 
 impl std::convert::Into<App> for AppTable {

+ 4 - 13
rust-lib/flowy-workspace/src/sql_tables/view/view_sql.rs

@@ -16,17 +16,11 @@ pub struct ViewTableSql {
 impl ViewTableSql {
     pub(crate) fn create_view(&self, view_table: ViewTable) -> Result<(), WorkspaceError> {
         let conn = self.database.db_connection()?;
-        let _ = diesel::insert_into(view_table::table)
-            .values(view_table)
-            .execute(&*conn)?;
+        let _ = diesel::insert_into(view_table::table).values(view_table).execute(&*conn)?;
         Ok(())
     }
 
-    pub(crate) fn read_view(
-        &self,
-        view_id: &str,
-        is_trash: bool,
-    ) -> Result<ViewTable, WorkspaceError> {
+    pub(crate) fn read_view(&self, view_id: &str, is_trash: bool) -> Result<ViewTable, WorkspaceError> {
         let view_table = dsl::view_table
             .filter(view_table::id.eq(view_id))
             .filter(view_table::is_trash.eq(is_trash))
@@ -35,10 +29,7 @@ impl ViewTableSql {
         Ok(view_table)
     }
 
-    pub(crate) fn read_views_belong_to(
-        &self,
-        belong_to_id: &str,
-    ) -> Result<Vec<ViewTable>, WorkspaceError> {
+    pub(crate) fn read_views_belong_to(&self, belong_to_id: &str) -> Result<Vec<ViewTable>, WorkspaceError> {
         let view_tables = dsl::view_table
             .filter(view_table::belong_to_id.eq(belong_to_id))
             .load::<ViewTable>(&*(self.database.db_connection()?))?;
@@ -48,7 +39,7 @@ impl ViewTableSql {
 
     pub(crate) fn update_view(&self, changeset: ViewTableChangeset) -> Result<(), WorkspaceError> {
         let conn = self.database.db_connection()?;
-        diesel_update_table!(view_table, changeset, conn);
+        diesel_update_table!(view_table, changeset, &*conn);
         Ok(())
     }
 

+ 58 - 22
rust-lib/flowy-workspace/src/sql_tables/workspace/workspace_sql.rs

@@ -1,34 +1,71 @@
 use crate::{
+    entities::app::App,
     errors::WorkspaceError,
     module::WorkspaceDatabase,
     sql_tables::{
-        app::AppTable,
+        app::{AppTable, AppTableSql},
         workspace::{WorkspaceTable, WorkspaceTableChangeset},
     },
 };
+use diesel::SqliteConnection;
 use flowy_database::{
+    macros::*,
     prelude::*,
     schema::{workspace_table, workspace_table::dsl},
+    DBConnection,
 };
 use std::sync::Arc;
 
-pub struct WorkspaceSql {
-    pub database: Arc<dyn WorkspaceDatabase>,
+pub(crate) struct WorkspaceSql {
+    database: Arc<dyn WorkspaceDatabase>,
+    app_sql: Arc<AppTableSql>,
 }
 
 impl WorkspaceSql {
-    pub fn create_workspace(&self, workspace_table: WorkspaceTable) -> Result<(), WorkspaceError> {
-        let _ = diesel::insert_into(workspace_table::table)
-            .values(workspace_table)
-            .execute(&*(self.database.db_connection()?))?;
+    pub fn new(database: Arc<dyn WorkspaceDatabase>) -> Self {
+        Self {
+            database: database.clone(),
+            app_sql: Arc::new(AppTableSql::new(database.clone())),
+        }
+    }
+}
+
+impl WorkspaceSql {
+    pub(crate) fn create_workspace(&self, table: WorkspaceTable) -> Result<(), WorkspaceError> {
+        let conn = &*self.database.db_connection()?;
+        //[[immediate_transaction]]
+        // https://sqlite.org/lang_transaction.html
+        // IMMEDIATE cause the database connection to start a new write immediately,
+        // without waiting for a write statement. The BEGIN IMMEDIATE might fail
+        // with SQLITE_BUSY if another write transaction is already active on another
+        // database connection.
+        //
+        // EXCLUSIVE is similar to IMMEDIATE in that a write transaction is started
+        // immediately. EXCLUSIVE and IMMEDIATE are the same in WAL mode, but in
+        // other journaling modes, EXCLUSIVE prevents other database connections from
+        // reading the database while the transaction is underway.
+        (conn).immediate_transaction::<_, WorkspaceError, _>(|| self.create_workspace_with(table, conn))
+    }
+
+    pub(crate) fn create_workspace_with(&self, table: WorkspaceTable, conn: &SqliteConnection) -> Result<(), WorkspaceError> {
+        match diesel_record_count!(workspace_table, &table.id, conn) {
+            0 => diesel_insert_table!(workspace_table, &table, conn),
+            _ => {
+                let changeset = WorkspaceTableChangeset::from_table(table);
+                diesel_update_table!(workspace_table, changeset, conn);
+            },
+        }
         Ok(())
     }
 
-    pub fn read_workspaces(
-        &self,
-        workspace_id: Option<String>,
-        user_id: &str,
-    ) -> Result<Vec<WorkspaceTable>, WorkspaceError> {
+    pub(crate) fn create_apps(&self, apps: Vec<App>, conn: &SqliteConnection) -> Result<(), WorkspaceError> {
+        for app in apps {
+            let _ = self.app_sql.create_app_with(AppTable::new(app), conn)?;
+        }
+        Ok(())
+    }
+
+    pub(crate) fn read_workspaces(&self, workspace_id: Option<String>, user_id: &str) -> Result<Vec<WorkspaceTable>, WorkspaceError> {
         let workspaces = match workspace_id {
             None => dsl::workspace_table
                 .filter(workspace_table::user_id.eq(user_id))
@@ -42,25 +79,19 @@ impl WorkspaceSql {
         Ok(workspaces)
     }
 
-    pub fn update_workspace(
-        &self,
-        changeset: WorkspaceTableChangeset,
-    ) -> Result<(), WorkspaceError> {
+    pub(crate) fn update_workspace(&self, changeset: WorkspaceTableChangeset) -> Result<(), WorkspaceError> {
         let conn = self.database.db_connection()?;
-        diesel_update_table!(workspace_table, changeset, conn);
+        diesel_update_table!(workspace_table, changeset, &*conn);
         Ok(())
     }
 
-    pub fn delete_workspace(&self, workspace_id: &str) -> Result<(), WorkspaceError> {
+    pub(crate) fn delete_workspace(&self, workspace_id: &str) -> Result<(), WorkspaceError> {
         let conn = self.database.db_connection()?;
         diesel_delete_table!(workspace_table, workspace_id, conn);
         Ok(())
     }
 
-    pub(crate) fn read_apps_belong_to_workspace(
-        &self,
-        workspace_id: &str,
-    ) -> Result<Vec<AppTable>, WorkspaceError> {
+    pub(crate) fn read_apps_belong_to_workspace(&self, workspace_id: &str) -> Result<Vec<AppTable>, WorkspaceError> {
         let conn = self.database.db_connection()?;
 
         let apps = conn.immediate_transaction::<_, WorkspaceError, _>(|| {
@@ -73,4 +104,9 @@ impl WorkspaceSql {
 
         Ok(apps)
     }
+
+    pub(crate) fn get_db_conn(&self) -> Result<DBConnection, WorkspaceError> {
+        let db = self.database.db_connection()?;
+        Ok(db)
+    }
 }

+ 8 - 0
rust-lib/flowy-workspace/src/sql_tables/workspace/workspace_table.rs

@@ -60,4 +60,12 @@ impl WorkspaceTableChangeset {
             desc: params.desc,
         }
     }
+
+    pub(crate) fn from_table(table: WorkspaceTable) -> Self {
+        WorkspaceTableChangeset {
+            id: table.id,
+            name: Some(table.name),
+            desc: Some(table.desc),
+        }
+    }
 }