feat: init flowy document 2 (#2248)

* feat: init flowy document 2

* feat: convert inner document to document PB

* feat: integrate colla document into Flutter

* feat: integrate colla document into tauri

* fix: cargo clippy
This commit is contained in:
Lucas.Xu 2023-04-13 18:53:51 +08:00 committed by GitHub
parent c7eb490db4
commit ec89e9517b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 1566 additions and 443 deletions

View File

@ -0,0 +1,24 @@
import 'dart:typed_data';
import 'package:appflowy/core/notification_helper.dart';
import 'package:appflowy_backend/protobuf/flowy-document2/notification.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:dartz/dartz.dart';
typedef DocumentNotificationCallback = void Function(
DocumentNotification,
Either<Uint8List, FlowyError>,
);
class DocumentNotificationParser
extends NotificationParser<DocumentNotification, FlowyError> {
DocumentNotificationParser({
String? id,
required DocumentNotificationCallback callback,
}) : super(
id: id,
callback: callback,
tyParser: (ty) => DocumentNotification.valueOf(ty),
errorParser: (bytes) => FlowyError.fromBuffer(bytes),
);
}

View File

@ -3,6 +3,7 @@ import 'package:appflowy/plugins/document/presentation/plugins/cover/cover_node_
import 'package:appflowy/plugins/trash/application/trash_service.dart';
import 'package:appflowy/user/application/user_service.dart';
import 'package:appflowy/workspace/application/view/view_listener.dart';
import 'package:appflowy/workspace/application/doc/doc_listener.dart';
import 'package:appflowy/plugins/document/application/doc_service.dart';
import 'package:appflowy_backend/protobuf/flowy-document/entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pbserver.dart';
@ -17,12 +18,13 @@ import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:dartz/dartz.dart';
import 'dart:async';
import 'package:appflowy/util/either_extension.dart';
import 'package:appflowy_backend/protobuf/flowy-document2/entities.pb.dart';
part 'doc_bloc.freezed.dart';
class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
final ViewPB view;
final DocumentService _documentService;
final DocumentListener _docListener;
final ViewListener _listener;
final TrashService _trashService;
@ -32,12 +34,14 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
DocumentBloc({
required this.view,
}) : _documentService = DocumentService(),
_docListener = DocumentListener(id: view.id),
_listener = ViewListener(view: view),
_trashService = TrashService(),
super(DocumentState.initial()) {
on<DocumentEvent>((event, emit) async {
await event.map(
initial: (Initial value) async {
_listenOnDocChange();
await _initial(value, emit);
_listenOnViewChange();
},
@ -73,6 +77,7 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
}
await _documentService.closeDocument(docId: view.id);
await _documentService.closeDocumentV2(view: view);
return super.close();
}
@ -88,6 +93,39 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
);
}
final result = await _documentService.openDocument(view: view);
// test code
final document = await _documentService.openDocumentV2(view: view);
BlockPB? root;
document.fold((l) {
print('---------<open document v2>-----------');
print('page id = ${l.pageId}');
l.blocks.blocks.forEach((key, value) {
print('-----<block begin>-----');
print('block = $value');
if (value.ty == 'page') {
root = value;
}
print('-----<block end>-----');
});
print('---------<open document v2>-----------');
}, (r) {});
if (root != null) {
await _documentService.applyAction(
view: view,
actions: [
BlockActionPB(
action: BlockActionTypePB.Insert,
payload: BlockActionPayloadPB(
block: BlockPB()
..id = 'id_0'
..ty = 'text'
..parentId = root!.id,
),
),
],
);
}
return result.fold(
(documentData) async {
await _initEditorState(documentData).whenComplete(() {
@ -126,6 +164,14 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
);
}
void _listenOnDocChange() {
_docListener.start(
didReceiveUpdate: () {
print('---------<receive document update>-----------');
},
);
}
Future<void> _initEditorState(DocumentDataPB documentData) async {
final document = Document.fromJson(jsonDecode(documentData.content));
final editorState = EditorState(document: document);

View File

@ -4,6 +4,7 @@ import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-document/entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-document2/entities.pb.dart';
class DocumentService {
Future<Either<DocumentDataPB, FlowyError>> openDocument({
@ -39,4 +40,32 @@ class DocumentService {
final payload = ViewIdPB(value: docId);
return FolderEventCloseView(payload).send();
}
Future<Either<DocumentDataPB2, FlowyError>> openDocumentV2({
required ViewPB view,
}) async {
await FolderEventSetLatestView(ViewIdPB(value: view.id)).send();
final payload = OpenDocumentPayloadPBV2()..documentId = view.id;
return DocumentEvent2OpenDocument(payload).send();
}
Future<Either<Unit, FlowyError>> closeDocumentV2({
required ViewPB view,
}) async {
final payload = CloseDocumentPayloadPBV2()..documentId = view.id;
return DocumentEvent2CloseDocument(payload).send();
}
Future<Either<Unit, FlowyError>> applyAction({
required ViewPB view,
required List<BlockActionPB> actions,
}) async {
final payload = ApplyActionPayloadPBV2(
documentId: view.id,
actions: actions,
);
return DocumentEvent2ApplyAction(payload).send();
}
}

View File

@ -0,0 +1,54 @@
import 'dart:async';
import 'dart:typed_data';
import 'package:appflowy/core/document_notification.dart';
import 'package:dartz/dartz.dart';
import 'package:appflowy_backend/protobuf/flowy-notification/subject.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-document2/notification.pb.dart';
import 'package:appflowy_backend/rust_stream.dart';
import 'package:flowy_infra/notifier.dart';
class DocumentListener {
DocumentListener({
required this.id,
});
final String id;
final _didReceiveUpdate = PublishNotifier();
StreamSubscription<SubscribeObject>? _subscription;
DocumentNotificationParser? _parser;
Function()? didReceiveUpdate;
void start({
void Function()? didReceiveUpdate,
}) {
this.didReceiveUpdate = didReceiveUpdate;
_parser = DocumentNotificationParser(
id: id,
callback: _callback,
);
_subscription = RustStreamReceiver.listen(
(observable) => _parser?.parse(observable),
);
}
void _callback(
DocumentNotification ty,
Either<Uint8List, FlowyError> result,
) {
switch (ty) {
case DocumentNotification.DidReceiveUpdate:
didReceiveUpdate?.call();
break;
default:
break;
}
}
Future<void> stop() async {
await _subscription?.cancel();
}
}

View File

@ -17,6 +17,7 @@ import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:appflowy_backend/protobuf/dart-ffi/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-document/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-document2/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-database/protobuf.dart';
// ignore: unused_import
@ -30,6 +31,7 @@ part 'dart_event/flowy-net/dart_event.dart';
part 'dart_event/flowy-user/dart_event.dart';
part 'dart_event/flowy-database/dart_event.dart';
part 'dart_event/flowy-document/dart_event.dart';
part 'dart_event/flowy-document2/dart_event.dart';
enum FFIException {
RequestIsEmpty,

View File

@ -564,7 +564,7 @@ dependencies = [
[[package]]
name = "collab"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab#9b3f895bb6f8e92830acd90cfb68b69aece83095"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab#4ccf3e5cf88ee71f1ce257e587cca54a8173abae"
dependencies = [
"anyhow",
"bytes",
@ -582,7 +582,7 @@ dependencies = [
[[package]]
name = "collab-derive"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab#9b3f895bb6f8e92830acd90cfb68b69aece83095"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab#4ccf3e5cf88ee71f1ce257e587cca54a8173abae"
dependencies = [
"proc-macro2",
"quote",
@ -591,10 +591,26 @@ dependencies = [
"yrs",
]
[[package]]
name = "collab-document"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab#4ccf3e5cf88ee71f1ce257e587cca54a8173abae"
dependencies = [
"anyhow",
"collab",
"collab-derive",
"collab-persistence",
"nanoid",
"parking_lot 0.12.1",
"serde",
"serde_json",
"thiserror",
]
[[package]]
name = "collab-folder"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab#9b3f895bb6f8e92830acd90cfb68b69aece83095"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab#4ccf3e5cf88ee71f1ce257e587cca54a8173abae"
dependencies = [
"anyhow",
"collab",
@ -612,7 +628,7 @@ dependencies = [
[[package]]
name = "collab-persistence"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab#9b3f895bb6f8e92830acd90cfb68b69aece83095"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab#4ccf3e5cf88ee71f1ce257e587cca54a8173abae"
dependencies = [
"bincode",
"chrono",
@ -1284,6 +1300,7 @@ dependencies = [
"flowy-client-ws",
"flowy-database",
"flowy-document",
"flowy-document2",
"flowy-error",
"flowy-folder",
"flowy-folder2",
@ -1407,6 +1424,28 @@ dependencies = [
"ws-model",
]
[[package]]
name = "flowy-document2"
version = "0.1.0"
dependencies = [
"bytes",
"collab",
"collab-document",
"collab-persistence",
"flowy-codegen",
"flowy-derive",
"flowy-error",
"flowy-notification",
"lib-dispatch",
"nanoid",
"parking_lot 0.12.1",
"protobuf",
"serde",
"serde_json",
"strum",
"strum_macros",
]
[[package]]
name = "flowy-error"
version = "0.1.0"
@ -1511,6 +1550,7 @@ dependencies = [
"flowy-codegen",
"flowy-derive",
"flowy-document",
"flowy-document2",
"flowy-error",
"flowy-folder2",
"flowy-server-sync",

View File

@ -36,9 +36,13 @@ custom-protocol = ["tauri/custom-protocol"]
collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab" }
collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab" }
collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab" }
collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab" }
#collab = { path = "../../AppFlowy-Collab/collab" }
#collab-folder = { path = "../../AppFlowy-Collab/collab-folder" }
#collab-persistence = { path = "../../AppFlowy-Collab/collab-persistence" }
#collab-document = { path = "../../AppFlowy-Collab/collab-document" }

View File

@ -4,11 +4,22 @@ import {
EditPayloadPB,
FlowyError,
OpenDocumentPayloadPB,
DocumentDataPB2,
ViewIdPB,
OpenDocumentPayloadPBV2,
ApplyActionPayloadPBV2,
BlockActionTypePB,
BlockActionPB,
CloseDocumentPayloadPBV2,
} from '@/services/backend';
import { DocumentEventApplyEdit, DocumentEventGetDocument } from '@/services/backend/events/flowy-document';
import { Result } from 'ts-results';
import { FolderEventCloseView } from '@/services/backend/events/flowy-folder2';
import {
DocumentEvent2ApplyAction,
DocumentEvent2CloseDocument,
DocumentEvent2OpenDocument,
} from '@/services/backend/events/flowy-document2';
export class DocumentBackendService {
constructor(public readonly viewId: string) {}
@ -27,4 +38,26 @@ export class DocumentBackendService {
const payload = ViewIdPB.fromObject({ value: this.viewId });
return FolderEventCloseView(payload);
};
openV2 = (): Promise<Result<DocumentDataPB2, FlowyError>> => {
const payload = OpenDocumentPayloadPBV2.fromObject({
document_id: this.viewId,
});
return DocumentEvent2OpenDocument(payload);
};
applyActions = (actions: [BlockActionPB]): Promise<Result<void, FlowyError>> => {
const payload = ApplyActionPayloadPBV2.fromObject({
document_id: this.viewId,
actions: actions,
});
return DocumentEvent2ApplyAction(payload);
};
closeV2 = (): Promise<Result<void, FlowyError>> => {
const payload = CloseDocumentPayloadPBV2.fromObject({
document_id: this.viewId,
});
return DocumentEvent2CloseDocument(payload);
};
}

View File

@ -2,37 +2,70 @@ import { DocumentData, BlockType, TextDelta } from '@/appflowy_app/interfaces/do
import { createContext } from 'react';
import { DocumentBackendService } from './document_bd_svc';
import { Err } from 'ts-results';
import { FlowyError } from '@/services/backend';
import { BlockActionPB, BlockActionPayloadPB, BlockActionTypePB, BlockPB, FlowyError } from '@/services/backend';
import { DocumentObserver } from './document_observer';
import { nanoid } from 'nanoid';
export const DocumentControllerContext = createContext<DocumentController | null>(null);
export class DocumentController {
private readonly backendService: DocumentBackendService;
private readonly observer: DocumentObserver;
constructor(public readonly viewId: string) {
this.backendService = new DocumentBackendService(viewId);
this.observer = new DocumentObserver(viewId);
}
open = async (): Promise<DocumentData | null> => {
// example:
await this.observer.subscribe({
didReceiveUpdate: () => {
console.log('didReceiveUpdate');
},
});
const document = await this.backendService.openV2();
let root_id = '';
if (document.ok) {
root_id = document.val.page_id;
console.log(document.val.blocks);
}
await this.backendService.applyActions([
BlockActionPB.fromObject({
action: BlockActionTypePB.Insert,
payload: BlockActionPayloadPB.fromObject({
block: BlockPB.fromObject({
id: nanoid(10),
ty: 'text',
parent_id: root_id,
}),
}),
}),
]);
const openDocumentResult = await this.backendService.open();
if (openDocumentResult.ok) {
return {
rootId: '',
blocks: {},
ytexts: {},
yarrays: {}
yarrays: {},
};
} else {
return null;
}
};
insert(node: {
id: string,
type: BlockType,
delta?: TextDelta[]
}, parentId: string, prevId: string) {
insert(
node: {
id: string;
type: BlockType;
delta?: TextDelta[];
},
parentId: string,
prevId: string
) {
//
}
@ -42,7 +75,7 @@ export class DocumentController {
yTextApply = (yTextId: string, delta: TextDelta[]) => {
//
}
};
dispose = async () => {
await this.backendService.close();

View File

@ -0,0 +1,36 @@
import { Ok, Result } from 'ts-results';
import { ChangeNotifier } from '$app/utils/change_notifier';
import { FolderNotificationObserver } from '../folder/notifications/observer';
import { DocumentNotification } from '@/services/backend';
import { DocumentNotificationObserver } from './notifications/observer';
export type DidReceiveUpdateCallback = () => void; // todo: add params
export class DocumentObserver {
private listener?: DocumentNotificationObserver;
constructor(public readonly workspaceId: string) {}
subscribe = async (callbacks: { didReceiveUpdate: DidReceiveUpdateCallback }) => {
this.listener = new DocumentNotificationObserver({
viewId: this.workspaceId,
parserHandler: (notification, result) => {
switch (notification) {
case DocumentNotification.DidReceiveUpdate:
callbacks.didReceiveUpdate();
// Fixme: ...
break;
default:
break;
}
},
});
await this.listener.start();
};
unsubscribe = async () => {
this.appListNotifier.unsubscribe();
this.workspaceNotifier.unsubscribe();
await this.listener?.stop();
};
}

View File

@ -0,0 +1,16 @@
import { OnNotificationError, AFNotificationObserver } from '@/services/backend/notifications';
import { DocumentNotificationParser } from './parser';
import { FlowyError, DocumentNotification } from '@/services/backend';
import { Result } from 'ts-results';
export type ParserHandler = (notification: DocumentNotification, payload: Result<Uint8Array, FlowyError>) => void;
export class DocumentNotificationObserver extends AFNotificationObserver<DocumentNotification> {
constructor(params: { viewId?: string; parserHandler: ParserHandler; onError?: OnNotificationError }) {
const parser = new DocumentNotificationParser({
callback: params.parserHandler,
id: params.viewId,
});
super(parser);
}
}

View File

@ -0,0 +1,26 @@
import { NotificationParser, OnNotificationError } from '@/services/backend/notifications';
import { FlowyError, DocumentNotification } from '@/services/backend';
import { Result } from 'ts-results';
declare type DocumentNotificationCallback = (ty: DocumentNotification, payload: Result<Uint8Array, FlowyError>) => void;
export class DocumentNotificationParser extends NotificationParser<DocumentNotification> {
constructor(params: { id?: string; callback: DocumentNotificationCallback; onError?: OnNotificationError }) {
super(
params.callback,
(ty) => {
const notification = DocumentNotification[ty];
if (isDocumentNotification(notification)) {
return DocumentNotification[notification];
} else {
return DocumentNotification.Unknown;
}
},
params.id
);
}
}
const isDocumentNotification = (notification: string): notification is keyof typeof DocumentNotification => {
return Object.values(DocumentNotification).indexOf(notification) !== -1;
};

View File

@ -2,5 +2,6 @@ export * from "./models/flowy-user";
export * from "./models/flowy-document";
export * from "./models/flowy-database";
export * from "./models/flowy-folder2";
export * from "./models/flowy-document2";
export * from "./models/flowy-net";
export * from "./models/flowy-error";

File diff suppressed because it is too large Load Diff

View File

@ -11,6 +11,7 @@ members = [
# "flowy-folder",r
"flowy-folder2",
"flowy-notification",
"flowy-document2",
"flowy-document",
"flowy-error",
"flowy-revision",
@ -38,9 +39,12 @@ opt-level = 3
incremental = false
[patch.crates-io]
collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab" }
collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab" }
collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab" }
#collab = { path = "../AppFlowy-Collab/collab" }
#collab-folder = { path = "../AppFlowy-Collab/collab-folder" }
#collab-persistence = { path = "../AppFlowy-Collab/collab-persistence" }
collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", branch = "main" }
collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", branch = "main" }
collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", branch = "main" }
collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", branch = "main" }
# collab = { path = "../AppFlowy-Collab/collab" }
# collab-folder = { path = "../AppFlowy-Collab/collab-folder" }
# collab-persistence = { path = "../AppFlowy-Collab/collab-persistence" }
# collab-document = { path = "../AppFlowy-Collab/collab-document" }

View File

@ -18,6 +18,7 @@ user-model = { path = "../../../shared-lib/user-model" }
flowy-client-ws = { path = "../../../shared-lib/flowy-client-ws" }
flowy-sqlite = { path = "../flowy-sqlite", optional = true }
flowy-document = { path = "../flowy-document" }
flowy-document2 = { path = "../flowy-document2" }
flowy-revision = { path = "../flowy-revision" }
flowy-error = { path = "../flowy-error", features = ["adaptor_ws"] }
flowy-task = { path = "../flowy-task" }
@ -50,6 +51,7 @@ dart = [
"flowy-folder2/dart",
"flowy-database/dart",
"flowy-document/dart",
"flowy-document2/dart",
]
ts = [
"flowy-user/ts",
@ -58,6 +60,7 @@ ts = [
"flowy-folder2/ts",
"flowy-database/ts",
"flowy-document/ts",
"flowy-document2/ts",
]
rev-sqlite = [
"flowy-sqlite",

View File

@ -0,0 +1,40 @@
use std::sync::Arc;
use collab_persistence::CollabKV;
use flowy_database::manager::DatabaseManager;
use flowy_document2::manager::{DocumentManager as DocumentManager2, DocumentUser};
use flowy_error::FlowyError;
use flowy_user::services::UserSession;
pub struct Document2DepsResolver();
impl Document2DepsResolver {
pub fn resolve(
user_session: Arc<UserSession>,
database_manager: &Arc<DatabaseManager>,
) -> Arc<DocumentManager2> {
let user: Arc<dyn DocumentUser> = Arc::new(DocumentUserImpl(user_session.clone()));
Arc::new(DocumentManager2::new(user.clone()))
}
}
struct DocumentUserImpl(Arc<UserSession>);
impl DocumentUser for DocumentUserImpl {
fn user_id(&self) -> Result<i64, FlowyError> {
self
.0
.user_id()
.map_err(|e| FlowyError::internal().context(e))
}
fn token(&self) -> Result<String, FlowyError> {
self
.0
.token()
.map_err(|e| FlowyError::internal().context(e))
}
fn kv_db(&self) -> Result<Arc<CollabKV>, FlowyError> {
self.0.get_kv_db()
}
}

View File

@ -1,9 +1,11 @@
mod document2_deps;
mod document_deps;
mod folder2_deps;
mod grid_deps;
mod user_deps;
mod util;
pub use document2_deps::*;
pub use document_deps::*;
pub use folder2_deps::*;
pub use grid_deps::*;

View File

@ -7,6 +7,7 @@ use flowy_database::entities::DatabaseLayoutPB;
use flowy_database::manager::DatabaseManager;
use flowy_document::entities::DocumentVersionPB;
use flowy_document::{DocumentConfig, DocumentManager};
use flowy_document2::manager::DocumentManager as DocumentManager2;
use flowy_error::FlowyResult;
use flowy_folder::errors::FlowyError;
use flowy_folder2::manager::Folder2Manager;
@ -124,6 +125,7 @@ pub struct AppFlowyCore {
pub config: AppFlowyCoreConfig,
pub user_session: Arc<UserSession>,
pub document_manager: Arc<DocumentManager>,
pub document_manager2: Arc<DocumentManager2>,
pub folder_manager: Arc<Folder2Manager>,
pub database_manager: Arc<DatabaseManager>,
pub event_dispatcher: Arc<AFPluginDispatcher>,
@ -146,40 +148,50 @@ impl AppFlowyCore {
runtime.spawn(TaskRunner::run(task_dispatcher.clone()));
let (local_server, ws_conn) = mk_local_server(&config.server_config);
let (user_session, document_manager, folder_manager, local_server, database_manager) = runtime
.block_on(async {
let user_session = mk_user_session(&config, &local_server, &config.server_config);
let document_manager = DocumentDepsResolver::resolve(
local_server.clone(),
ws_conn.clone(),
user_session.clone(),
&config.server_config,
&config.document,
);
let (
user_session,
document_manager,
folder_manager,
local_server,
database_manager,
document_manager2,
) = runtime.block_on(async {
let user_session = mk_user_session(&config, &local_server, &config.server_config);
let document_manager = DocumentDepsResolver::resolve(
local_server.clone(),
ws_conn.clone(),
user_session.clone(),
&config.server_config,
&config.document,
);
let database_manager = DatabaseDepsResolver::resolve(
ws_conn.clone(),
user_session.clone(),
task_dispatcher.clone(),
)
.await;
let database_manager = DatabaseDepsResolver::resolve(
ws_conn.clone(),
user_session.clone(),
task_dispatcher.clone(),
)
.await;
let folder_manager =
Folder2DepsResolver::resolve(user_session.clone(), &document_manager, &database_manager)
.await;
let folder_manager =
Folder2DepsResolver::resolve(user_session.clone(), &document_manager, &database_manager)
.await;
if let Some(local_server) = local_server.as_ref() {
local_server.run();
}
ws_conn.init().await;
(
user_session,
document_manager,
folder_manager,
local_server,
database_manager,
)
});
let document_manager2 =
Document2DepsResolver::resolve(user_session.clone(), &database_manager);
if let Some(local_server) = local_server.as_ref() {
local_server.run();
}
ws_conn.init().await;
(
user_session,
document_manager,
folder_manager,
local_server,
database_manager,
document_manager2,
)
});
let user_status_listener = UserStatusListener {
document_manager: document_manager.clone(),
@ -203,6 +215,7 @@ impl AppFlowyCore {
&database_manager,
&user_session,
&document_manager,
&document_manager2,
)
}));
_start_listening(&event_dispatcher, &ws_conn, &folder_manager);
@ -211,6 +224,7 @@ impl AppFlowyCore {
config,
user_session,
document_manager,
document_manager2,
folder_manager,
database_manager,
event_dispatcher,

View File

@ -1,6 +1,7 @@
use flowy_client_ws::FlowyWebSocketConnect;
use flowy_database::manager::DatabaseManager;
use flowy_document::DocumentManager;
use flowy_document2::manager::DocumentManager as DocumentManager2;
use flowy_folder2::manager::Folder2Manager;
use flowy_user::services::UserSession;
@ -13,17 +14,20 @@ pub fn make_plugins(
grid_manager: &Arc<DatabaseManager>,
user_session: &Arc<UserSession>,
document_manager: &Arc<DocumentManager>,
document_manager2: &Arc<DocumentManager2>,
) -> Vec<AFPlugin> {
let user_plugin = flowy_user::event_map::init(user_session.clone());
let folder_plugin = flowy_folder2::event_map::init(folder_manager.clone());
let network_plugin = flowy_net::event_map::init(ws_conn.clone());
let grid_plugin = flowy_database::event_map::init(grid_manager.clone());
let document_plugin = flowy_document::event_map::init(document_manager.clone());
let document_plugin2 = flowy_document2::event_map::init(document_manager2.clone());
vec![
user_plugin,
folder_plugin,
network_plugin,
grid_plugin,
document_plugin,
document_plugin2,
]
}

View File

@ -0,0 +1,33 @@
[package]
name = "flowy-document2"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
collab = { version = "0.1.0" }
collab-persistence = { version = "0.1.0" }
collab-document = { version = "0.1.0" }
flowy-derive = { path = "../flowy-derive" }
flowy-notification = { path = "../flowy-notification" }
flowy-error = { path = "../flowy-error", features = ["adaptor_sync", "adaptor_ot", "adaptor_serde", "adaptor_database", "adaptor_dispatch"] }
lib-dispatch = { path = "../lib-dispatch" }
protobuf = {version = "2.28.0"}
bytes = { version = "1.4" }
nanoid = "0.4.0"
parking_lot = "0.12.1"
strum = "0.21"
strum_macros = "0.21"
serde = { version = "1.0", features = ["derive"] }
serde_json = {version = "1.0"}
[build-dependencies]
flowy-codegen = { path = "../flowy-codegen"}
[features]
dart = ["flowy-codegen/dart", "flowy-notification/dart"]
ts = ["flowy-codegen/ts", "flowy-notification/ts"]

View File

@ -0,0 +1,3 @@
# Check out the FlowyConfig (located in flowy_toml.rs) for more details.
proto_input = ["src/event_map.rs", "src/entities.rs", "src/notification.rs"]
event_files = ["src/event_map.rs"]

View File

@ -0,0 +1,10 @@
fn main() {
let crate_name = env!("CARGO_PKG_NAME");
flowy_codegen::protobuf_file::gen(crate_name);
#[cfg(feature = "dart")]
flowy_codegen::dart_event::gen(crate_name);
#[cfg(feature = "ts")]
flowy_codegen::ts_event::gen(crate_name);
}

View File

@ -0,0 +1,148 @@
use std::{
collections::HashMap,
ops::{Deref, DerefMut},
sync::Arc,
vec,
};
use collab::preclude::Collab;
use collab_document::{
blocks::{Block, DocumentData, DocumentMeta},
document::Document as InnerDocument,
};
use flowy_error::{ErrorCode, FlowyError, FlowyResult};
use nanoid::nanoid;
use parking_lot::Mutex;
use crate::entities::{BlockMapPB, BlockPB, ChildrenPB, DocumentDataPB2, MetaPB};
#[derive(Clone)]
pub struct Document(Arc<Mutex<InnerDocument>>);
impl Document {
pub fn new(collab: Collab, data: DocumentDataWrapper) -> FlowyResult<Self> {
let inner = InnerDocument::create(collab, data.0)
.map_err(|_| FlowyError::from(ErrorCode::DocumentDataInvalid))?;
Ok(Self(Arc::new(Mutex::new(inner))))
}
}
unsafe impl Sync for Document {}
unsafe impl Send for Document {}
impl Deref for Document {
type Target = Arc<Mutex<InnerDocument>>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for Document {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
#[derive(Clone)]
pub struct DocumentDataWrapper(pub DocumentData);
impl Deref for DocumentDataWrapper {
type Target = DocumentData;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for DocumentDataWrapper {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl From<DocumentDataWrapper> for DocumentDataPB2 {
fn from(data: DocumentDataWrapper) -> Self {
let blocks = data
.0
.blocks
.into_iter()
.map(|(id, block)| {
(
id,
BlockPB {
id: block.id,
ty: block.ty,
parent_id: block.parent,
children_id: block.children,
data: serde_json::to_string(&block.data).unwrap(),
},
)
})
.collect::<HashMap<String, BlockPB>>();
let children_map = data
.0
.meta
.children_map
.into_iter()
.map(|(id, children)| {
(
id,
ChildrenPB {
children: children.into_iter().collect(),
},
)
})
.collect::<HashMap<String, ChildrenPB>>();
Self {
page_id: data.0.page_id,
blocks: BlockMapPB { blocks },
meta: MetaPB { children_map },
}
}
}
impl Default for DocumentDataWrapper {
fn default() -> Self {
let mut blocks: HashMap<String, Block> = HashMap::new();
let mut meta: HashMap<String, Vec<String>> = HashMap::new();
// page block
let page_id = nanoid!(10);
let children_id = nanoid!(10);
let root = Block {
id: page_id.clone(),
ty: "page".to_string(),
parent: "".to_string(),
children: children_id.clone(),
external_id: None,
external_type: None,
data: HashMap::new(),
};
blocks.insert(page_id.clone(), root);
// text block
let text_block_id = nanoid!(10);
let text_0_children_id = nanoid!(10);
let text_block = Block {
id: text_block_id.clone(),
ty: "text".to_string(),
parent: page_id.clone(),
children: text_0_children_id.clone(),
external_id: None,
external_type: None,
data: HashMap::new(),
};
blocks.insert(text_block_id.clone(), text_block);
// meta
meta.insert(children_id, vec![text_block_id]);
meta.insert(text_0_children_id, vec![]);
Self(DocumentData {
page_id,
blocks,
meta: DocumentMeta { children_map: meta },
})
}
}

View File

@ -0,0 +1,110 @@
use std::collections::HashMap;
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
#[derive(Default, ProtoBuf)]
pub struct OpenDocumentPayloadPBV2 {
#[pb(index = 1)]
pub document_id: String,
// Support customize initial data
}
#[derive(Default, ProtoBuf)]
pub struct CloseDocumentPayloadPBV2 {
#[pb(index = 1)]
pub document_id: String,
// Support customize initial data
}
#[derive(Default, ProtoBuf)]
pub struct ApplyActionPayloadPBV2 {
#[pb(index = 1)]
pub document_id: String,
#[pb(index = 2)]
pub actions: Vec<BlockActionPB>,
}
#[derive(Default, ProtoBuf)]
pub struct DocumentDataPB2 {
#[pb(index = 1)]
pub page_id: String,
#[pb(index = 2)]
pub blocks: BlockMapPB,
#[pb(index = 3)]
pub meta: MetaPB,
}
#[derive(Default, ProtoBuf)]
pub struct BlockMapPB {
#[pb(index = 1)]
pub blocks: HashMap<String, BlockPB>,
}
#[derive(Default, ProtoBuf)]
pub struct BlockPB {
#[pb(index = 1)]
pub id: String,
#[pb(index = 2)]
pub ty: String,
#[pb(index = 3)]
pub data: String,
#[pb(index = 4)]
pub parent_id: String,
#[pb(index = 5)]
pub children_id: String,
}
#[derive(Default, ProtoBuf)]
pub struct MetaPB {
#[pb(index = 1)]
pub children_map: HashMap<String, ChildrenPB>,
}
#[derive(Default, ProtoBuf)]
pub struct ChildrenPB {
#[pb(index = 1)]
pub children: Vec<String>,
}
// Actions
#[derive(Default, ProtoBuf)]
pub struct BlockActionPB {
#[pb(index = 1)]
pub action: BlockActionTypePB,
#[pb(index = 2)]
pub payload: BlockActionPayloadPB,
}
#[derive(Default, ProtoBuf)]
pub struct BlockActionPayloadPB {
#[pb(index = 1)]
pub block: BlockPB,
#[pb(index = 2, one_of)]
pub prev_id: Option<String>,
#[pb(index = 3, one_of)]
pub parent_id: Option<String>,
}
#[derive(ProtoBuf_Enum)]
pub enum BlockActionTypePB {
Insert = 0,
Update = 1,
Delete = 2,
Move = 3,
}
impl Default for BlockActionTypePB {
fn default() -> Self {
Self::Insert
}
}

View File

@ -0,0 +1,98 @@
use std::{sync::Arc};
use crate::{
document::DocumentDataWrapper,
entities::{
ApplyActionPayloadPBV2, BlockActionPB, BlockActionPayloadPB, BlockActionTypePB,
BlockPB, CloseDocumentPayloadPBV2, DocumentDataPB2, OpenDocumentPayloadPBV2,
},
manager::DocumentManager,
};
use collab_document::blocks::{
json_str_to_hashmap, Block, BlockAction, BlockActionPayload, BlockActionType,
};
use flowy_error::{FlowyError, FlowyResult};
use lib_dispatch::prelude::{data_result_ok, AFPluginData, AFPluginState, DataResult};
pub(crate) async fn open_document_handler(
data: AFPluginData<OpenDocumentPayloadPBV2>,
manager: AFPluginState<Arc<DocumentManager>>,
) -> DataResult<DocumentDataPB2, FlowyError> {
let context = data.into_inner();
let document = manager.open_document(context.document_id)?;
let document_data = document
.lock()
.get_document()
.map_err(|err| FlowyError::internal().context(err))?;
data_result_ok(DocumentDataPB2::from(DocumentDataWrapper(document_data)))
}
pub(crate) async fn close_document_handler(
data: AFPluginData<CloseDocumentPayloadPBV2>,
manager: AFPluginState<Arc<DocumentManager>>,
) -> FlowyResult<()> {
let context = data.into_inner();
manager.close_document(context.document_id)?;
Ok(())
}
pub(crate) async fn apply_action_handler(
data: AFPluginData<ApplyActionPayloadPBV2>,
manager: AFPluginState<Arc<DocumentManager>>,
) -> FlowyResult<()> {
let context = data.into_inner();
let doc_id = context.document_id;
let actions = context
.actions
.into_iter()
.map(|action| action.into())
.collect();
let document = manager.open_document(doc_id)?;
document.lock().apply_action(actions);
Ok(())
}
impl From<BlockActionPB> for BlockAction {
fn from(pb: BlockActionPB) -> Self {
Self {
action: pb.action.into(),
payload: pb.payload.into(),
}
}
}
impl From<BlockActionTypePB> for BlockActionType {
fn from(pb: BlockActionTypePB) -> Self {
match pb {
BlockActionTypePB::Insert => Self::Insert,
BlockActionTypePB::Update => Self::Update,
BlockActionTypePB::Delete => Self::Delete,
BlockActionTypePB::Move => Self::Move,
}
}
}
impl From<BlockActionPayloadPB> for BlockActionPayload {
fn from(pb: BlockActionPayloadPB) -> Self {
Self {
block: pb.block.into(),
parent_id: pb.parent_id,
prev_id: pb.prev_id,
}
}
}
impl From<BlockPB> for Block {
fn from(pb: BlockPB) -> Self {
let data = json_str_to_hashmap(&pb.data).unwrap_or_default();
Self {
id: pb.id,
ty: pb.ty,
children: pb.children_id,
parent: pb.parent_id,
data,
external_id: None,
external_type: None,
}
}
}

View File

@ -0,0 +1,35 @@
use std::sync::Arc;
use strum_macros::Display;
use flowy_derive::{Flowy_Event, ProtoBuf_Enum};
use lib_dispatch::prelude::AFPlugin;
use crate::{
event_handler::{apply_action_handler, close_document_handler, open_document_handler},
manager::DocumentManager,
};
pub fn init(document_manager: Arc<DocumentManager>) -> AFPlugin {
let mut plugin = AFPlugin::new()
.name(env!("CARGO_PKG_NAME"))
.state(document_manager);
plugin = plugin.event(DocumentEvent2::OpenDocument, open_document_handler);
plugin = plugin.event(DocumentEvent2::CloseDocument, close_document_handler);
plugin = plugin.event(DocumentEvent2::ApplyAction, apply_action_handler);
plugin
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Display, ProtoBuf_Enum, Flowy_Event)]
#[event_err = "FlowyError"]
pub enum DocumentEvent2 {
#[event(input = "OpenDocumentPayloadPBV2", output = "DocumentDataPB2")]
OpenDocument = 0,
#[event(input = "CloseDocumentPayloadPBV2")]
CloseDocument = 1,
#[event(input = "ApplyActionPayloadPBV2")]
ApplyAction = 2,
}

View File

@ -0,0 +1,8 @@
pub mod entities;
pub mod event_map;
pub mod manager;
pub mod protobuf;
mod document;
mod event_handler;
mod notification;

View File

@ -0,0 +1,71 @@
use std::{collections::HashMap, sync::Arc};
use collab::{plugin_impl::disk::CollabDiskPlugin, preclude::CollabBuilder};
use collab_persistence::CollabKV;
use flowy_error::{FlowyError, FlowyResult};
use parking_lot::RwLock;
use crate::{
document::{Document, DocumentDataWrapper},
notification::{send_notification, DocumentNotification},
};
pub trait DocumentUser: Send + Sync {
fn user_id(&self) -> Result<i64, FlowyError>;
fn token(&self) -> Result<String, FlowyError>; // unused now.
fn kv_db(&self) -> Result<Arc<CollabKV>, FlowyError>;
}
pub struct DocumentManager {
documents: Arc<RwLock<HashMap<String, Arc<Document>>>>,
user: Arc<dyn DocumentUser>,
}
// unsafe impl Send for DocumentManager {}
// unsafe impl Sync for DocumentManager {}
impl DocumentManager {
pub fn new(user: Arc<dyn DocumentUser>) -> Self {
Self {
documents: Default::default(),
user,
}
}
pub fn open_document(&self, doc_id: String) -> FlowyResult<Arc<Document>> {
if let Some(doc) = self.documents.read().get(&doc_id) {
return Ok(doc.clone());
}
let collab = self.get_collab_for_doc_id(&doc_id)?;
let data = DocumentDataWrapper::default();
let document = Arc::new(Document::new(collab, data)?);
let clone_doc_id = doc_id.clone();
let _document_data = document
.lock()
.open(move |_, _| {
// TODO: add payload data.
send_notification(&clone_doc_id, DocumentNotification::DidReceiveUpdate).send();
})
.map_err(|err| FlowyError::internal().context(err))?;
self.documents.write().insert(doc_id, document.clone());
Ok(document)
}
pub fn close_document(&self, doc_id: String) -> FlowyResult<()> {
self.documents.write().remove(&doc_id);
Ok(())
}
fn get_collab_for_doc_id(&self, doc_id: &str) -> Result<collab::preclude::Collab, FlowyError> {
let uid = self.user.user_id()?;
let kv_db = self.user.kv_db()?;
let mut collab = CollabBuilder::new(uid, doc_id).build();
let disk_plugin = Arc::new(
CollabDiskPlugin::new(uid, kv_db).map_err(|err| FlowyError::internal().context(err))?,
);
collab.add_plugin(disk_plugin);
collab.initial();
Ok(collab)
}
}

View File

@ -0,0 +1,28 @@
use flowy_derive::ProtoBuf_Enum;
use flowy_notification::NotificationBuilder;
const OBSERVABLE_CATEGORY: &str = "Document";
#[derive(ProtoBuf_Enum, Debug)]
pub(crate) enum DocumentNotification {
Unknown = 0,
DidReceiveUpdate = 1,
}
impl std::default::Default for DocumentNotification {
fn default() -> Self {
DocumentNotification::Unknown
}
}
impl std::convert::From<DocumentNotification> for i32 {
fn from(notification: DocumentNotification) -> Self {
notification as i32
}
}
pub(crate) fn send_notification(id: &str, ty: DocumentNotification) -> NotificationBuilder {
NotificationBuilder::new(id, ty, OBSERVABLE_CATEGORY)
}

View File

@ -189,6 +189,9 @@ pub enum ErrorCode {
#[error("Only the date type can be used in calendar")]
UnexpectedCalendarFieldType = 61,
#[error("Document Data Invalid")]
DocumentDataInvalid = 62,
}
impl ErrorCode {

View File

@ -20,6 +20,7 @@ flowy-client-network-config= { path = "../../../shared-lib/flowy-client-network-
flowy-sync = { path = "../../../shared-lib/flowy-sync"}
user-model = { path = "../../../shared-lib/user-model"}
flowy-folder2 = { path = "../flowy-folder2" }
flowy-document2 = { path = "../flowy-document2" }
flowy-user = { path = "../flowy-user" }
flowy-document = { path = "../flowy-document" }
lazy_static = "1.4.0"