From fa2cfd7c208b748ad84c48c81a73b19ef8cafd7b Mon Sep 17 00:00:00 2001 From: appflowy <annie@appflowy.io> Date: Tue, 13 Sep 2022 20:23:56 +0800 Subject: [PATCH] chore: node transform path test --- .../lib/plugins/doc/application/doc_bloc.dart | 16 +- .../plugins/doc/application/doc_service.dart | 20 ++- .../flowy-net/src/http_server/document.rs | 10 +- .../flowy-net/src/local_server/server.rs | 10 +- .../flowy-sdk/src/deps_resolve/folder_deps.rs | 18 +-- .../src/deps_resolve/text_block_deps.rs | 12 +- frontend/rust-lib/flowy-sdk/src/lib.rs | 4 +- frontend/rust-lib/flowy-sdk/src/module.rs | 6 +- .../rust-lib/flowy-text-block/src/editor.rs | 7 +- .../rust-lib/flowy-text-block/src/entities.rs | 46 ++++++ .../flowy-text-block/src/event_handler.rs | 37 ++--- .../flowy-text-block/src/event_map.rs | 16 +- frontend/rust-lib/flowy-text-block/src/lib.rs | 8 +- .../rust-lib/flowy-text-block/src/manager.rs | 119 +++++++------- .../rust-lib/flowy-text-block/src/queue.rs | 6 +- .../flowy-text-block/tests/document/script.rs | 2 +- .../flowy-sync/src/entities/text_block.rs | 2 +- shared-lib/lib-ot/src/core/document/node.rs | 2 +- .../lib-ot/src/core/document/node_tree.rs | 4 +- .../lib-ot/src/core/document/operation.rs | 146 +++++++++++++----- .../lib-ot/src/core/document/transaction.rs | 23 ++- shared-lib/lib-ot/src/text_delta/delta.rs | 1 - shared-lib/lib-ot/tests/node/editor_test.rs | 7 +- .../lib-ot/tests/node/operation_test.rs | 63 +++++++- shared-lib/lib-ot/tests/node/script.rs | 99 ++++++++++-- shared-lib/lib-ot/tests/node/tree_test.rs | 58 ++++--- 26 files changed, 515 insertions(+), 227 deletions(-) diff --git a/frontend/app_flowy/lib/plugins/doc/application/doc_bloc.dart b/frontend/app_flowy/lib/plugins/doc/application/doc_bloc.dart index ed12227af0..89c87db455 100644 --- a/frontend/app_flowy/lib/plugins/doc/application/doc_bloc.dart +++ b/frontend/app_flowy/lib/plugins/doc/application/doc_bloc.dart @@ -90,7 +90,7 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> { final result = await service.openDocument(docId: view.id); result.fold( (block) { - document = _decodeJsonToDocument(block.deltaStr); + document = _decodeJsonToDocument(block.snapshot); _subscription = document.changes.listen((event) { final delta = event.item2; final documentDelta = document.toDelta(); @@ -115,16 +115,12 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> { void _composeDelta(Delta composedDelta, Delta documentDelta) async { final json = jsonEncode(composedDelta.toJson()); Log.debug("doc_id: $view.id - Send json: $json"); - final result = await service.composeDelta(docId: view.id, data: json); + final result = await service.applyEdit(docId: view.id, data: json); - result.fold((rustDoc) { - // final json = utf8.decode(doc.data); - final rustDelta = Delta.fromJson(jsonDecode(rustDoc.deltaStr)); - if (documentDelta != rustDelta) { - Log.error("Receive : $rustDelta"); - Log.error("Expected : $documentDelta"); - } - }, (r) => null); + result.fold( + (_) {}, + (r) => Log.error(r), + ); } Document _decodeJsonToDocument(String data) { diff --git a/frontend/app_flowy/lib/plugins/doc/application/doc_service.dart b/frontend/app_flowy/lib/plugins/doc/application/doc_service.dart index 659a99e371..067d51a510 100644 --- a/frontend/app_flowy/lib/plugins/doc/application/doc_service.dart +++ b/frontend/app_flowy/lib/plugins/doc/application/doc_service.dart @@ -4,22 +4,28 @@ import 'package:flowy_sdk/dispatch/dispatch.dart'; import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-sync/text_block.pb.dart'; +import 'package:flowy_sdk/protobuf/flowy-text-block/entities.pb.dart'; class DocumentService { - Future<Either<TextBlockDeltaPB, FlowyError>> openDocument({ + Future<Either<TextBlockPB, FlowyError>> openDocument({ required String docId, }) async { await FolderEventSetLatestView(ViewIdPB(value: docId)).send(); final payload = TextBlockIdPB(value: docId); - return TextBlockEventGetBlockData(payload).send(); + return TextBlockEventGetTextBlock(payload).send(); } - Future<Either<TextBlockDeltaPB, FlowyError>> composeDelta({required String docId, required String data}) { - final payload = TextBlockDeltaPB.create() - ..blockId = docId - ..deltaStr = data; - return TextBlockEventApplyDelta(payload).send(); + Future<Either<Unit, FlowyError>> applyEdit({ + required String docId, + required String data, + String operations = "", + }) { + final payload = EditPayloadPB.create() + ..textBlockId = docId + ..operations = operations + ..delta = data; + return TextBlockEventApplyEdit(payload).send(); } Future<Either<Unit, FlowyError>> closeDocument({required String docId}) { diff --git a/frontend/rust-lib/flowy-net/src/http_server/document.rs b/frontend/rust-lib/flowy-net/src/http_server/document.rs index ca9cb47955..b286e5c102 100644 --- a/frontend/rust-lib/flowy-net/src/http_server/document.rs +++ b/frontend/rust-lib/flowy-net/src/http_server/document.rs @@ -4,7 +4,7 @@ use crate::{ }; use flowy_error::FlowyError; use flowy_sync::entities::text_block::{CreateTextBlockParams, DocumentPB, ResetTextBlockParams, TextBlockIdPB}; -use flowy_text_block::BlockCloudService; +use flowy_text_block::TextEditorCloudService; use http_flowy::response::FlowyResponse; use lazy_static::lazy_static; use lib_infra::future::FutureResult; @@ -20,20 +20,20 @@ impl BlockHttpCloudService { } } -impl BlockCloudService for BlockHttpCloudService { - fn create_block(&self, token: &str, params: CreateTextBlockParams) -> FutureResult<(), FlowyError> { +impl TextEditorCloudService for BlockHttpCloudService { + fn create_text_block(&self, token: &str, params: CreateTextBlockParams) -> FutureResult<(), FlowyError> { let token = token.to_owned(); let url = self.config.doc_url(); FutureResult::new(async move { create_document_request(&token, params, &url).await }) } - fn read_block(&self, token: &str, params: TextBlockIdPB) -> FutureResult<Option<DocumentPB>, FlowyError> { + fn read_text_block(&self, token: &str, params: TextBlockIdPB) -> FutureResult<Option<DocumentPB>, FlowyError> { let token = token.to_owned(); let url = self.config.doc_url(); FutureResult::new(async move { read_document_request(&token, params, &url).await }) } - fn update_block(&self, token: &str, params: ResetTextBlockParams) -> FutureResult<(), FlowyError> { + fn update_text_block(&self, token: &str, params: ResetTextBlockParams) -> FutureResult<(), FlowyError> { let token = token.to_owned(); let url = self.config.doc_url(); FutureResult::new(async move { reset_doc_request(&token, params, &url).await }) diff --git a/frontend/rust-lib/flowy-net/src/local_server/server.rs b/frontend/rust-lib/flowy-net/src/local_server/server.rs index 387ff4e885..060abf08bf 100644 --- a/frontend/rust-lib/flowy-net/src/local_server/server.rs +++ b/frontend/rust-lib/flowy-net/src/local_server/server.rs @@ -261,7 +261,7 @@ use flowy_folder::entities::{ use flowy_folder_data_model::revision::{ gen_app_id, gen_workspace_id, AppRevision, TrashRevision, ViewRevision, WorkspaceRevision, }; -use flowy_text_block::BlockCloudService; +use flowy_text_block::TextEditorCloudService; use flowy_user::entities::{ SignInParams, SignInResponse, SignUpParams, SignUpResponse, UpdateUserProfileParams, UserProfilePB, }; @@ -414,12 +414,12 @@ impl UserCloudService for LocalServer { } } -impl BlockCloudService for LocalServer { - fn create_block(&self, _token: &str, _params: CreateTextBlockParams) -> FutureResult<(), FlowyError> { +impl TextEditorCloudService for LocalServer { + fn create_text_block(&self, _token: &str, _params: CreateTextBlockParams) -> FutureResult<(), FlowyError> { FutureResult::new(async { Ok(()) }) } - fn read_block(&self, _token: &str, params: TextBlockIdPB) -> FutureResult<Option<DocumentPB>, FlowyError> { + fn read_text_block(&self, _token: &str, params: TextBlockIdPB) -> FutureResult<Option<DocumentPB>, FlowyError> { let doc = DocumentPB { block_id: params.value, text: initial_quill_delta_string(), @@ -429,7 +429,7 @@ impl BlockCloudService for LocalServer { FutureResult::new(async { Ok(Some(doc)) }) } - fn update_block(&self, _token: &str, _params: ResetTextBlockParams) -> FutureResult<(), FlowyError> { + fn update_text_block(&self, _token: &str, _params: ResetTextBlockParams) -> FutureResult<(), FlowyError> { FutureResult::new(async { Ok(()) }) } } diff --git a/frontend/rust-lib/flowy-sdk/src/deps_resolve/folder_deps.rs b/frontend/rust-lib/flowy-sdk/src/deps_resolve/folder_deps.rs index 0dc51c1a09..79b5468a56 100644 --- a/frontend/rust-lib/flowy-sdk/src/deps_resolve/folder_deps.rs +++ b/frontend/rust-lib/flowy-sdk/src/deps_resolve/folder_deps.rs @@ -18,7 +18,7 @@ use flowy_revision::{RevisionWebSocket, WSStateReceiver}; use flowy_sync::client_document::default::initial_quill_delta_string; use flowy_sync::entities::revision::{RepeatedRevision, Revision}; use flowy_sync::entities::ws_data::ClientRevisionWSData; -use flowy_text_block::TextBlockManager; +use flowy_text_block::TextEditorManager; use flowy_user::services::UserSession; use futures_core::future::BoxFuture; use lib_infra::future::{BoxResultFuture, FutureResult}; @@ -34,7 +34,7 @@ impl FolderDepsResolver { user_session: Arc<UserSession>, server_config: &ClientServerConfiguration, ws_conn: &Arc<FlowyWebSocketConnect>, - text_block_manager: &Arc<TextBlockManager>, + text_block_manager: &Arc<TextEditorManager>, grid_manager: &Arc<GridManager>, ) -> Arc<FolderManager> { let user: Arc<dyn WorkspaceUser> = Arc::new(WorkspaceUserImpl(user_session.clone())); @@ -63,7 +63,7 @@ impl FolderDepsResolver { } fn make_view_data_processor( - text_block_manager: Arc<TextBlockManager>, + text_block_manager: Arc<TextEditorManager>, grid_manager: Arc<GridManager>, ) -> ViewDataProcessorMap { let mut map: HashMap<ViewDataTypePB, Arc<dyn ViewDataProcessor + Send + Sync>> = HashMap::new(); @@ -135,7 +135,7 @@ impl WSMessageReceiver for FolderWSMessageReceiverImpl { } } -struct TextBlockViewDataProcessor(Arc<TextBlockManager>); +struct TextBlockViewDataProcessor(Arc<TextEditorManager>); impl ViewDataProcessor for TextBlockViewDataProcessor { fn initialize(&self) -> FutureResult<(), FlowyError> { let manager = self.0.clone(); @@ -147,7 +147,7 @@ impl ViewDataProcessor for TextBlockViewDataProcessor { let view_id = view_id.to_string(); let manager = self.0.clone(); FutureResult::new(async move { - let _ = manager.create_block(view_id, repeated_revision).await?; + let _ = manager.create_text_block(view_id, repeated_revision).await?; Ok(()) }) } @@ -156,7 +156,7 @@ impl ViewDataProcessor for TextBlockViewDataProcessor { let manager = self.0.clone(); let view_id = view_id.to_string(); FutureResult::new(async move { - let _ = manager.delete_block(view_id)?; + let _ = manager.close_text_editor(view_id)?; Ok(()) }) } @@ -165,7 +165,7 @@ impl ViewDataProcessor for TextBlockViewDataProcessor { let manager = self.0.clone(); let view_id = view_id.to_string(); FutureResult::new(async move { - let _ = manager.close_block(view_id)?; + let _ = manager.close_text_editor(view_id)?; Ok(()) }) } @@ -174,7 +174,7 @@ impl ViewDataProcessor for TextBlockViewDataProcessor { let view_id = view_id.to_string(); let manager = self.0.clone(); FutureResult::new(async move { - let editor = manager.open_block(view_id).await?; + let editor = manager.open_text_editor(view_id).await?; let delta_bytes = Bytes::from(editor.delta_str().await?); Ok(delta_bytes) }) @@ -195,7 +195,7 @@ impl ViewDataProcessor for TextBlockViewDataProcessor { let delta_data = Bytes::from(view_data); let repeated_revision: RepeatedRevision = Revision::initial_revision(&user_id, &view_id, delta_data.clone()).into(); - let _ = manager.create_block(view_id, repeated_revision).await?; + let _ = manager.create_text_block(view_id, repeated_revision).await?; Ok(delta_data) }) } diff --git a/frontend/rust-lib/flowy-sdk/src/deps_resolve/text_block_deps.rs b/frontend/rust-lib/flowy-sdk/src/deps_resolve/text_block_deps.rs index 7b80e637ab..9fa5667bb1 100644 --- a/frontend/rust-lib/flowy-sdk/src/deps_resolve/text_block_deps.rs +++ b/frontend/rust-lib/flowy-sdk/src/deps_resolve/text_block_deps.rs @@ -8,7 +8,7 @@ use flowy_revision::{RevisionWebSocket, WSStateReceiver}; use flowy_sync::entities::ws_data::ClientRevisionWSData; use flowy_text_block::{ errors::{internal_error, FlowyError}, - BlockCloudService, TextBlockManager, TextBlockUser, + TextEditorCloudService, TextEditorManager, TextEditorUser, }; use flowy_user::services::UserSession; use futures_core::future::BoxFuture; @@ -23,15 +23,15 @@ impl TextBlockDepsResolver { ws_conn: Arc<FlowyWebSocketConnect>, user_session: Arc<UserSession>, server_config: &ClientServerConfiguration, - ) -> Arc<TextBlockManager> { + ) -> Arc<TextEditorManager> { let user = Arc::new(BlockUserImpl(user_session)); let rev_web_socket = Arc::new(TextBlockWebSocket(ws_conn.clone())); - let cloud_service: Arc<dyn BlockCloudService> = match local_server { + let cloud_service: Arc<dyn TextEditorCloudService> = match local_server { None => Arc::new(BlockHttpCloudService::new(server_config.clone())), Some(local_server) => local_server, }; - let manager = Arc::new(TextBlockManager::new(cloud_service, user, rev_web_socket)); + let manager = Arc::new(TextEditorManager::new(cloud_service, user, rev_web_socket)); let receiver = Arc::new(DocumentWSMessageReceiverImpl(manager.clone())); ws_conn.add_ws_message_receiver(receiver).unwrap(); @@ -40,7 +40,7 @@ impl TextBlockDepsResolver { } struct BlockUserImpl(Arc<UserSession>); -impl TextBlockUser for BlockUserImpl { +impl TextEditorUser for BlockUserImpl { fn user_dir(&self) -> Result<String, FlowyError> { let dir = self.0.user_dir().map_err(|e| FlowyError::unauthorized().context(e))?; @@ -90,7 +90,7 @@ impl RevisionWebSocket for TextBlockWebSocket { } } -struct DocumentWSMessageReceiverImpl(Arc<TextBlockManager>); +struct DocumentWSMessageReceiverImpl(Arc<TextEditorManager>); impl WSMessageReceiver for DocumentWSMessageReceiverImpl { fn source(&self) -> WSChannel { WSChannel::Document diff --git a/frontend/rust-lib/flowy-sdk/src/lib.rs b/frontend/rust-lib/flowy-sdk/src/lib.rs index e0bd601987..e780f9f937 100644 --- a/frontend/rust-lib/flowy-sdk/src/lib.rs +++ b/frontend/rust-lib/flowy-sdk/src/lib.rs @@ -11,7 +11,7 @@ use flowy_net::{ local_server::LocalServer, ws::connection::{listen_on_websocket, FlowyWebSocketConnect}, }; -use flowy_text_block::TextBlockManager; +use flowy_text_block::TextEditorManager; use flowy_user::services::{notifier::UserStatus, UserSession, UserSessionConfig}; use lib_dispatch::prelude::*; use lib_dispatch::runtime::tokio_default_runtime; @@ -89,7 +89,7 @@ pub struct FlowySDK { #[allow(dead_code)] config: FlowySDKConfig, pub user_session: Arc<UserSession>, - pub text_block_manager: Arc<TextBlockManager>, + pub text_block_manager: Arc<TextEditorManager>, pub folder_manager: Arc<FolderManager>, pub grid_manager: Arc<GridManager>, pub dispatcher: Arc<EventDispatcher>, diff --git a/frontend/rust-lib/flowy-sdk/src/module.rs b/frontend/rust-lib/flowy-sdk/src/module.rs index cbb1673a17..2f6a20d6c6 100644 --- a/frontend/rust-lib/flowy-sdk/src/module.rs +++ b/frontend/rust-lib/flowy-sdk/src/module.rs @@ -1,7 +1,7 @@ use flowy_folder::manager::FolderManager; use flowy_grid::manager::GridManager; use flowy_net::ws::connection::FlowyWebSocketConnect; -use flowy_text_block::TextBlockManager; +use flowy_text_block::TextEditorManager; use flowy_user::services::UserSession; use lib_dispatch::prelude::Module; use std::sync::Arc; @@ -11,7 +11,7 @@ pub fn mk_modules( folder_manager: &Arc<FolderManager>, grid_manager: &Arc<GridManager>, user_session: &Arc<UserSession>, - text_block_manager: &Arc<TextBlockManager>, + text_block_manager: &Arc<TextEditorManager>, ) -> Vec<Module> { let user_module = mk_user_module(user_session.clone()); let folder_module = mk_folder_module(folder_manager.clone()); @@ -43,6 +43,6 @@ fn mk_grid_module(grid_manager: Arc<GridManager>) -> Module { flowy_grid::event_map::create(grid_manager) } -fn mk_text_block_module(text_block_manager: Arc<TextBlockManager>) -> Module { +fn mk_text_block_module(text_block_manager: Arc<TextEditorManager>) -> Module { flowy_text_block::event_map::create(text_block_manager) } diff --git a/frontend/rust-lib/flowy-text-block/src/editor.rs b/frontend/rust-lib/flowy-text-block/src/editor.rs index 8f75a67d88..8ca41f3f5a 100644 --- a/frontend/rust-lib/flowy-text-block/src/editor.rs +++ b/frontend/rust-lib/flowy-text-block/src/editor.rs @@ -2,7 +2,7 @@ use crate::web_socket::EditorCommandSender; use crate::{ errors::FlowyError, queue::{EditBlockQueue, EditorCommand}, - TextBlockUser, + TextEditorUser, }; use bytes::Bytes; use flowy_error::{internal_error, FlowyResult}; @@ -24,7 +24,6 @@ use tokio::sync::{mpsc, oneshot}; pub struct TextBlockEditor { pub doc_id: String, - #[allow(dead_code)] rev_manager: Arc<RevisionManager>, #[cfg(feature = "sync")] ws_manager: Arc<flowy_revision::RevisionWebSocketManager>, @@ -35,7 +34,7 @@ impl TextBlockEditor { #[allow(unused_variables)] pub(crate) async fn new( doc_id: &str, - user: Arc<dyn TextBlockUser>, + user: Arc<dyn TextEditorUser>, mut rev_manager: RevisionManager, rev_web_socket: Arc<dyn RevisionWebSocket>, cloud_service: Arc<dyn RevisionCloudService>, @@ -194,7 +193,7 @@ impl std::ops::Drop for TextBlockEditor { // The edit queue will exit after the EditorCommandSender was dropped. fn spawn_edit_queue( - user: Arc<dyn TextBlockUser>, + user: Arc<dyn TextEditorUser>, rev_manager: Arc<RevisionManager>, delta: TextDelta, ) -> EditorCommandSender { diff --git a/frontend/rust-lib/flowy-text-block/src/entities.rs b/frontend/rust-lib/flowy-text-block/src/entities.rs index ec8767285b..d7cc1d8665 100644 --- a/frontend/rust-lib/flowy-text-block/src/entities.rs +++ b/frontend/rust-lib/flowy-text-block/src/entities.rs @@ -29,6 +29,52 @@ impl std::convert::From<i32> for ExportType { } } +#[derive(Default, ProtoBuf)] +pub struct EditPayloadPB { + #[pb(index = 1)] + pub text_block_id: String, + + // Encode in JSON format + #[pb(index = 2)] + pub operations: String, + + // Encode in JSON format + #[pb(index = 3)] + pub delta: String, +} + +#[derive(Default)] +pub struct EditParams { + pub text_block_id: String, + + // Encode in JSON format + pub operations: String, + + // Encode in JSON format + pub delta: String, +} + +impl TryInto<EditParams> for EditPayloadPB { + type Error = ErrorCode; + fn try_into(self) -> Result<EditParams, Self::Error> { + Ok(EditParams { + text_block_id: self.text_block_id, + operations: self.operations, + delta: self.delta, + }) + } +} + +#[derive(Default, ProtoBuf)] +pub struct TextBlockPB { + #[pb(index = 1)] + pub text_block_id: String, + + /// Encode in JSON format + #[pb(index = 2)] + pub snapshot: String, +} + #[derive(Default, ProtoBuf)] pub struct ExportPayloadPB { #[pb(index = 1)] diff --git a/frontend/rust-lib/flowy-text-block/src/event_handler.rs b/frontend/rust-lib/flowy-text-block/src/event_handler.rs index dc9812d862..d7844ad12d 100644 --- a/frontend/rust-lib/flowy-text-block/src/event_handler.rs +++ b/frontend/rust-lib/flowy-text-block/src/event_handler.rs @@ -1,39 +1,40 @@ -use crate::entities::{ExportDataPB, ExportParams, ExportPayloadPB}; -use crate::TextBlockManager; +use crate::entities::{EditParams, EditPayloadPB, ExportDataPB, ExportParams, ExportPayloadPB, TextBlockPB}; +use crate::TextEditorManager; use flowy_error::FlowyError; use flowy_sync::entities::text_block::{TextBlockDeltaPB, TextBlockIdPB}; use lib_dispatch::prelude::{data_result, AppData, Data, DataResult}; use std::convert::TryInto; use std::sync::Arc; -pub(crate) async fn get_block_data_handler( +pub(crate) async fn get_text_block_handler( data: Data<TextBlockIdPB>, - manager: AppData<Arc<TextBlockManager>>, -) -> DataResult<TextBlockDeltaPB, FlowyError> { - let block_id: TextBlockIdPB = data.into_inner(); - let editor = manager.open_block(&block_id).await?; + manager: AppData<Arc<TextEditorManager>>, +) -> DataResult<TextBlockPB, FlowyError> { + let text_block_id: TextBlockIdPB = data.into_inner(); + let editor = manager.open_text_editor(&text_block_id).await?; let delta_str = editor.delta_str().await?; - data_result(TextBlockDeltaPB { - block_id: block_id.into(), - delta_str, + data_result(TextBlockPB { + text_block_id: text_block_id.into(), + snapshot: delta_str, }) } -pub(crate) async fn apply_delta_handler( - data: Data<TextBlockDeltaPB>, - manager: AppData<Arc<TextBlockManager>>, -) -> DataResult<TextBlockDeltaPB, FlowyError> { - let block_delta = manager.receive_local_delta(data.into_inner()).await?; - data_result(block_delta) +pub(crate) async fn apply_edit_handler( + data: Data<EditPayloadPB>, + manager: AppData<Arc<TextEditorManager>>, +) -> Result<(), FlowyError> { + let params: EditParams = data.into_inner().try_into()?; + let _ = manager.apply_edit(params).await?; + Ok(()) } #[tracing::instrument(level = "debug", skip(data, manager), err)] pub(crate) async fn export_handler( data: Data<ExportPayloadPB>, - manager: AppData<Arc<TextBlockManager>>, + manager: AppData<Arc<TextEditorManager>>, ) -> DataResult<ExportDataPB, FlowyError> { let params: ExportParams = data.into_inner().try_into()?; - let editor = manager.open_block(¶ms.view_id).await?; + let editor = manager.open_text_editor(¶ms.view_id).await?; let delta_json = editor.delta_str().await?; data_result(ExportDataPB { data: delta_json, diff --git a/frontend/rust-lib/flowy-text-block/src/event_map.rs b/frontend/rust-lib/flowy-text-block/src/event_map.rs index cfc06bf32c..d66ee490ee 100644 --- a/frontend/rust-lib/flowy-text-block/src/event_map.rs +++ b/frontend/rust-lib/flowy-text-block/src/event_map.rs @@ -1,16 +1,16 @@ use crate::event_handler::*; -use crate::TextBlockManager; +use crate::TextEditorManager; use flowy_derive::{Flowy_Event, ProtoBuf_Enum}; use lib_dispatch::prelude::Module; use std::sync::Arc; use strum_macros::Display; -pub fn create(block_manager: Arc<TextBlockManager>) -> Module { +pub fn create(block_manager: Arc<TextEditorManager>) -> Module { let mut module = Module::new().name(env!("CARGO_PKG_NAME")).data(block_manager); module = module - .event(TextBlockEvent::GetBlockData, get_block_data_handler) - .event(TextBlockEvent::ApplyDelta, apply_delta_handler) + .event(TextBlockEvent::GetTextBlock, get_text_block_handler) + .event(TextBlockEvent::ApplyEdit, apply_edit_handler) .event(TextBlockEvent::ExportDocument, export_handler); module @@ -19,11 +19,11 @@ pub fn create(block_manager: Arc<TextBlockManager>) -> Module { #[derive(Clone, Copy, PartialEq, Eq, Debug, Display, Hash, ProtoBuf_Enum, Flowy_Event)] #[event_err = "FlowyError"] pub enum TextBlockEvent { - #[event(input = "TextBlockIdPB", output = "TextBlockDeltaPB")] - GetBlockData = 0, + #[event(input = "TextBlockIdPB", output = "TextBlockPB")] + GetTextBlock = 0, - #[event(input = "TextBlockDeltaPB", output = "TextBlockDeltaPB")] - ApplyDelta = 1, + #[event(input = "EditPayloadPB")] + ApplyEdit = 1, #[event(input = "ExportPayloadPB", output = "ExportDataPB")] ExportDocument = 2, diff --git a/frontend/rust-lib/flowy-text-block/src/lib.rs b/frontend/rust-lib/flowy-text-block/src/lib.rs index 37ddf6ea1e..b3f9839fde 100644 --- a/frontend/rust-lib/flowy-text-block/src/lib.rs +++ b/frontend/rust-lib/flowy-text-block/src/lib.rs @@ -18,10 +18,10 @@ use crate::errors::FlowyError; use flowy_sync::entities::text_block::{CreateTextBlockParams, DocumentPB, ResetTextBlockParams, TextBlockIdPB}; use lib_infra::future::FutureResult; -pub trait BlockCloudService: Send + Sync { - fn create_block(&self, token: &str, params: CreateTextBlockParams) -> FutureResult<(), FlowyError>; +pub trait TextEditorCloudService: Send + Sync { + fn create_text_block(&self, token: &str, params: CreateTextBlockParams) -> FutureResult<(), FlowyError>; - fn read_block(&self, token: &str, params: TextBlockIdPB) -> FutureResult<Option<DocumentPB>, FlowyError>; + fn read_text_block(&self, token: &str, params: TextBlockIdPB) -> FutureResult<Option<DocumentPB>, FlowyError>; - fn update_block(&self, token: &str, params: ResetTextBlockParams) -> FutureResult<(), FlowyError>; + fn update_text_block(&self, token: &str, params: ResetTextBlockParams) -> FutureResult<(), FlowyError>; } diff --git a/frontend/rust-lib/flowy-text-block/src/manager.rs b/frontend/rust-lib/flowy-text-block/src/manager.rs index 57e875668e..424f7a8d50 100644 --- a/frontend/rust-lib/flowy-text-block/src/manager.rs +++ b/frontend/rust-lib/flowy-text-block/src/manager.rs @@ -1,5 +1,6 @@ +use crate::entities::{EditParams, EditPayloadPB}; use crate::queue::TextBlockRevisionCompactor; -use crate::{editor::TextBlockEditor, errors::FlowyError, BlockCloudService}; +use crate::{editor::TextBlockEditor, errors::FlowyError, TextEditorCloudService}; use bytes::Bytes; use dashmap::DashMap; use flowy_database::ConnectionPool; @@ -16,30 +17,30 @@ use flowy_sync::entities::{ use lib_infra::future::FutureResult; use std::{convert::TryInto, sync::Arc}; -pub trait TextBlockUser: Send + Sync { +pub trait TextEditorUser: Send + Sync { fn user_dir(&self) -> Result<String, FlowyError>; fn user_id(&self) -> Result<String, FlowyError>; fn token(&self) -> Result<String, FlowyError>; fn db_pool(&self) -> Result<Arc<ConnectionPool>, FlowyError>; } -pub struct TextBlockManager { - cloud_service: Arc<dyn BlockCloudService>, +pub struct TextEditorManager { + cloud_service: Arc<dyn TextEditorCloudService>, rev_web_socket: Arc<dyn RevisionWebSocket>, - editor_map: Arc<TextBlockEditorMap>, - user: Arc<dyn TextBlockUser>, + editor_map: Arc<TextEditorMap>, + user: Arc<dyn TextEditorUser>, } -impl TextBlockManager { +impl TextEditorManager { pub fn new( - cloud_service: Arc<dyn BlockCloudService>, - text_block_user: Arc<dyn TextBlockUser>, + cloud_service: Arc<dyn TextEditorCloudService>, + text_block_user: Arc<dyn TextEditorUser>, rev_web_socket: Arc<dyn RevisionWebSocket>, ) -> Self { Self { cloud_service, rev_web_socket, - editor_map: Arc::new(TextBlockEditorMap::new()), + editor_map: Arc::new(TextEditorMap::new()), user: text_block_user, } } @@ -50,45 +51,47 @@ impl TextBlockManager { Ok(()) } - #[tracing::instrument(level = "trace", skip(self, block_id), fields(block_id), err)] - pub async fn open_block<T: AsRef<str>>(&self, block_id: T) -> Result<Arc<TextBlockEditor>, FlowyError> { - let block_id = block_id.as_ref(); - tracing::Span::current().record("block_id", &block_id); - self.get_block_editor(block_id).await + #[tracing::instrument(level = "trace", skip(self, editor_id), fields(editor_id), err)] + pub async fn open_text_editor<T: AsRef<str>>(&self, editor_id: T) -> Result<Arc<TextBlockEditor>, FlowyError> { + let editor_id = editor_id.as_ref(); + tracing::Span::current().record("editor_id", &editor_id); + self.get_text_editor(editor_id).await } - #[tracing::instrument(level = "trace", skip(self, block_id), fields(block_id), err)] - pub fn close_block<T: AsRef<str>>(&self, block_id: T) -> Result<(), FlowyError> { - let block_id = block_id.as_ref(); - tracing::Span::current().record("block_id", &block_id); - self.editor_map.remove(block_id); + #[tracing::instrument(level = "trace", skip(self, editor_id), fields(editor_id), err)] + pub fn close_text_editor<T: AsRef<str>>(&self, editor_id: T) -> Result<(), FlowyError> { + let editor_id = editor_id.as_ref(); + tracing::Span::current().record("editor_id", &editor_id); + self.editor_map.remove(editor_id); Ok(()) } - #[tracing::instrument(level = "debug", skip(self, doc_id), fields(doc_id), err)] - pub fn delete_block<T: AsRef<str>>(&self, doc_id: T) -> Result<(), FlowyError> { - let doc_id = doc_id.as_ref(); - tracing::Span::current().record("doc_id", &doc_id); - self.editor_map.remove(doc_id); - Ok(()) - } - - #[tracing::instrument(level = "debug", skip(self, delta), fields(doc_id = %delta.block_id), err)] + #[tracing::instrument(level = "debug", skip(self, delta), err)] pub async fn receive_local_delta(&self, delta: TextBlockDeltaPB) -> Result<TextBlockDeltaPB, FlowyError> { - let editor = self.get_block_editor(&delta.block_id).await?; + let editor = self.get_text_editor(&delta.text_block_id).await?; let _ = editor.compose_local_delta(Bytes::from(delta.delta_str)).await?; - let document_json = editor.delta_str().await?; + let delta_str = editor.delta_str().await?; Ok(TextBlockDeltaPB { - block_id: delta.block_id.clone(), - delta_str: document_json, + text_block_id: delta.text_block_id.clone(), + delta_str, }) } - pub async fn create_block<T: AsRef<str>>(&self, doc_id: T, revisions: RepeatedRevision) -> FlowyResult<()> { - let doc_id = doc_id.as_ref().to_owned(); + pub async fn apply_edit(&self, params: EditParams) -> FlowyResult<()> { + let editor = self.get_text_editor(¶ms.text_block_id).await?; + let _ = editor.compose_local_delta(Bytes::from(params.delta)).await?; + Ok(()) + } + + pub async fn create_text_block<T: AsRef<str>>( + &self, + text_block_id: T, + revisions: RepeatedRevision, + ) -> FlowyResult<()> { + let doc_id = text_block_id.as_ref().to_owned(); let db_pool = self.user.db_pool()?; // Maybe we could save the block to disk without creating the RevisionManager - let rev_manager = self.make_rev_manager(&doc_id, db_pool)?; + let rev_manager = self.make_text_block_rev_manager(&doc_id, db_pool)?; let _ = rev_manager.reset_object(revisions).await?; Ok(()) } @@ -110,26 +113,26 @@ impl TextBlockManager { } } -impl TextBlockManager { - async fn get_block_editor(&self, block_id: &str) -> FlowyResult<Arc<TextBlockEditor>> { +impl TextEditorManager { + async fn get_text_editor(&self, block_id: &str) -> FlowyResult<Arc<TextBlockEditor>> { match self.editor_map.get(block_id) { None => { let db_pool = self.user.db_pool()?; - self.make_text_block_editor(block_id, db_pool).await + self.make_text_editor(block_id, db_pool).await } Some(editor) => Ok(editor), } } #[tracing::instrument(level = "trace", skip(self, pool), err)] - async fn make_text_block_editor( + async fn make_text_editor( &self, block_id: &str, pool: Arc<ConnectionPool>, ) -> Result<Arc<TextBlockEditor>, FlowyError> { let user = self.user.clone(); let token = self.user.token()?; - let rev_manager = self.make_rev_manager(block_id, pool.clone())?; + let rev_manager = self.make_text_block_rev_manager(block_id, pool.clone())?; let cloud_service = Arc::new(TextBlockRevisionCloudService { token, server: self.cloud_service.clone(), @@ -140,7 +143,11 @@ impl TextBlockManager { Ok(doc_editor) } - fn make_rev_manager(&self, doc_id: &str, pool: Arc<ConnectionPool>) -> Result<RevisionManager, FlowyError> { + fn make_text_block_rev_manager( + &self, + doc_id: &str, + pool: Arc<ConnectionPool>, + ) -> Result<RevisionManager, FlowyError> { let user_id = self.user.user_id()?; let disk_cache = SQLiteTextBlockRevisionPersistence::new(&user_id, pool.clone()); let rev_persistence = RevisionPersistence::new(&user_id, doc_id, disk_cache); @@ -161,7 +168,7 @@ impl TextBlockManager { struct TextBlockRevisionCloudService { token: String, - server: Arc<dyn BlockCloudService>, + server: Arc<dyn TextEditorCloudService>, } impl RevisionCloudService for TextBlockRevisionCloudService { @@ -173,7 +180,7 @@ impl RevisionCloudService for TextBlockRevisionCloudService { let user_id = user_id.to_string(); FutureResult::new(async move { - match server.read_block(&token, params).await? { + match server.read_text_block(&token, params).await? { None => Err(FlowyError::record_not_found().context("Remote doesn't have this document")), Some(doc) => { let delta_data = Bytes::from(doc.text.clone()); @@ -193,36 +200,36 @@ impl RevisionCloudService for TextBlockRevisionCloudService { } } -pub struct TextBlockEditorMap { +pub struct TextEditorMap { inner: DashMap<String, Arc<TextBlockEditor>>, } -impl TextBlockEditorMap { +impl TextEditorMap { fn new() -> Self { Self { inner: DashMap::new() } } - pub(crate) fn insert(&self, block_id: &str, doc: &Arc<TextBlockEditor>) { - if self.inner.contains_key(block_id) { - log::warn!("Doc:{} already exists in cache", block_id); + pub(crate) fn insert(&self, editor_id: &str, doc: &Arc<TextBlockEditor>) { + if self.inner.contains_key(editor_id) { + log::warn!("Doc:{} already exists in cache", editor_id); } - self.inner.insert(block_id.to_string(), doc.clone()); + self.inner.insert(editor_id.to_string(), doc.clone()); } - pub(crate) fn get(&self, block_id: &str) -> Option<Arc<TextBlockEditor>> { - Some(self.inner.get(block_id)?.clone()) + pub(crate) fn get(&self, editor_id: &str) -> Option<Arc<TextBlockEditor>> { + Some(self.inner.get(editor_id)?.clone()) } - pub(crate) fn remove(&self, block_id: &str) { - if let Some(editor) = self.get(block_id) { + pub(crate) fn remove(&self, editor_id: &str) { + if let Some(editor) = self.get(editor_id) { editor.stop() } - self.inner.remove(block_id); + self.inner.remove(editor_id); } } #[tracing::instrument(level = "trace", skip(web_socket, handlers))] -fn listen_ws_state_changed(web_socket: Arc<dyn RevisionWebSocket>, handlers: Arc<TextBlockEditorMap>) { +fn listen_ws_state_changed(web_socket: Arc<dyn RevisionWebSocket>, handlers: Arc<TextEditorMap>) { tokio::spawn(async move { let mut notify = web_socket.subscribe_state_changed().await; while let Ok(state) = notify.recv().await { diff --git a/frontend/rust-lib/flowy-text-block/src/queue.rs b/frontend/rust-lib/flowy-text-block/src/queue.rs index d81461d25e..510205c1ba 100644 --- a/frontend/rust-lib/flowy-text-block/src/queue.rs +++ b/frontend/rust-lib/flowy-text-block/src/queue.rs @@ -1,5 +1,5 @@ use crate::web_socket::EditorCommandReceiver; -use crate::TextBlockUser; +use crate::TextEditorUser; use async_stream::stream; use bytes::Bytes; use flowy_error::{FlowyError, FlowyResult}; @@ -23,14 +23,14 @@ use tokio::sync::{oneshot, RwLock}; // serial. pub(crate) struct EditBlockQueue { document: Arc<RwLock<ClientDocument>>, - user: Arc<dyn TextBlockUser>, + user: Arc<dyn TextEditorUser>, rev_manager: Arc<RevisionManager>, receiver: Option<EditorCommandReceiver>, } impl EditBlockQueue { pub(crate) fn new( - user: Arc<dyn TextBlockUser>, + user: Arc<dyn TextEditorUser>, rev_manager: Arc<RevisionManager>, delta: TextDelta, receiver: EditorCommandReceiver, diff --git a/frontend/rust-lib/flowy-text-block/tests/document/script.rs b/frontend/rust-lib/flowy-text-block/tests/document/script.rs index 254b94958e..ae3209020a 100644 --- a/frontend/rust-lib/flowy-text-block/tests/document/script.rs +++ b/frontend/rust-lib/flowy-text-block/tests/document/script.rs @@ -27,7 +27,7 @@ impl TextBlockEditorTest { let sdk = FlowySDKTest::default(); let _ = sdk.init_user().await; let test = ViewTest::new_text_block_view(&sdk).await; - let editor = sdk.text_block_manager.open_block(&test.view.id).await.unwrap(); + let editor = sdk.text_block_manager.open_text_editor(&test.view.id).await.unwrap(); Self { sdk, editor } } diff --git a/shared-lib/flowy-sync/src/entities/text_block.rs b/shared-lib/flowy-sync/src/entities/text_block.rs index d65ec683ee..cca7222bd0 100644 --- a/shared-lib/flowy-sync/src/entities/text_block.rs +++ b/shared-lib/flowy-sync/src/entities/text_block.rs @@ -69,7 +69,7 @@ pub struct ResetTextBlockParams { #[derive(ProtoBuf, Default, Debug, Clone)] pub struct TextBlockDeltaPB { #[pb(index = 1)] - pub block_id: String, + pub text_block_id: String, #[pb(index = 2)] pub delta_str: String, diff --git a/shared-lib/lib-ot/src/core/document/node.rs b/shared-lib/lib-ot/src/core/document/node.rs index 18651a6062..908cfa3f18 100644 --- a/shared-lib/lib-ot/src/core/document/node.rs +++ b/shared-lib/lib-ot/src/core/document/node.rs @@ -6,7 +6,7 @@ use crate::errors::OTError; use crate::text_delta::TextDelta; use serde::{Deserialize, Serialize}; -#[derive(Default, Clone, Serialize, Deserialize, Eq, PartialEq)] +#[derive(Default, Debug, Clone, Serialize, Deserialize, Eq, PartialEq)] pub struct NodeData { #[serde(rename = "type")] pub node_type: String, diff --git a/shared-lib/lib-ot/src/core/document/node_tree.rs b/shared-lib/lib-ot/src/core/document/node_tree.rs index 7f88224f24..87e8e427f3 100644 --- a/shared-lib/lib-ot/src/core/document/node_tree.rs +++ b/shared-lib/lib-ot/src/core/document/node_tree.rs @@ -216,7 +216,9 @@ impl NodeTree { return Ok(()); } - if index == parent.children(&self.arena).count() { + /// Append the node to the end of the children list if index greater or equal to the + /// length of the children. + if index >= parent.children(&self.arena).count() { self.append_nodes(&parent, nodes); return Ok(()); } diff --git a/shared-lib/lib-ot/src/core/document/operation.rs b/shared-lib/lib-ot/src/core/document/operation.rs index f9e772e5d0..9117a0aeb7 100644 --- a/shared-lib/lib-ot/src/core/document/operation.rs +++ b/shared-lib/lib-ot/src/core/document/operation.rs @@ -1,10 +1,10 @@ use crate::core::attributes::Attributes; use crate::core::document::path::Path; -use crate::core::{NodeBodyChangeset, NodeData}; +use crate::core::{NodeBodyChangeset, NodeData, OperationTransform}; use crate::errors::OTError; use serde::{Deserialize, Serialize}; -#[derive(Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "op")] pub enum NodeOperation { #[serde(rename = "insert")] @@ -27,8 +27,37 @@ pub enum NodeOperation { Delete { path: Path, nodes: Vec<NodeData> }, } +// impl OperationTransform for NodeOperation { +// fn compose(&self, other: &Self) -> Result<Self, OTError> +// where +// Self: Sized, +// { +// match self { +// NodeOperation::Insert { path, nodes } => { +// let new_path = Path::transform(path, other.path(), nodes.len() as i64); +// Ok((self.clone(), other.clone_with_new_path(new_path))) +// } +// NodeOperation::Delete { path, nodes } => { +// let new_path = Path::transform(path, other.path(), nodes.len() as i64); +// other.clone_with_new_path(new_path) +// } +// _ => other.clone(), +// } +// } +// +// fn transform(&self, other: &Self) -> Result<(Self, Self), OTError> +// where +// Self: Sized, +// { +// todo!() +// } +// +// fn invert(&self, other: &Self) -> Self { +// todo!() +// } +// } impl NodeOperation { - pub fn path(&self) -> &Path { + pub fn get_path(&self) -> &Path { match self { NodeOperation::Insert { path, .. } => path, NodeOperation::UpdateAttributes { path, .. } => path, @@ -36,6 +65,19 @@ impl NodeOperation { NodeOperation::UpdateBody { path, .. } => path, } } + + pub fn mut_path<F>(&mut self, f: F) + where + F: FnOnce(&mut Path), + { + match self { + NodeOperation::Insert { path, .. } => f(path), + NodeOperation::UpdateAttributes { path, .. } => f(path), + NodeOperation::Delete { path, .. } => f(path), + NodeOperation::UpdateBody { path, .. } => f(path), + } + } + pub fn invert(&self) -> NodeOperation { match self { NodeOperation::Insert { path, nodes } => NodeOperation::Delete { @@ -61,47 +103,73 @@ impl NodeOperation { }, } } - pub fn clone_with_new_path(&self, path: Path) -> NodeOperation { + + /// Transform the `other` operation into a new operation that carries the changes made by + /// the current operation. + /// + /// # Arguments + /// + /// * `other`: The operation that is going to be transformed + /// + /// returns: NodeOperation + /// + /// # Examples + /// + /// ``` + /// use lib_ot::core::{NodeDataBuilder, NodeOperation, Path}; + /// let node_1 = NodeDataBuilder::new("text_1").build(); + /// let node_2 = NodeDataBuilder::new("text_2").build(); + /// + /// let op_1 = NodeOperation::Insert { + /// path: Path(vec![0, 1]), + /// nodes: vec![node_1], + /// }; + /// + /// let mut op_2 = NodeOperation::Insert { + /// path: Path(vec![0, 1]), + /// nodes: vec![node_2], + /// }; + /// + /// assert_eq!(serde_json::to_string(&op_2).unwrap(), r#"{"op":"insert","path":[0,1], + /// "nodes":[{"type":"text_2"}]}"#); + /// + /// let new_op = op_1.transform(&op_2); + /// assert_eq!(serde_json::to_string(&new_op).unwrap(), r#"{"op":"insert","path":[0,2], + /// "nodes":[{"type":"text_2"}]}"#); + /// + /// ``` + pub fn transform(&self, other: &NodeOperation) -> NodeOperation { + let mut other = other.clone(); match self { - NodeOperation::Insert { nodes, .. } => NodeOperation::Insert { - path, - nodes: nodes.clone(), - }, - NodeOperation::UpdateAttributes { - attributes, - old_attributes, - .. - } => NodeOperation::UpdateAttributes { - path, - attributes: attributes.clone(), - old_attributes: old_attributes.clone(), - }, - NodeOperation::Delete { nodes, .. } => NodeOperation::Delete { - path, - nodes: nodes.clone(), - }, - NodeOperation::UpdateBody { path, changeset } => NodeOperation::UpdateBody { - path: path.clone(), - changeset: changeset.clone(), - }, - } - } - pub fn transform(a: &NodeOperation, b: &NodeOperation) -> NodeOperation { - match a { - NodeOperation::Insert { path: a_path, nodes } => { - let new_path = Path::transform(a_path, b.path(), nodes.len() as i64); - b.clone_with_new_path(new_path) + NodeOperation::Insert { path, nodes } => { + let new_path = Path::transform(path, other.get_path(), nodes.len() as i64); + other.mut_path(|path| *path = new_path); } NodeOperation::Delete { path: a_path, nodes } => { - let new_path = Path::transform(a_path, b.path(), nodes.len() as i64); - b.clone_with_new_path(new_path) + let new_path = Path::transform(a_path, other.get_path(), nodes.len() as i64); + other.mut_path(|path| *path = new_path); } - _ => b.clone(), + _ => {} + } + other + } + + pub fn mut_transform(&self, other: &mut NodeOperation) { + match self { + NodeOperation::Insert { path, nodes } => { + let new_path = Path::transform(path, other.get_path(), nodes.len() as i64); + other.mut_path(|path| *path = new_path); + } + NodeOperation::Delete { path: a_path, nodes } => { + let new_path = Path::transform(a_path, other.get_path(), nodes.len() as i64); + other.mut_path(|path| *path = new_path); + } + _ => {} } } } -#[derive(Serialize, Deserialize, Default)] +#[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct NodeOperationList { operations: Vec<NodeOperation>, } @@ -126,6 +194,12 @@ impl std::ops::DerefMut for NodeOperationList { } } +impl std::convert::From<Vec<NodeOperation>> for NodeOperationList { + fn from(operations: Vec<NodeOperation>) -> Self { + Self { operations } + } +} + impl NodeOperationList { pub fn new(operations: Vec<NodeOperation>) -> Self { Self { operations } diff --git a/shared-lib/lib-ot/src/core/document/transaction.rs b/shared-lib/lib-ot/src/core/document/transaction.rs index 9b48ddd92d..20e58fe1ec 100644 --- a/shared-lib/lib-ot/src/core/document/transaction.rs +++ b/shared-lib/lib-ot/src/core/document/transaction.rs @@ -5,18 +5,35 @@ use indextree::NodeId; use super::{NodeBodyChangeset, NodeOperationList}; +#[derive(Debug, Clone)] pub struct Transaction { operations: NodeOperationList, } impl Transaction { - pub fn new(operations: NodeOperationList) -> Transaction { - Transaction { operations } + pub fn new() -> Self { + Transaction { + operations: vec![].into(), + } + } + + pub fn from_operations<T: Into<NodeOperationList>>(operations: T) -> Self { + Self { + operations: operations.into(), + } } pub fn into_operations(self) -> Vec<NodeOperation> { self.operations.into_inner() } + + pub fn transform(&self, other: &mut Transaction) { + for other_operation in other.iter_mut() { + for operation in self.operations.iter() { + operation.mut_transform(other_operation); + } + } + } } impl std::ops::Deref for Transaction { @@ -177,6 +194,6 @@ impl<'a> TransactionBuilder<'a> { } pub fn finalize(self) -> Transaction { - Transaction::new(self.operations) + Transaction::from_operations(self.operations) } } diff --git a/shared-lib/lib-ot/src/text_delta/delta.rs b/shared-lib/lib-ot/src/text_delta/delta.rs index c559f9cb0f..e8eb3c3fd8 100644 --- a/shared-lib/lib-ot/src/text_delta/delta.rs +++ b/shared-lib/lib-ot/src/text_delta/delta.rs @@ -2,5 +2,4 @@ use crate::core::{Attributes, Operation, OperationBuilder, Operations}; pub type TextDelta = Operations<Attributes>; pub type TextDeltaBuilder = OperationBuilder<Attributes>; - pub type TextOperation = Operation<Attributes>; diff --git a/shared-lib/lib-ot/tests/node/editor_test.rs b/shared-lib/lib-ot/tests/node/editor_test.rs index 6136fe5efd..abd294ecc0 100644 --- a/shared-lib/lib-ot/tests/node/editor_test.rs +++ b/shared-lib/lib-ot/tests/node/editor_test.rs @@ -26,7 +26,8 @@ fn editor_deserialize_node_test() { test.run_scripts(vec![ InsertNode { path, - node: node.clone(), + node_data: node.clone(), + rev_id: 1, }, AssertNumberOfNodesAtPath { path: None, len: 1 }, AssertNumberOfNodesAtPath { @@ -41,11 +42,11 @@ fn editor_deserialize_node_test() { path: vec![0, 1].into(), expected: expected_delta, }, - AssertNode { + AssertNodeData { path: vec![0, 0].into(), expected: Some(node.children[0].clone()), }, - AssertNode { + AssertNodeData { path: vec![0, 3].into(), expected: Some(node.children[3].clone()), }, diff --git a/shared-lib/lib-ot/tests/node/operation_test.rs b/shared-lib/lib-ot/tests/node/operation_test.rs index 6a7f8bb25a..d60b035c1c 100644 --- a/shared-lib/lib-ot/tests/node/operation_test.rs +++ b/shared-lib/lib-ot/tests/node/operation_test.rs @@ -1,4 +1,6 @@ -use lib_ot::core::AttributeBuilder; +use crate::node::script::NodeScript::*; +use crate::node::script::NodeTest; +use lib_ot::core::{AttributeBuilder, Node, NodeTree, Transaction, TransactionBuilder}; use lib_ot::{ core::{NodeBodyChangeset, NodeData, NodeDataBuilder, NodeOperation, Path}, text_delta::TextDeltaBuilder, @@ -69,3 +71,62 @@ fn operation_update_node_body_deserialize_test() { let json_2 = serde_json::to_string(&operation).unwrap(); assert_eq!(json_1, json_2); } + +#[test] +fn operation_insert_transform_test() { + let node_1 = NodeDataBuilder::new("text_1").build(); + let node_2 = NodeDataBuilder::new("text_2").build(); + let op_1 = NodeOperation::Insert { + path: Path(vec![0, 1]), + nodes: vec![node_1], + }; + + let mut insert_2 = NodeOperation::Insert { + path: Path(vec![0, 1]), + nodes: vec![node_2], + }; + + // let mut node_tree = NodeTree::new("root"); + // node_tree.apply_op(insert_1.clone()).unwrap(); + + let new_op = op_1.transform(&insert_2); + let json = serde_json::to_string(&new_op).unwrap(); + assert_eq!(json, r#"{"op":"insert","path":[0,2],"nodes":[{"type":"text_2"}]}"#); +} + +#[test] +fn operation_insert_transform_test2() { + let mut test = NodeTest::new(); + let node_data_1 = NodeDataBuilder::new("text_1").build(); + let node_data_2 = NodeDataBuilder::new("text_2").build(); + let node_2: Node = node_data_2.clone().into(); + let node_data_3 = NodeDataBuilder::new("text_3").build(); + let node_3: Node = node_data_3.clone().into(); + + let scripts = vec![ + InsertNode { + path: 0.into(), + node_data: node_data_1.clone(), + rev_id: 1, + }, + InsertNode { + path: 1.into(), + node_data: node_data_2.clone(), + rev_id: 2, + }, + InsertNode { + path: 1.into(), + node_data: node_data_3.clone(), + rev_id: 1, + }, + // AssertNode { + // path: 2.into(), + // expected: node_2, + // }, + AssertNode { + path: 1.into(), + expected: node_3, + }, + ]; + test.run_scripts(scripts); +} diff --git a/shared-lib/lib-ot/tests/node/script.rs b/shared-lib/lib-ot/tests/node/script.rs index 2aac683de8..01d1ad16db 100644 --- a/shared-lib/lib-ot/tests/node/script.rs +++ b/shared-lib/lib-ot/tests/node/script.rs @@ -1,26 +1,61 @@ +use lib_ot::core::{Node, NodeOperation, Transaction}; use lib_ot::{ core::attributes::Attributes, core::{NodeBody, NodeBodyChangeset, NodeData, NodeTree, Path, TransactionBuilder}, text_delta::TextDelta, }; +use std::collections::HashMap; pub enum NodeScript { - InsertNode { path: Path, node: NodeData }, - UpdateAttributes { path: Path, attributes: Attributes }, - UpdateBody { path: Path, changeset: NodeBodyChangeset }, - DeleteNode { path: Path }, - AssertNumberOfNodesAtPath { path: Option<Path>, len: usize }, - AssertNode { path: Path, expected: Option<NodeData> }, - AssertNodeDelta { path: Path, expected: TextDelta }, + InsertNode { + path: Path, + node_data: NodeData, + rev_id: usize, + }, + UpdateAttributes { + path: Path, + attributes: Attributes, + }, + UpdateBody { + path: Path, + changeset: NodeBodyChangeset, + }, + DeleteNode { + path: Path, + }, + AssertNumberOfNodesAtPath { + path: Option<Path>, + len: usize, + }, + AssertNodeData { + path: Path, + expected: Option<NodeData>, + }, + AssertNode { + path: Path, + expected: Node, + }, + AssertNodeDelta { + path: Path, + expected: TextDelta, + }, + ApplyTransaction { + transaction: Transaction, + rev_id: usize, + }, } pub struct NodeTest { + rev_id: usize, + rev_operations: HashMap<usize, Transaction>, node_tree: NodeTree, } impl NodeTest { pub fn new() -> Self { Self { + rev_id: 0, + rev_operations: HashMap::new(), node_tree: NodeTree::new("root"), } } @@ -33,40 +68,49 @@ impl NodeTest { pub fn run_script(&mut self, script: NodeScript) { match script { - NodeScript::InsertNode { path, node } => { - let transaction = TransactionBuilder::new(&self.node_tree) + NodeScript::InsertNode { + path, + node_data: node, + rev_id, + } => { + let mut transaction = TransactionBuilder::new(&self.node_tree) .insert_node_at_path(path, node) .finalize(); - - self.node_tree.apply(transaction).unwrap(); + self.transform_transaction_if_need(&mut transaction, rev_id); + self.apply_transaction(transaction); } NodeScript::UpdateAttributes { path, attributes } => { let transaction = TransactionBuilder::new(&self.node_tree) .update_attributes_at_path(&path, attributes) .finalize(); - self.node_tree.apply(transaction).unwrap(); + self.apply_transaction(transaction); } NodeScript::UpdateBody { path, changeset } => { // let transaction = TransactionBuilder::new(&self.node_tree) .update_body_at_path(&path, changeset) .finalize(); - self.node_tree.apply(transaction).unwrap(); + self.apply_transaction(transaction); } NodeScript::DeleteNode { path } => { let transaction = TransactionBuilder::new(&self.node_tree) .delete_node_at_path(&path) .finalize(); - self.node_tree.apply(transaction).unwrap(); + self.apply_transaction(transaction); } NodeScript::AssertNode { path, expected } => { + let node_id = self.node_tree.node_id_at_path(path).unwrap(); + let node = self.node_tree.get_node(node_id).cloned().unwrap(); + assert_eq!(node, expected); + } + NodeScript::AssertNodeData { path, expected } => { let node_id = self.node_tree.node_id_at_path(path); match node_id { None => assert!(node_id.is_none()), Some(node_id) => { - let node_data = self.node_tree.get_node(node_id).cloned(); - assert_eq!(node_data, expected.map(|e| e.into())); + let node = self.node_tree.get_node(node_id).cloned(); + assert_eq!(node, expected.map(|e| e.into())); } } } @@ -92,6 +136,29 @@ impl NodeTest { panic!("Node body type not match, expect Delta"); } } + + NodeScript::ApplyTransaction { + mut transaction, + rev_id, + } => { + self.transform_transaction_if_need(&mut transaction, rev_id); + self.apply_transaction(transaction); + } + } + } + + fn apply_transaction(&mut self, transaction: Transaction) { + self.rev_id += 1; + self.rev_operations.insert(self.rev_id, transaction.clone()); + self.node_tree.apply(transaction).unwrap(); + } + + fn transform_transaction_if_need(&mut self, transaction: &mut Transaction, rev_id: usize) { + if self.rev_id >= rev_id { + for rev_id in rev_id..self.rev_id { + let old_transaction = self.rev_operations.get(&rev_id).unwrap(); + old_transaction.transform(transaction); + } } } } diff --git a/shared-lib/lib-ot/tests/node/tree_test.rs b/shared-lib/lib-ot/tests/node/tree_test.rs index 95c05ea25a..39357b158c 100644 --- a/shared-lib/lib-ot/tests/node/tree_test.rs +++ b/shared-lib/lib-ot/tests/node/tree_test.rs @@ -14,9 +14,10 @@ fn node_insert_test() { let scripts = vec![ InsertNode { path: path.clone(), - node: inserted_node.clone(), + node_data: inserted_node.clone(), + rev_id: 1, }, - AssertNode { + AssertNodeData { path, expected: Some(inserted_node), }, @@ -32,9 +33,10 @@ fn node_insert_node_with_children_test() { let scripts = vec![ InsertNode { path: path.clone(), - node: inserted_node.clone(), + node_data: inserted_node.clone(), + rev_id: 1, }, - AssertNode { + AssertNodeData { path, expected: Some(inserted_node), }, @@ -57,25 +59,28 @@ fn node_insert_multi_nodes_test() { let scripts = vec![ InsertNode { path: path_1.clone(), - node: node_1.clone(), + node_data: node_1.clone(), + rev_id: 1, }, InsertNode { path: path_2.clone(), - node: node_2.clone(), + node_data: node_2.clone(), + rev_id: 2, }, InsertNode { path: path_3.clone(), - node: node_3.clone(), + node_data: node_3.clone(), + rev_id: 3, }, - AssertNode { + AssertNodeData { path: path_1, expected: Some(node_1), }, - AssertNode { + AssertNodeData { path: path_2, expected: Some(node_2), }, - AssertNode { + AssertNodeData { path: path_3, expected: Some(node_3), }, @@ -101,35 +106,39 @@ fn node_insert_node_in_ordered_nodes_test() { let scripts = vec![ InsertNode { path: path_1.clone(), - node: node_1.clone(), + node_data: node_1.clone(), + rev_id: 1, }, InsertNode { path: path_2.clone(), - node: node_2_1.clone(), + node_data: node_2_1.clone(), + rev_id: 2, }, InsertNode { path: path_3.clone(), - node: node_3.clone(), + node_data: node_3.clone(), + rev_id: 3, }, // 0:note_1 , 1: note_2_1, 2: note_3 InsertNode { path: path_2.clone(), - node: node_2_2.clone(), + node_data: node_2_2.clone(), + rev_id: 4, }, // 0:note_1 , 1:note_2_2, 2: note_2_1, 3: note_3 - AssertNode { + AssertNodeData { path: path_1, expected: Some(node_1), }, - AssertNode { + AssertNodeData { path: path_2, expected: Some(node_2_2), }, - AssertNode { + AssertNodeData { path: path_3, expected: Some(node_2_1), }, - AssertNode { + AssertNodeData { path: path_4, expected: Some(node_3), }, @@ -149,13 +158,14 @@ fn node_insert_with_attributes_test() { let scripts = vec![ InsertNode { path: path.clone(), - node: inserted_node.clone(), + node_data: inserted_node.clone(), + rev_id: 1, }, UpdateAttributes { path: path.clone(), attributes: inserted_node.attributes.clone(), }, - AssertNode { + AssertNodeData { path, expected: Some(inserted_node), }, @@ -172,10 +182,11 @@ fn node_delete_test() { let scripts = vec![ InsertNode { path: path.clone(), - node: inserted_node, + node_data: inserted_node, + rev_id: 1, }, DeleteNode { path: path.clone() }, - AssertNode { path, expected: None }, + AssertNodeData { path, expected: None }, ]; test.run_scripts(scripts); } @@ -198,7 +209,8 @@ fn node_update_body_test() { let scripts = vec![ InsertNode { path: path.clone(), - node, + node_data: node, + rev_id: 1, }, UpdateBody { path: path.clone(),