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 { 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 { 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> openDocument({ + Future> 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> composeDelta({required String docId, required String data}) { - final payload = TextBlockDeltaPB.create() - ..blockId = docId - ..deltaStr = data; - return TextBlockEventApplyDelta(payload).send(); + Future> applyEdit({ + required String docId, + required String data, + String operations = "", + }) { + final payload = EditPayloadPB.create() + ..textBlockId = docId + ..operations = operations + ..delta = data; + return TextBlockEventApplyEdit(payload).send(); } Future> 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, FlowyError> { + fn read_text_block(&self, token: &str, params: TextBlockIdPB) -> FutureResult, 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, FlowyError> { + fn read_text_block(&self, _token: &str, params: TextBlockIdPB) -> FutureResult, 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, server_config: &ClientServerConfiguration, ws_conn: &Arc, - text_block_manager: &Arc, + text_block_manager: &Arc, grid_manager: &Arc, ) -> Arc { let user: Arc = Arc::new(WorkspaceUserImpl(user_session.clone())); @@ -63,7 +63,7 @@ impl FolderDepsResolver { } fn make_view_data_processor( - text_block_manager: Arc, + text_block_manager: Arc, grid_manager: Arc, ) -> ViewDataProcessorMap { let mut map: HashMap> = HashMap::new(); @@ -135,7 +135,7 @@ impl WSMessageReceiver for FolderWSMessageReceiverImpl { } } -struct TextBlockViewDataProcessor(Arc); +struct TextBlockViewDataProcessor(Arc); 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, user_session: Arc, server_config: &ClientServerConfiguration, - ) -> Arc { + ) -> Arc { let user = Arc::new(BlockUserImpl(user_session)); let rev_web_socket = Arc::new(TextBlockWebSocket(ws_conn.clone())); - let cloud_service: Arc = match local_server { + let cloud_service: Arc = 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); -impl TextBlockUser for BlockUserImpl { +impl TextEditorUser for BlockUserImpl { fn user_dir(&self) -> Result { let dir = self.0.user_dir().map_err(|e| FlowyError::unauthorized().context(e))?; @@ -90,7 +90,7 @@ impl RevisionWebSocket for TextBlockWebSocket { } } -struct DocumentWSMessageReceiverImpl(Arc); +struct DocumentWSMessageReceiverImpl(Arc); 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, - pub text_block_manager: Arc, + pub text_block_manager: Arc, pub folder_manager: Arc, pub grid_manager: Arc, pub dispatcher: Arc, 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, grid_manager: &Arc, user_session: &Arc, - text_block_manager: &Arc, + text_block_manager: &Arc, ) -> Vec { 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) -> Module { flowy_grid::event_map::create(grid_manager) } -fn mk_text_block_module(text_block_manager: Arc) -> Module { +fn mk_text_block_module(text_block_manager: Arc) -> 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, #[cfg(feature = "sync")] ws_manager: Arc, @@ -35,7 +34,7 @@ impl TextBlockEditor { #[allow(unused_variables)] pub(crate) async fn new( doc_id: &str, - user: Arc, + user: Arc, mut rev_manager: RevisionManager, rev_web_socket: Arc, cloud_service: Arc, @@ -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, + user: Arc, rev_manager: Arc, 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 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 for EditPayloadPB { + type Error = ErrorCode; + fn try_into(self) -> Result { + 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..98ca7e19fe 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 flowy_sync::entities::text_block::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, - manager: AppData>, -) -> DataResult { - let block_id: TextBlockIdPB = data.into_inner(); - let editor = manager.open_block(&block_id).await?; + manager: AppData>, +) -> DataResult { + 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, - manager: AppData>, -) -> DataResult { - let block_delta = manager.receive_local_delta(data.into_inner()).await?; - data_result(block_delta) +pub(crate) async fn apply_edit_handler( + data: Data, + manager: AppData>, +) -> 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, - manager: AppData>, + manager: AppData>, ) -> DataResult { 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) -> Module { +pub fn create(block_manager: Arc) -> 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) -> 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, FlowyError>; + fn read_text_block(&self, token: &str, params: TextBlockIdPB) -> FutureResult, 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..2e10873523 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; 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; fn user_id(&self) -> Result; fn token(&self) -> Result; fn db_pool(&self) -> Result, FlowyError>; } -pub struct TextBlockManager { - cloud_service: Arc, +pub struct TextEditorManager { + cloud_service: Arc, rev_web_socket: Arc, - editor_map: Arc, - user: Arc, + editor_map: Arc, + user: Arc, } -impl TextBlockManager { +impl TextEditorManager { pub fn new( - cloud_service: Arc, - text_block_user: Arc, + cloud_service: Arc, + text_block_user: Arc, rev_web_socket: Arc, ) -> 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>(&self, block_id: T) -> Result, 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>(&self, editor_id: T) -> Result, 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>(&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>(&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>(&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 { - 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>(&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>( + &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> { +impl TextEditorManager { + async fn get_text_editor(&self, block_id: &str) -> FlowyResult> { 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, ) -> Result, 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) -> Result { + fn make_text_block_rev_manager( + &self, + doc_id: &str, + pool: Arc, + ) -> Result { 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, + server: Arc, } 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>, } -impl TextBlockEditorMap { +impl TextEditorMap { fn new() -> Self { Self { inner: DashMap::new() } } - pub(crate) fn insert(&self, block_id: &str, doc: &Arc) { - 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) { + 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> { - Some(self.inner.get(block_id)?.clone()) + pub(crate) fn get(&self, editor_id: &str) -> Option> { + 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, handlers: Arc) { +fn listen_ws_state_changed(web_socket: Arc, handlers: Arc) { 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>, - user: Arc, + user: Arc, rev_manager: Arc, receiver: Option, } impl EditBlockQueue { pub(crate) fn new( - user: Arc, + user: Arc, rev_manager: Arc, 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/Cargo.toml b/shared-lib/lib-ot/Cargo.toml index 3d0bd5aadd..81b75507a0 100644 --- a/shared-lib/lib-ot/Cargo.toml +++ b/shared-lib/lib-ot/Cargo.toml @@ -7,7 +7,7 @@ edition = "2018" [dependencies] bytecount = "0.6.0" -serde = { version = "1.0", features = ["derive"] } +serde = { version = "1.0", features = ["derive", "rc"] } #protobuf = {version = "2.18.0"} #flowy-derive = { path = "../flowy-derive" } tokio = { version = "1", features = ["sync"] } diff --git a/shared-lib/lib-ot/src/core/document/operation.rs b/shared-lib/lib-ot/src/core/document/operation.rs deleted file mode 100644 index f9e772e5d0..0000000000 --- a/shared-lib/lib-ot/src/core/document/operation.rs +++ /dev/null @@ -1,143 +0,0 @@ -use crate::core::attributes::Attributes; -use crate::core::document::path::Path; -use crate::core::{NodeBodyChangeset, NodeData}; -use crate::errors::OTError; -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Serialize, Deserialize)] -#[serde(tag = "op")] -pub enum NodeOperation { - #[serde(rename = "insert")] - Insert { path: Path, nodes: Vec }, - - #[serde(rename = "update")] - UpdateAttributes { - path: Path, - attributes: Attributes, - #[serde(rename = "oldAttributes")] - old_attributes: Attributes, - }, - - #[serde(rename = "update-body")] - // #[serde(serialize_with = "serialize_edit_body")] - // #[serde(deserialize_with = "deserialize_edit_body")] - UpdateBody { path: Path, changeset: NodeBodyChangeset }, - - #[serde(rename = "delete")] - Delete { path: Path, nodes: Vec }, -} - -impl NodeOperation { - pub fn path(&self) -> &Path { - match self { - NodeOperation::Insert { path, .. } => path, - NodeOperation::UpdateAttributes { path, .. } => path, - NodeOperation::Delete { path, .. } => path, - NodeOperation::UpdateBody { path, .. } => path, - } - } - pub fn invert(&self) -> NodeOperation { - match self { - NodeOperation::Insert { path, nodes } => NodeOperation::Delete { - path: path.clone(), - nodes: nodes.clone(), - }, - NodeOperation::UpdateAttributes { - path, - attributes, - old_attributes, - } => NodeOperation::UpdateAttributes { - path: path.clone(), - attributes: old_attributes.clone(), - old_attributes: attributes.clone(), - }, - NodeOperation::Delete { path, nodes } => NodeOperation::Insert { - path: path.clone(), - nodes: nodes.clone(), - }, - NodeOperation::UpdateBody { path, changeset: body } => NodeOperation::UpdateBody { - path: path.clone(), - changeset: body.inverted(), - }, - } - } - pub fn clone_with_new_path(&self, path: Path) -> NodeOperation { - 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::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) - } - _ => b.clone(), - } - } -} - -#[derive(Serialize, Deserialize, Default)] -pub struct NodeOperationList { - operations: Vec, -} - -impl NodeOperationList { - pub fn into_inner(self) -> Vec { - self.operations - } -} - -impl std::ops::Deref for NodeOperationList { - type Target = Vec; - - fn deref(&self) -> &Self::Target { - &self.operations - } -} - -impl std::ops::DerefMut for NodeOperationList { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.operations - } -} - -impl NodeOperationList { - pub fn new(operations: Vec) -> Self { - Self { operations } - } - - pub fn from_bytes(bytes: Vec) -> Result { - let operation_list = serde_json::from_slice(&bytes).map_err(|err| OTError::serde().context(err))?; - Ok(operation_list) - } - - pub fn to_bytes(&self) -> Result, OTError> { - let bytes = serde_json::to_vec(self).map_err(|err| OTError::serde().context(err))?; - Ok(bytes) - } -} diff --git a/shared-lib/lib-ot/src/core/document/path.rs b/shared-lib/lib-ot/src/core/document/path.rs deleted file mode 100644 index 245abde36c..0000000000 --- a/shared-lib/lib-ot/src/core/document/path.rs +++ /dev/null @@ -1,127 +0,0 @@ -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Serialize, Deserialize, Eq, PartialEq, Debug)] -pub struct Path(pub Vec); - -impl std::ops::Deref for Path { - type Target = Vec; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl std::convert::From for Path { - fn from(val: usize) -> Self { - Path(vec![val]) - } -} - -impl std::convert::From<&usize> for Path { - fn from(val: &usize) -> Self { - Path(vec![*val]) - } -} - -impl std::convert::From<&Path> for Path { - fn from(path: &Path) -> Self { - path.clone() - } -} - -impl From> for Path { - fn from(v: Vec) -> Self { - Path(v) - } -} - -impl From<&Vec> for Path { - fn from(values: &Vec) -> Self { - Path(values.clone()) - } -} - -impl From<&[usize]> for Path { - fn from(values: &[usize]) -> Self { - Path(values.to_vec()) - } -} - -impl Path { - // delta is default to be 1 - pub fn transform(pre_insert_path: &Path, b: &Path, offset: i64) -> Path { - if pre_insert_path.len() > b.len() { - return b.clone(); - } - if pre_insert_path.is_empty() || b.is_empty() { - return b.clone(); - } - // check the prefix - for i in 0..(pre_insert_path.len() - 1) { - if pre_insert_path.0[i] != b.0[i] { - return b.clone(); - } - } - let mut prefix: Vec = pre_insert_path.0[0..(pre_insert_path.len() - 1)].into(); - let mut suffix: Vec = b.0[pre_insert_path.0.len()..].into(); - let prev_insert_last: usize = *pre_insert_path.0.last().unwrap(); - let b_at_index = b.0[pre_insert_path.0.len() - 1]; - if prev_insert_last <= b_at_index { - prefix.push(((b_at_index as i64) + offset) as usize); - } else { - prefix.push(b_at_index); - } - prefix.append(&mut suffix); - - Path(prefix) - } -} - -#[cfg(test)] -mod tests { - use crate::core::Path; - #[test] - fn path_transform_test_1() { - assert_eq!( - { Path::transform(&Path(vec![0, 1]), &Path(vec![0, 1]), 1) }.0, - vec![0, 2] - ); - - assert_eq!( - { Path::transform(&Path(vec![0, 1]), &Path(vec![0, 1]), 5) }.0, - vec![0, 6] - ); - } - - #[test] - fn path_transform_test_2() { - assert_eq!( - { Path::transform(&Path(vec![0, 1]), &Path(vec![0, 2]), 1) }.0, - vec![0, 3] - ); - } - - #[test] - fn path_transform_test_3() { - assert_eq!( - { Path::transform(&Path(vec![0, 1]), &Path(vec![0, 2, 7, 8, 9]), 1) }.0, - vec![0, 3, 7, 8, 9] - ); - } - - #[test] - fn path_transform_no_changed_test() { - assert_eq!( - { Path::transform(&Path(vec![0, 1, 2]), &Path(vec![0, 0, 7, 8, 9]), 1) }.0, - vec![0, 0, 7, 8, 9] - ); - assert_eq!( - { Path::transform(&Path(vec![0, 1, 2]), &Path(vec![0, 1]), 1) }.0, - vec![0, 1] - ); - assert_eq!( - { Path::transform(&Path(vec![1, 1]), &Path(vec![1, 0]), 1) }.0, - vec![1, 0] - ); - } -} diff --git a/shared-lib/lib-ot/src/core/mod.rs b/shared-lib/lib-ot/src/core/mod.rs index e8253e6ebc..7d038c8b5d 100644 --- a/shared-lib/lib-ot/src/core/mod.rs +++ b/shared-lib/lib-ot/src/core/mod.rs @@ -1,12 +1,12 @@ pub mod attributes; mod delta; -mod document; mod interval; +mod node_tree; mod ot_str; pub use attributes::*; pub use delta::operation::*; pub use delta::*; -pub use document::*; pub use interval::*; +pub use node_tree::*; pub use ot_str::*; diff --git a/shared-lib/lib-ot/src/core/document/mod.rs b/shared-lib/lib-ot/src/core/node_tree/mod.rs similarity index 84% rename from shared-lib/lib-ot/src/core/document/mod.rs rename to shared-lib/lib-ot/src/core/node_tree/mod.rs index b60d8c0251..1f7205201d 100644 --- a/shared-lib/lib-ot/src/core/document/mod.rs +++ b/shared-lib/lib-ot/src/core/node_tree/mod.rs @@ -2,14 +2,14 @@ mod node; mod node_serde; -mod node_tree; mod operation; mod operation_serde; mod path; mod transaction; +mod tree; pub use node::*; -pub use node_tree::*; pub use operation::*; pub use path::*; pub use transaction::*; +pub use tree::*; diff --git a/shared-lib/lib-ot/src/core/document/node.rs b/shared-lib/lib-ot/src/core/node_tree/node.rs similarity index 98% rename from shared-lib/lib-ot/src/core/document/node.rs rename to shared-lib/lib-ot/src/core/node_tree/node.rs index 18651a6062..908cfa3f18 100644 --- a/shared-lib/lib-ot/src/core/document/node.rs +++ b/shared-lib/lib-ot/src/core/node_tree/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_serde.rs b/shared-lib/lib-ot/src/core/node_tree/node_serde.rs similarity index 100% rename from shared-lib/lib-ot/src/core/document/node_serde.rs rename to shared-lib/lib-ot/src/core/node_tree/node_serde.rs diff --git a/shared-lib/lib-ot/src/core/node_tree/operation.rs b/shared-lib/lib-ot/src/core/node_tree/operation.rs new file mode 100644 index 0000000000..cdd2b99c7b --- /dev/null +++ b/shared-lib/lib-ot/src/core/node_tree/operation.rs @@ -0,0 +1,174 @@ +use crate::core::attributes::Attributes; +use crate::core::{NodeBodyChangeset, NodeData, Path}; +use crate::errors::OTError; +use serde::{Deserialize, Serialize}; +use std::rc::Rc; + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "op")] +pub enum NodeOperation { + #[serde(rename = "insert")] + Insert { path: Path, nodes: Vec }, + + #[serde(rename = "update-attribute")] + UpdateAttributes { + path: Path, + new: Attributes, + old: Attributes, + }, + + #[serde(rename = "update-body")] + // #[serde(serialize_with = "serialize_edit_body")] + // #[serde(deserialize_with = "deserialize_edit_body")] + UpdateBody { path: Path, changeset: NodeBodyChangeset }, + + #[serde(rename = "delete")] + Delete { path: Path, nodes: Vec }, +} + +impl NodeOperation { + pub fn get_path(&self) -> &Path { + match self { + NodeOperation::Insert { path, .. } => path, + NodeOperation::UpdateAttributes { path, .. } => path, + NodeOperation::Delete { path, .. } => path, + NodeOperation::UpdateBody { path, .. } => path, + } + } + + pub fn get_mut_path(&mut self) -> &mut Path { + match self { + NodeOperation::Insert { path, .. } => path, + NodeOperation::UpdateAttributes { path, .. } => path, + NodeOperation::Delete { path, .. } => path, + NodeOperation::UpdateBody { path, .. } => path, + } + } + + pub fn invert(&self) -> NodeOperation { + match self { + NodeOperation::Insert { path, nodes } => NodeOperation::Delete { + path: path.clone(), + nodes: nodes.clone(), + }, + NodeOperation::UpdateAttributes { + path, + new: attributes, + old: old_attributes, + } => NodeOperation::UpdateAttributes { + path: path.clone(), + new: old_attributes.clone(), + old: attributes.clone(), + }, + NodeOperation::Delete { path, nodes } => NodeOperation::Insert { + path: path.clone(), + nodes: nodes.clone(), + }, + NodeOperation::UpdateBody { path, changeset: body } => NodeOperation::UpdateBody { + path: path.clone(), + changeset: body.inverted(), + }, + } + } + + /// Make the `other` operation can be applied to the version after applying the `self` operation. + /// The semantics of transform is used when editing conflicts occur, which is often determined by the version id。 + /// For example, if the inserted position has been acquired by others, then it's needed to do the transform to + /// make sure the inserted position is right. + /// + /// # Arguments + /// + /// * `other`: The operation that is going to be transformed + /// + /// # 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"}]}"#); + /// + /// op_1.transform(&mut op_2); + /// assert_eq!(serde_json::to_string(&op_2).unwrap(), r#"{"op":"insert","path":[0,2],"nodes":[{"type":"text_2"}]}"#); + /// + /// ``` + pub fn transform(&self, other: &mut NodeOperation) { + match self { + NodeOperation::Insert { path, nodes } => { + let new_path = path.transform(other.get_path(), nodes.len()); + *other.get_mut_path() = new_path; + } + NodeOperation::Delete { path, nodes } => { + let new_path = path.transform(other.get_path(), nodes.len()); + *other.get_mut_path() = new_path; + } + _ => { + // Only insert/delete will change the path. + } + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct NodeOperationList { + operations: Vec>, +} + +impl NodeOperationList { + pub fn into_inner(self) -> Vec> { + self.operations + } + + pub fn add_op(&mut self, operation: NodeOperation) { + self.operations.push(Rc::new(operation)); + } +} + +impl std::ops::Deref for NodeOperationList { + type Target = Vec>; + + fn deref(&self) -> &Self::Target { + &self.operations + } +} + +impl std::ops::DerefMut for NodeOperationList { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.operations + } +} + +impl std::convert::From> for NodeOperationList { + fn from(operations: Vec) -> Self { + Self::new(operations) + } +} + +impl NodeOperationList { + pub fn new(operations: Vec) -> Self { + Self { + operations: operations.into_iter().map(Rc::new).collect(), + } + } + + pub fn from_bytes(bytes: Vec) -> Result { + let operation_list = serde_json::from_slice(&bytes).map_err(|err| OTError::serde().context(err))?; + Ok(operation_list) + } + + pub fn to_bytes(&self) -> Result, OTError> { + let bytes = serde_json::to_vec(self).map_err(|err| OTError::serde().context(err))?; + Ok(bytes) + } +} diff --git a/shared-lib/lib-ot/src/core/document/operation_serde.rs b/shared-lib/lib-ot/src/core/node_tree/operation_serde.rs similarity index 100% rename from shared-lib/lib-ot/src/core/document/operation_serde.rs rename to shared-lib/lib-ot/src/core/node_tree/operation_serde.rs diff --git a/shared-lib/lib-ot/src/core/node_tree/path.rs b/shared-lib/lib-ot/src/core/node_tree/path.rs new file mode 100644 index 0000000000..cf7ae647ed --- /dev/null +++ b/shared-lib/lib-ot/src/core/node_tree/path.rs @@ -0,0 +1,190 @@ +use serde::{Deserialize, Serialize}; + +/// The `Path` represents as a path to reference to the node in the `NodeTree`. +/// ┌─────────┐ +/// │ Root │ +/// └─────────┼──────────┐ +/// │0: Node A │ +/// └──────────┼────────────┐ +/// │0: Node A-1 │ +/// ├────────────┤ +/// │1: Node A-2 │ +/// ┌──────────┼────────────┘ +/// │1: Node B │ +/// └──────────┼────────────┐ +/// │0: Node B-1 │ +/// ├────────────┤ +/// │1: Node B-2 │ +/// ┌──────────┼────────────┘ +/// │2: Node C │ +/// └──────────┘ +/// +/// The path of Node A will be [0] +/// The path of Node A-1 will be [0,0] +/// The path of Node A-2 will be [0,1] +/// The path of Node B-2 will be [1,1] +#[derive(Clone, Serialize, Deserialize, Eq, PartialEq, Debug)] +pub struct Path(pub Vec); + +impl std::ops::Deref for Path { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl std::convert::From for Path { + fn from(val: usize) -> Self { + Path(vec![val]) + } +} + +impl std::convert::From<&usize> for Path { + fn from(val: &usize) -> Self { + Path(vec![*val]) + } +} + +impl std::convert::From<&Path> for Path { + fn from(path: &Path) -> Self { + path.clone() + } +} + +impl From> for Path { + fn from(v: Vec) -> Self { + Path(v) + } +} + +impl From<&Vec> for Path { + fn from(values: &Vec) -> Self { + Path(values.clone()) + } +} + +impl From<&[usize]> for Path { + fn from(values: &[usize]) -> Self { + Path(values.to_vec()) + } +} + +impl Path { + /// Calling this function if there are two changes want to modify the same path. + /// + /// # Arguments + /// + /// * `other`: the path that need to be transformed + /// * `offset`: represents the len of nodes referenced by the current path + /// + /// If two changes modify the same path or the path was shared by them. Then it needs to do the + /// transformation to make sure the changes are applied to the right path. + /// + /// returns: the path represents the position that the other path reference to. + /// + /// # Examples + /// + /// ``` + /// use lib_ot::core::Path; + /// let path = Path(vec![0, 1]); + /// for (old_path, len_of_nodes, expected_path) in vec![ + /// // Try to modify the path [0, 1], but someone has inserted one element before the + /// // current path [0,1] in advance. That causes the modified path [0,1] to no longer + /// // valid. It needs to do the transformation to get the right path. + /// // + /// // [0,2] is the path you want to modify. + /// (Path(vec![0, 1]), 1, Path(vec![0, 2])), + /// (Path(vec![0, 1]), 5, Path(vec![0, 6])), + /// (Path(vec![0, 2]), 1, Path(vec![0, 3])), + /// // Try to modify the path [0, 2,3,4], but someone has inserted one element before the + /// // current path [0,1] in advance. That cause the prefix path [0,2] of [0,2,3,4] + /// // no longer valid. + /// // It needs to do the transformation to get the right path. So [0,2] is transformed to [0,3] + /// // and the suffix [3,4] of the [0,2,3,4] remains the same. So the transformed result is + /// // + /// // [0,3,3,4] + /// (Path(vec![0, 2, 3, 4]), 1, Path(vec![0, 3, 3, 4])), + /// ] { + /// assert_eq!(path.transform(&old_path, len_of_nodes), expected_path); + /// } + /// // The path remains the same in the following test. Because the shared path is not changed. + /// let path = Path(vec![0, 1, 2]); + /// for (old_path, len_of_nodes, expected_path) in vec![ + /// // Try to modify the path [0,0,0,1,2], but someone has inserted one element + /// // before [0,1,2]. [0,0,0,1,2] and [0,1,2] share the same path [0,x], because + /// // the element was inserted at [0,1,2] that didn't affect the shared path [0, x]. + /// // So, after the transformation, the path is not changed. + /// (Path(vec![0, 0, 0, 1, 2]), 1, Path(vec![0, 0, 0, 1, 2])), + /// (Path(vec![0, 1]), 1, Path(vec![0, 1])), + /// ] { + /// assert_eq!(path.transform(&old_path, len_of_nodes), expected_path); + /// } + /// + /// let path = Path(vec![1, 1]); + /// for (old_path, len_of_nodes, expected_path) in vec![(Path(vec![1, 0]), 1, Path(vec![1, 0]))] { + /// assert_eq!(path.transform(&old_path, len_of_nodes), expected_path); + /// } + /// ``` + /// For example, client A and client B want to insert a node at the same index, the server applies + /// the changes made by client B. But, before applying the client A's changes, server transforms + /// the changes first in order to make sure that client A modify the right position. After that, + /// the changes can be applied to the server. + /// + /// ┌──────────┐ ┌──────────┐ ┌──────────┐ + /// │ Client A │ │ Server │ │ Client B │ + /// └─────┬────┘ └─────┬────┘ └────┬─────┘ + /// │ │ ┌ ─ ─ ─ ─ ─ ─ ─ ┐ │ + /// │ │ Root │ + /// │ │ │ 0:A │ │ + /// │ │ ─ ─ ─ ─ ─ ─ ─ ─ │ + /// │ │ ◀───────────────────────│ + /// │ │ Insert B at index 1 │ + /// │ │ │ + /// │ │ ┌ ─ ─ ─ ─ ─ ─ ─ ┐ │ + /// │ │ Root │ + /// │ │ │ 0:A │ │ + /// ├──────────────────────▶│ 1:B │ + /// │ Insert C at index 1 │ └ ─ ─ ─ ─ ─ ─ ─ ┘ │ + /// │ │ │ + /// │ │ transform index 1 to 2 │ + /// │ │ │ + /// │ │ ┌ ─ ─ ─ ─ ─ ─ ─ ─ │ + /// │ │ Root │ │ + /// │ │ │ 0:A │ + /// ▼ ▼ 1:B │ ▼ + /// │ 2:C + /// ─ ─ ─ ─ ─ ─ ─ ─ ┘ + pub fn transform(&self, other: &Path, offset: usize) -> Path { + if self.len() > other.len() { + return other.clone(); + } + if self.is_empty() || other.is_empty() { + return other.clone(); + } + for i in 0..(self.len() - 1) { + if self.0[i] != other.0[i] { + return other.clone(); + } + } + + // Splits the `Path` into two part. The suffix will contain the last element of the `Path`. + let second_last_index = self.0.len() - 1; + let mut prefix: Vec = self.0[0..second_last_index].into(); + let mut suffix: Vec = other.0[self.0.len()..].into(); + let last_value = *self.0.last().unwrap(); + + let other_second_last_value = other.0[second_last_index]; + + // + if last_value <= other_second_last_value { + prefix.push(other_second_last_value + offset); + } else { + prefix.push(other_second_last_value); + } + + // concat the prefix and suffix into a new path + prefix.append(&mut suffix); + Path(prefix) + } +} diff --git a/shared-lib/lib-ot/src/core/document/transaction.rs b/shared-lib/lib-ot/src/core/node_tree/transaction.rs similarity index 70% rename from shared-lib/lib-ot/src/core/document/transaction.rs rename to shared-lib/lib-ot/src/core/node_tree/transaction.rs index 9b48ddd92d..1f855698cf 100644 --- a/shared-lib/lib-ot/src/core/document/transaction.rs +++ b/shared-lib/lib-ot/src/core/node_tree/transaction.rs @@ -1,26 +1,57 @@ use crate::core::attributes::Attributes; -use crate::core::document::path::Path; -use crate::core::{NodeData, NodeOperation, NodeTree}; +use crate::core::{NodeData, NodeOperation, NodeTree, Path}; +use crate::errors::OTError; use indextree::NodeId; +use std::rc::Rc; use super::{NodeBodyChangeset, NodeOperationList}; +#[derive(Debug, Clone, Default)] pub struct Transaction { operations: NodeOperationList, } impl Transaction { - pub fn new(operations: NodeOperationList) -> Transaction { - Transaction { operations } + pub fn new() -> Self { + Self::default() } - pub fn into_operations(self) -> Vec { + pub fn from_operations>(operations: T) -> Self { + Self { + operations: operations.into(), + } + } + + pub fn into_operations(self) -> Vec> { self.operations.into_inner() } + + /// Make the `other` can be applied to the version after applying the `self` transaction. + /// + /// The semantics of transform is used when editing conflicts occur, which is often determined by the version id。 + /// the operations of the transaction will be transformed into the conflict operations. + pub fn transform(&self, other: &Transaction) -> Result { + let mut new_transaction = other.clone(); + for other_operation in new_transaction.iter_mut() { + let other_operation = Rc::make_mut(other_operation); + for operation in self.operations.iter() { + operation.transform(other_operation); + } + } + Ok(new_transaction) + } + + pub fn compose(&mut self, other: &Transaction) -> Result<(), OTError> { + // For the moment, just append `other` operations to the end of `self`. + for operation in other.operations.iter() { + self.operations.push(operation.clone()); + } + Ok(()) + } } impl std::ops::Deref for Transaction { - type Target = NodeOperationList; + type Target = Vec>; fn deref(&self) -> &Self::Target { &self.operations @@ -64,7 +95,7 @@ impl<'a> TransactionBuilder<'a> { /// let transaction = TransactionBuilder::new(&node_tree) /// .insert_nodes_at_path(0,vec![ NodeData::new("text_1"), NodeData::new("text_2")]) /// .finalize(); - /// node_tree.apply(transaction).unwrap(); + /// node_tree.apply_transaction(transaction).unwrap(); /// /// node_tree.node_id_at_path(vec![0, 0]); /// ``` @@ -94,7 +125,7 @@ impl<'a> TransactionBuilder<'a> { /// let transaction = TransactionBuilder::new(&node_tree) /// .insert_node_at_path(0, NodeData::new("text")) /// .finalize(); - /// node_tree.apply(transaction).unwrap(); + /// node_tree.apply_transaction(transaction).unwrap(); /// ``` /// pub fn insert_node_at_path>(self, path: T, node: NodeData) -> Self { @@ -112,10 +143,10 @@ impl<'a> TransactionBuilder<'a> { } } - self.operations.push(NodeOperation::UpdateAttributes { + self.operations.add_op(NodeOperation::UpdateAttributes { path: path.clone(), - attributes, - old_attributes, + new: attributes, + old: old_attributes, }); } None => tracing::warn!("Update attributes at path: {:?} failed. Node is not exist", path), @@ -126,7 +157,7 @@ impl<'a> TransactionBuilder<'a> { pub fn update_body_at_path(mut self, path: &Path, changeset: NodeBodyChangeset) -> Self { match self.node_tree.node_id_at_path(path) { Some(_) => { - self.operations.push(NodeOperation::UpdateBody { + self.operations.add_op(NodeOperation::UpdateBody { path: path.clone(), changeset, }); @@ -148,7 +179,7 @@ impl<'a> TransactionBuilder<'a> { node = self.node_tree.following_siblings(node).next().unwrap(); } - self.operations.push(NodeOperation::Delete { + self.operations.add_op(NodeOperation::Delete { path: path.clone(), nodes: deleted_nodes, }); @@ -172,11 +203,11 @@ impl<'a> TransactionBuilder<'a> { } pub fn push(mut self, op: NodeOperation) -> Self { - self.operations.push(op); + self.operations.add_op(op); self } pub fn finalize(self) -> Transaction { - Transaction::new(self.operations) + Transaction::from_operations(self.operations) } } diff --git a/shared-lib/lib-ot/src/core/document/node_tree.rs b/shared-lib/lib-ot/src/core/node_tree/tree.rs similarity index 86% rename from shared-lib/lib-ot/src/core/document/node_tree.rs rename to shared-lib/lib-ot/src/core/node_tree/tree.rs index 7f88224f24..fde8ccf20c 100644 --- a/shared-lib/lib-ot/src/core/document/node_tree.rs +++ b/shared-lib/lib-ot/src/core/node_tree/tree.rs @@ -1,8 +1,8 @@ use crate::core::attributes::Attributes; -use crate::core::document::path::Path; -use crate::core::{Node, NodeBodyChangeset, NodeData, NodeOperation, OperationTransform, Transaction}; +use crate::core::{Node, NodeBodyChangeset, NodeData, NodeOperation, OperationTransform, Path, Transaction}; use crate::errors::{ErrorBuilder, OTError, OTErrorCode}; use indextree::{Arena, Children, FollowingSiblings, NodeId}; +use std::rc::Rc; use super::NodeOperationList; @@ -26,14 +26,13 @@ impl NodeTree { } pub fn from_bytes(root_name: &str, bytes: Vec) -> Result { - let operations = NodeOperationList::from_bytes(bytes)?.into_inner(); + let operations = NodeOperationList::from_bytes(bytes)?; Self::from_operations(root_name, operations) } - pub fn from_operations(root_name: &str, operations: Vec) -> Result { + pub fn from_operations(root_name: &str, operations: NodeOperationList) -> Result { let mut node_tree = NodeTree::new(root_name); - - for operation in operations { + for operation in operations.into_inner().into_iter() { let _ = node_tree.apply_op(operation)?; } Ok(node_tree) @@ -54,13 +53,14 @@ impl NodeTree { /// # Examples /// /// ``` + /// use std::rc::Rc; /// use lib_ot::core::{NodeOperation, NodeTree, NodeData, Path}; /// let nodes = vec![NodeData::new("text".to_string())]; /// let root_path: Path = vec![0].into(); /// let op = NodeOperation::Insert {path: root_path.clone(),nodes }; /// /// let mut node_tree = NodeTree::new("root"); - /// node_tree.apply_op(op).unwrap(); + /// node_tree.apply_op(Rc::new(op)).unwrap(); /// let node_id = node_tree.node_id_at_path(&root_path).unwrap(); /// let node_path = node_tree.path_from_node_id(node_id); /// debug_assert_eq!(node_path, root_path); @@ -105,23 +105,25 @@ impl NodeTree { counter } - /// + /// Returns the note_id at the position of the tree with id note_id /// # Arguments /// - /// * `node_id`: - /// * `index`: + /// * `node_id`: the node id of the child's parent + /// * `index`: index of the node in parent children list /// /// returns: Option /// /// # Examples /// /// ``` + /// use std::rc::Rc; /// use lib_ot::core::{NodeOperation, NodeTree, NodeData, Path}; /// let node_1 = NodeData::new("text".to_string()); /// let inserted_path: Path = vec![0].into(); /// /// let mut node_tree = NodeTree::new("root"); - /// node_tree.apply_op(NodeOperation::Insert {path: inserted_path.clone(),nodes: vec![node_1.clone()] }).unwrap(); + /// let op = NodeOperation::Insert {path: inserted_path.clone(),nodes: vec![node_1.clone()] }; + /// node_tree.apply_op(Rc::new(op)).unwrap(); /// /// let node_2 = node_tree.get_node_at_path(&inserted_path).unwrap(); /// assert_eq!(node_2.node_type, node_1.node_type); @@ -137,6 +139,10 @@ impl NodeTree { None } + /// Returns all children whose parent node id is node_id + /// + /// * `node_id`: the children's parent node id + /// pub fn children_from_node(&self, node_id: NodeId) -> Children<'_, Node> { node_id.children(&self.arena) } @@ -159,7 +165,7 @@ impl NodeTree { node_id.following_siblings(&self.arena) } - pub fn apply(&mut self, transaction: Transaction) -> Result<(), OTError> { + pub fn apply_transaction(&mut self, transaction: Transaction) -> Result<(), OTError> { let operations = transaction.into_operations(); for operation in operations { self.apply_op(operation)?; @@ -167,10 +173,15 @@ impl NodeTree { Ok(()) } - pub fn apply_op(&mut self, op: NodeOperation) -> Result<(), OTError> { + pub fn apply_op(&mut self, op: Rc) -> Result<(), OTError> { + let op = match Rc::try_unwrap(op) { + Ok(op) => op, + Err(op) => op.as_ref().clone(), + }; + match op { NodeOperation::Insert { path, nodes } => self.insert_nodes(&path, nodes), - NodeOperation::UpdateAttributes { path, attributes, .. } => self.update_attributes(&path, attributes), + NodeOperation::UpdateAttributes { path, new, .. } => self.update_attributes(&path, new), NodeOperation::UpdateBody { path, changeset } => self.update_body(&path, changeset), NodeOperation::Delete { path, nodes } => self.delete_node(&path, nodes), } @@ -216,7 +227,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/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; pub type TextDeltaBuilder = OperationBuilder; - pub type TextOperation = Operation; 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..a0be952afb 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}; use lib_ot::{ core::{NodeBodyChangeset, NodeData, NodeDataBuilder, NodeOperation, Path}, text_delta::TextDeltaBuilder, @@ -33,15 +35,14 @@ fn operation_insert_node_with_children_serde_test() { fn operation_update_node_attributes_serde_test() { let operation = NodeOperation::UpdateAttributes { path: Path(vec![0, 1]), - attributes: AttributeBuilder::new().insert("bold", true).build(), - old_attributes: AttributeBuilder::new().insert("bold", false).build(), + new: AttributeBuilder::new().insert("bold", true).build(), + old: AttributeBuilder::new().insert("bold", false).build(), }; let result = serde_json::to_string(&operation).unwrap(); - assert_eq!( result, - r#"{"op":"update","path":[0,1],"attributes":{"bold":true},"oldAttributes":{"bold":null}}"# + r#"{"op":"update-attribute","path":[0,1],"new":{"bold":true},"old":{"bold":null}}"# ); } @@ -69,3 +70,166 @@ 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_op_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(); + + op_1.transform(&mut insert_2); + let json = serde_json::to_string(&insert_2).unwrap(); + assert_eq!(json, r#"{"op":"insert","path":[0,2],"nodes":[{"type":"text_2"}]}"#); +} + +#[test] +fn operation_insert_transform_one_level_path_test() { + 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_data_3 = NodeDataBuilder::new("text_3").build(); + let node_3: Node = node_data_3.clone().into(); + // 0: text_1 + // 1: text_2 + // + // Insert a new operation with rev_id 1,but the rev_id:1 is already exist, so + // it needs to be transformed. + // 1:text_3 => 2:text_3 + // + // 0: text_1 + // 1: text_2 + // 2: text_3 + // + // If the rev_id of the insert operation is 3. then the tree will be: + // 0: text_1 + // 1: text_3 + // 2: text_2 + let scripts = vec![ + InsertNode { + path: 0.into(), + node_data: node_data_1, + rev_id: 1, + }, + InsertNode { + path: 1.into(), + node_data: node_data_2, + rev_id: 2, + }, + InsertNode { + path: 1.into(), + node_data: node_data_3, + rev_id: 1, + }, + AssertNode { + path: 2.into(), + expected: Some(node_3), + }, + ]; + test.run_scripts(scripts); +} + +#[test] +fn operation_insert_transform_multiple_level_path_test() { + let mut test = NodeTest::new(); + let node_data_1 = NodeDataBuilder::new("text_1") + .add_node(NodeDataBuilder::new("text_1_1").build()) + .add_node(NodeDataBuilder::new("text_1_2").build()) + .build(); + + let node_data_2 = NodeDataBuilder::new("text_2") + .add_node(NodeDataBuilder::new("text_2_1").build()) + .add_node(NodeDataBuilder::new("text_2_2").build()) + .build(); + + let node_data_3 = NodeDataBuilder::new("text_3").build(); + let scripts = vec![ + InsertNode { + path: 0.into(), + node_data: node_data_1, + rev_id: 1, + }, + InsertNode { + path: 1.into(), + node_data: node_data_2, + rev_id: 2, + }, + InsertNode { + path: 1.into(), + node_data: node_data_3.clone(), + rev_id: 1, + }, + AssertNode { + path: 2.into(), + expected: Some(node_data_3.into()), + }, + ]; + test.run_scripts(scripts); +} + +#[test] +fn operation_delete_transform_path_test() { + 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_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, + rev_id: 1, + }, + InsertNode { + path: 1.into(), + node_data: node_data_2, + rev_id: 2, + }, + // The node's in the tree will be: + // 0: text_1 + // 2: text_2 + // + // The insert action is happened concurrently with the delete action, because they + // share the same rev_id. aka, 3. The delete action is want to delete the node at index 1, + // but it was moved to index 2. + InsertNode { + path: 1.into(), + node_data: node_data_3, + rev_id: 3, + }, + // 0: text_1 + // 1: text_3 + // 2: text_2 + // + // The path of the delete action will be transformed to a new path that point to the text_2. + // 1 -> 2 + DeleteNode { + path: 1.into(), + rev_id: 3, + }, + // After perform the delete action, the tree will be: + // 0: text_1 + // 1: text_3 + AssertNumberOfNodesAtPath { path: None, len: 2 }, + AssertNode { + path: 1.into(), + expected: Some(node_3), + }, + AssertNode { + path: 2.into(), + expected: None, + }, + ]; + 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..678138e183 100644 --- a/shared-lib/lib-ot/tests/node/script.rs +++ b/shared-lib/lib-ot/tests/node/script.rs @@ -1,26 +1,58 @@ +use lib_ot::core::{Node, 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, len: usize }, - AssertNode { path: Path, expected: Option }, - 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, + rev_id: usize, + }, + AssertNumberOfNodesAtPath { + path: Option, + len: usize, + }, + AssertNodeData { + path: Path, + expected: Option, + }, + AssertNode { + path: Path, + expected: Option, + }, + AssertNodeDelta { + path: Path, + expected: TextDelta, + }, } pub struct NodeTest { + rev_id: usize, + rev_operations: HashMap, node_tree: NodeTree, } impl NodeTest { pub fn new() -> Self { Self { + rev_id: 0, + rev_operations: HashMap::new(), node_tree: NodeTree::new("root"), } } @@ -33,40 +65,54 @@ 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) + NodeScript::DeleteNode { path, rev_id } => { + let mut transaction = TransactionBuilder::new(&self.node_tree) .delete_node_at_path(&path) .finalize(); - self.node_tree.apply(transaction).unwrap(); + self.transform_transaction_if_need(&mut transaction, rev_id); + self.apply_transaction(transaction); } NodeScript::AssertNode { path, expected } => { let node_id = self.node_tree.node_id_at_path(path); + if expected.is_none() && node_id.is_none() { + return; + } + + let node = self.node_tree.get_node(node_id.unwrap()).cloned(); + 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())); } } } @@ -94,4 +140,19 @@ impl NodeTest { } } } + + 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(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(); + *transaction = old_transaction.transform(transaction).unwrap(); + } + } + } } diff --git a/shared-lib/lib-ot/tests/node/tree_test.rs b/shared-lib/lib-ot/tests/node/tree_test.rs index 95c05ea25a..c1f1532385 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), }, @@ -96,48 +101,145 @@ fn node_insert_node_in_ordered_nodes_test() { let path_3: Path = 2.into(); let node_3 = NodeData::new("text_3"); - let path_4: Path = 3.into(); - 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, + rev_id: 3, }, - // 0:note_1 , 1: note_2_1, 2: note_3 + // 0:text_1 + // 1:text_2_1 + // 2:text_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 { + // 0:text_1 + // 1:text_2_2 + // 2:text_2_1 + // 3:text_3 + 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 { - path: path_4, - expected: Some(node_3), - }, AssertNumberOfNodesAtPath { path: None, len: 4 }, ]; test.run_scripts(scripts); } +#[test] +fn node_insert_nested_nodes_test() { + let mut test = NodeTest::new(); + let node_data_1_1 = NodeDataBuilder::new("text_1_1").build(); + let node_data_1_2 = NodeDataBuilder::new("text_1_2").build(); + let node_data_1 = NodeDataBuilder::new("text_1") + .add_node(node_data_1_1.clone()) + .add_node(node_data_1_2.clone()) + .build(); + + let node_data_2_1 = NodeDataBuilder::new("text_2_1").build(); + let node_data_2_2 = NodeDataBuilder::new("text_2_2").build(); + let node_data_2 = NodeDataBuilder::new("text_2") + .add_node(node_data_2_1.clone()) + .add_node(node_data_2_2.clone()) + .build(); + + let scripts = vec![ + InsertNode { + path: 0.into(), + node_data: node_data_1, + rev_id: 1, + }, + InsertNode { + path: 1.into(), + node_data: node_data_2, + rev_id: 2, + }, + // the tree will be: + // 0:text_1 + // 0:text_1_1 + // 1:text_1_2 + // 1:text_2 + // 0:text_2_1 + // 1:text_2_2 + AssertNode { + path: vec![0, 0].into(), + expected: Some(node_data_1_1.into()), + }, + AssertNode { + path: vec![0, 1].into(), + expected: Some(node_data_1_2.into()), + }, + AssertNode { + path: vec![1, 0].into(), + expected: Some(node_data_2_1.into()), + }, + AssertNode { + path: vec![1, 1].into(), + expected: Some(node_data_2_2.into()), + }, + ]; + test.run_scripts(scripts); +} + +#[test] +fn node_insert_node_before_existing_nested_nodes_test() { + let mut test = NodeTest::new(); + let node_data_1_1 = NodeDataBuilder::new("text_1_1").build(); + let node_data_1_2 = NodeDataBuilder::new("text_1_2").build(); + let node_data_1 = NodeDataBuilder::new("text_1") + .add_node(node_data_1_1.clone()) + .add_node(node_data_1_2.clone()) + .build(); + + let scripts = vec![ + InsertNode { + path: 0.into(), + node_data: node_data_1, + rev_id: 1, + }, + // 0:text_1 + // 0:text_1_1 + // 1:text_1_2 + InsertNode { + path: 0.into(), + node_data: NodeDataBuilder::new("text_0").build(), + rev_id: 2, + }, + // 0:text_0 + // 1:text_1 + // 0:text_1_1 + // 1:text_1_2 + AssertNode { + path: vec![1, 0].into(), + expected: Some(node_data_1_1.into()), + }, + AssertNode { + path: vec![1, 1].into(), + expected: Some(node_data_1_2.into()), + }, + ]; + test.run_scripts(scripts); +} #[test] fn node_insert_with_attributes_test() { let mut test = NodeTest::new(); @@ -149,13 +251,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 +275,14 @@ 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 }, + DeleteNode { + path: path.clone(), + rev_id: 2, + }, + AssertNodeData { path, expected: None }, ]; test.run_scripts(scripts); } @@ -198,7 +305,8 @@ fn node_update_body_test() { let scripts = vec![ InsertNode { path: path.clone(), - node, + node_data: node, + rev_id: 1, }, UpdateBody { path: path.clone(),