appflowy 3 лет назад
Родитель
Сommit
1a869c0003
22 измененных файлов с 335 добавлено и 447 удалено
  1. 1 1
      backend/src/services/document/ws_actor.rs
  2. 9 8
      backend/tests/document_test/edit_script.rs
  3. 6 7
      backend/tests/document_test/edit_test.rs
  4. 1 1
      frontend/rust-lib/flowy-document/src/services/doc/edit/editor.rs
  5. 42 28
      frontend/rust-lib/flowy-document/src/services/doc/edit/queue.rs
  6. 12 14
      frontend/rust-lib/flowy-document/src/services/doc/revision/cache/cache.rs
  7. 11 0
      frontend/rust-lib/flowy-document/src/services/doc/revision/cache/disk.rs
  8. 23 2
      frontend/rust-lib/flowy-document/src/services/doc/revision/cache/memory.rs
  9. 26 27
      frontend/rust-lib/flowy-document/src/services/doc/revision/manager.rs
  10. 6 2
      frontend/rust-lib/flowy-document/src/services/doc/web_socket/http_ws_impl.rs
  11. 102 46
      frontend/rust-lib/flowy-document/src/services/doc/web_socket/web_socket.rs
  12. 0 116
      frontend/rust-lib/flowy-document/src/services/file/file.rs
  13. 0 120
      frontend/rust-lib/flowy-document/src/services/file/manager.rs
  14. 0 5
      frontend/rust-lib/flowy-document/src/services/file/mod.rs
  15. 25 25
      frontend/rust-lib/flowy-document/tests/editor/attribute_test.rs
  16. 2 2
      frontend/rust-lib/flowy-document/tests/editor/mod.rs
  17. 2 2
      frontend/rust-lib/flowy-document/tests/editor/op_test.rs
  18. 21 21
      frontend/rust-lib/flowy-document/tests/editor/undo_redo_test.rs
  19. 10 8
      shared-lib/flowy-collaboration/src/document/document.rs
  20. 22 7
      shared-lib/flowy-collaboration/src/entities/revision.rs
  21. 6 3
      shared-lib/flowy-collaboration/src/sync/server.rs
  22. 8 2
      shared-lib/flowy-collaboration/src/sync/synchronizer.rs

+ 1 - 1
backend/src/services/document/ws_actor.rs

@@ -73,7 +73,7 @@ impl DocumentWebSocketActor {
             .map_err(internal_error)??;
 
         tracing::debug!(
-            "[DocumentWebSocketActor]: receive client data: {}:{}, {:?}",
+            "[DocumentWebSocketActor]: client data: {}:{}, {:?}",
             document_client_data.doc_id,
             document_client_data.id,
             document_client_data.ty

+ 9 - 8
backend/tests/document_test/edit_script.rs

@@ -54,7 +54,7 @@ impl DocumentTest {
 
 #[derive(Clone)]
 struct ScriptContext {
-    client_edit_context: Option<Arc<ClientDocumentEditor>>,
+    client_editor: Option<Arc<ClientDocumentEditor>>,
     client_sdk: FlowySDKTest,
     client_user_session: Arc<UserSession>,
     ws_conn: Arc<FlowyWSConnect>,
@@ -69,7 +69,7 @@ impl ScriptContext {
         let doc_id = create_doc(&client_sdk).await;
 
         Self {
-            client_edit_context: None,
+            client_editor: None,
             client_sdk,
             client_user_session: user_session,
             ws_conn: ws_manager,
@@ -81,10 +81,10 @@ impl ScriptContext {
     async fn open_doc(&mut self) {
         let doc_id = self.doc_id.clone();
         let edit_context = self.client_sdk.document_ctx.controller.open(doc_id).await.unwrap();
-        self.client_edit_context = Some(edit_context);
+        self.client_editor = Some(edit_context);
     }
 
-    fn client_edit_context(&self) -> Arc<ClientDocumentEditor> { self.client_edit_context.as_ref().unwrap().clone() }
+    fn client_editor(&self) -> Arc<ClientDocumentEditor> { self.client_editor.as_ref().unwrap().clone() }
 }
 
 async fn run_scripts(context: Arc<RwLock<ScriptContext>>, scripts: Vec<DocScript>) {
@@ -106,23 +106,23 @@ async fn run_scripts(context: Arc<RwLock<ScriptContext>>, scripts: Vec<DocScript
                 },
                 DocScript::ClientInsertText(index, s) => {
                     sleep(Duration::from_millis(2000)).await;
-                    context.read().client_edit_context().insert(index, s).await.unwrap();
+                    context.read().client_editor().insert(index, s).await.unwrap();
                 },
                 DocScript::ClientFormatText(interval, attribute) => {
                     context
                         .read()
-                        .client_edit_context()
+                        .client_editor()
                         .format(interval, attribute)
                         .await
                         .unwrap();
                 },
                 DocScript::AssertClient(s) => {
                     sleep(Duration::from_millis(2000)).await;
-                    let json = context.read().client_edit_context().doc_json().await.unwrap();
+                    let json = context.read().client_editor().doc_json().await.unwrap();
                     assert_eq(s, &json);
                 },
                 DocScript::AssertServer(s, rev_id) => {
-                    sleep(Duration::from_millis(100)).await;
+                    sleep(Duration::from_millis(2000)).await;
                     let persistence = Data::new(context.read().server.app_ctx.persistence.kv_store());
                     let doc_identifier: flowy_collaboration::protobuf::DocumentId = DocumentId {
                         doc_id
@@ -148,6 +148,7 @@ async fn run_scripts(context: Arc<RwLock<ScriptContext>>, scripts: Vec<DocScript
                     
                     let kv_store = Data::new(context.read().server.app_ctx.persistence.kv_store());
                     reset_doc(&doc_id, RepeatedRevision::new(vec![revision]), kv_store.get_ref()).await;
+                    sleep(Duration::from_millis(2000)).await;
                 },
                 // DocScript::Sleep(sec) => {
                 //     sleep(Duration::from_secs(sec)).await;

+ 6 - 7
backend/tests/document_test/edit_test.rs

@@ -1,5 +1,5 @@
 use crate::document_test::edit_script::{DocScript, DocumentTest};
-use flowy_collaboration::document::{Document, FlowyDoc};
+use flowy_collaboration::document::{Document, NewlineDoc};
 use lib_ot::{core::Interval, rich_text::RichTextAttribute};
 
 #[rustfmt::skip]
@@ -76,7 +76,7 @@ async fn delta_sync_while_editing_with_attribute() {
 #[actix_rt::test]
 async fn delta_sync_with_http_request() {
     let test = DocumentTest::new().await;
-    let mut document = Document::new::<FlowyDoc>();
+    let mut document = Document::new::<NewlineDoc>();
     document.insert(0, "123").unwrap();
     document.insert(3, "456").unwrap();
     
@@ -94,14 +94,13 @@ async fn delta_sync_with_http_request() {
 #[actix_rt::test]
 async fn delta_sync_with_server_push_delta() {
     let test = DocumentTest::new().await;
-    let mut document = Document::new::<FlowyDoc>();
+    let mut document = Document::new::<NewlineDoc>();
     document.insert(0, "123").unwrap();
     let json = document.to_json();
 
     test.run_scripts(vec![
         DocScript::ClientOpenDoc,
         DocScript::ServerSaveDocument(json, 3),
-        DocScript::ClientConnectWS,
         DocScript::AssertClient(r#"[{"insert":"123\n\n"}]"#),
         DocScript::AssertServer(r#"[{"insert":"123\n\n"}]"#, 3),
     ])
@@ -142,7 +141,7 @@ async fn delta_sync_with_server_push_delta() {
 #[actix_rt::test]
 async fn delta_sync_while_local_rev_less_than_server_rev() {
     let test = DocumentTest::new().await;
-    let mut document = Document::new::<FlowyDoc>();
+    let mut document = Document::new::<NewlineDoc>();
     document.insert(0, "123").unwrap();
     let json = document.to_json();
 
@@ -150,7 +149,7 @@ async fn delta_sync_while_local_rev_less_than_server_rev() {
         DocScript::ClientOpenDoc,
         DocScript::ServerSaveDocument(json, 3),
         DocScript::ClientInsertText(0, "abc"),
-        DocScript::ClientConnectWS,
+        // DocScript::ClientConnectWS,
         DocScript::AssertClient(r#"[{"insert":"abc\n123\n"}]"#),
         DocScript::AssertServer(r#"[{"insert":"abc\n123\n"}]"#, 4),
     ])
@@ -185,7 +184,7 @@ async fn delta_sync_while_local_rev_less_than_server_rev() {
 #[actix_rt::test]
 async fn delta_sync_while_local_rev_greater_than_server_rev() {
     let test = DocumentTest::new().await;
-    let mut document = Document::new::<FlowyDoc>();
+    let mut document = Document::new::<NewlineDoc>();
     document.insert(0, "123").unwrap();
     let json = document.to_json();
 

+ 1 - 1
frontend/rust-lib/flowy-document/src/services/doc/edit/editor.rs

@@ -150,7 +150,7 @@ impl ClientDocumentEditor {
 
     async fn save_local_delta(&self, delta: RichTextDelta, md5: String) -> Result<RevId, FlowyError> {
         let delta_data = delta.to_bytes();
-        let (base_rev_id, rev_id) = self.rev_manager.next_rev_id();
+        let (base_rev_id, rev_id) = self.rev_manager.next_rev_id_pair();
         let user_id = self.user.user_id()?;
         let revision = Revision::new(&self.doc_id, base_rev_id, rev_id, delta_data, &user_id, md5);
         let _ = self.rev_manager.add_local_revision(&revision).await?;

+ 42 - 28
frontend/rust-lib/flowy-document/src/services/doc/edit/queue.rs

@@ -1,6 +1,7 @@
 use async_stream::stream;
 
 use flowy_collaboration::{
+    document::{history::UndoResult, Document, NewlineDoc},
     entities::revision::Revision,
     errors::CollaborateError,
 };
@@ -12,7 +13,6 @@ use lib_ot::{
 };
 use std::sync::Arc;
 use tokio::sync::{mpsc, oneshot, RwLock};
-use flowy_collaboration::document::{Document, history::UndoResult};
 
 pub(crate) struct EditorCommandQueue {
     doc_id: String,
@@ -53,10 +53,29 @@ impl EditorCommandQueue {
     async fn handle_message(&self, msg: EditorCommand) -> Result<(), FlowyError> {
         match msg {
             EditorCommand::ComposeDelta { delta, ret } => {
-                let result = self.composed_delta(delta).await;
-                let _ = ret.send(result);
+                let fut = || async {
+                    let mut document = self.document.write().await;
+                    let _ = document.compose_delta(delta)?;
+                    let md5 = document.md5();
+                    drop(document);
+
+                    Ok::<String, CollaborateError>(md5)
+                };
+
+                let _ = ret.send(fut().await);
             },
-            EditorCommand::ProcessRemoteRevision { revisions, ret } => {
+            EditorCommand::OverrideDelta { delta, ret } => {
+                let fut = || async {
+                    let mut document = self.document.write().await;
+                    let _ = document.set_delta(delta);
+                    let md5 = document.md5();
+                    drop(document);
+                    Ok::<String, CollaborateError>(md5)
+                };
+
+                let _ = ret.send(fut().await);
+            },
+            EditorCommand::TransformRevision { revisions, ret } => {
                 let f = || async {
                     let mut new_delta = RichTextDelta::new();
                     for revision in revisions {
@@ -73,15 +92,22 @@ impl EditorCommandQueue {
                     }
 
                     let read_guard = self.document.read().await;
-                    let (server_prime, client_prime) = read_guard.delta().transform(&new_delta)?;
-                    drop(read_guard);
+                    let mut server_prime: Option<RichTextDelta> = None;
+                    let client_prime: RichTextDelta;
+                    if read_guard.is_empty::<NewlineDoc>() {
+                        // Do nothing
+                        client_prime = new_delta;
+                    } else {
+                        let (s_prime, c_prime) = read_guard.delta().transform(&new_delta)?;
+                        client_prime = c_prime;
+                        server_prime = Some(s_prime);
+                    }
 
-                    let transform_delta = TransformDeltas {
+                    drop(read_guard);
+                    Ok::<TransformDeltas, CollaborateError>(TransformDeltas {
                         client_prime,
                         server_prime,
-                    };
-
-                    Ok::<TransformDeltas, CollaborateError>(transform_delta)
+                    })
                 };
                 let _ = ret.send(f().await);
             },
@@ -138,22 +164,6 @@ impl EditorCommandQueue {
         }
         Ok(())
     }
-
-    #[tracing::instrument(level = "debug", skip(self, delta), fields(compose_result), err)]
-    async fn composed_delta(&self, delta: RichTextDelta) -> Result<String, CollaborateError> {
-        // tracing::debug!("{:?} thread handle_message", thread::current(),);
-        let mut document = self.document.write().await;
-        tracing::Span::current().record(
-            "composed_delta",
-            &format!("doc_id:{} - {}", &self.doc_id, delta.to_json()).as_str(),
-        );
-
-        let _ = document.compose_delta(delta)?;
-        let md5 = document.md5();
-        drop(document);
-
-        Ok(md5)
-    }
 }
 
 pub(crate) type Ret<T> = oneshot::Sender<Result<T, CollaborateError>>;
@@ -166,7 +176,11 @@ pub(crate) enum EditorCommand {
         delta: RichTextDelta,
         ret: Ret<DocumentMD5>,
     },
-    ProcessRemoteRevision {
+    OverrideDelta {
+        delta: RichTextDelta,
+        ret: Ret<DocumentMD5>,
+    },
+    TransformRevision {
         revisions: Vec<Revision>,
         ret: Ret<TransformDeltas>,
     },
@@ -212,5 +226,5 @@ pub(crate) enum EditorCommand {
 
 pub(crate) struct TransformDeltas {
     pub client_prime: RichTextDelta,
-    pub server_prime: RichTextDelta,
+    pub server_prime: Option<RichTextDelta>,
 }

+ 12 - 14
frontend/rust-lib/flowy-document/src/services/doc/revision/cache/cache.rs

@@ -6,6 +6,7 @@ use crate::{
     },
     sql_tables::{RevisionChangeset, RevisionTableState},
 };
+use std::borrow::Cow;
 
 use flowy_collaboration::entities::revision::{Revision, RevisionRange, RevisionState};
 use flowy_database::ConnectionPool;
@@ -46,13 +47,14 @@ impl RevisionCache {
         if self.memory_cache.contains(&revision.rev_id) {
             return Err(FlowyError::internal().context(format!("Duplicate remote revision id: {}", revision.rev_id)));
         }
+        let state = state.as_ref().clone();
         let rev_id = revision.rev_id;
         let record = RevisionRecord {
             revision,
             state,
             write_to_disk,
         };
-        self.memory_cache.add(&record).await;
+        self.memory_cache.add(Cow::Borrowed(&record)).await;
         self.set_latest_rev_id(rev_id);
         Ok(record)
     }
@@ -63,10 +65,9 @@ impl RevisionCache {
         match self.memory_cache.get(&rev_id).await {
             None => match self.disk_cache.read_revision_records(&self.doc_id, Some(vec![rev_id])) {
                 Ok(mut records) => {
-                    if records.is_empty() {
-                        tracing::warn!("Can't find revision in {} with rev_id: {}", &self.doc_id, rev_id);
+                    if !records.is_empty() {
+                        assert_eq!(records.len(), 1);
                     }
-                    assert_eq!(records.len(), 1);
                     records.pop()
                 },
                 Err(e) => {
@@ -108,23 +109,20 @@ impl RevisionCache {
     }
 
     #[tracing::instrument(level = "debug", skip(self, doc_id, revisions))]
-    pub fn reset_document(&self, doc_id: &str, revisions: Vec<Revision>) -> FlowyResult<()> {
-        let disk_cache = self.disk_cache.clone();
-        let conn = disk_cache.db_pool().get().map_err(internal_error)?;
-        let records = revisions
+    pub async fn reset_document(&self, doc_id: &str, revisions: Vec<Revision>) -> FlowyResult<()> {
+        let revision_records = revisions
+            .to_vec()
             .into_iter()
             .map(|revision| RevisionRecord {
                 revision,
                 state: RevisionState::Local,
-                write_to_disk: true,
+                write_to_disk: false,
             })
             .collect::<Vec<_>>();
 
-        conn.immediate_transaction::<_, FlowyError, _>(|| {
-            let _ = disk_cache.delete_revision_records(doc_id, None, &*conn)?;
-            let _ = disk_cache.write_revision_records(records, &*conn)?;
-            Ok(())
-        })
+        let _ = self.memory_cache.reset_with_revisions(&revision_records).await?;
+        let _ = self.disk_cache.reset_with_revisions(doc_id, revision_records)?;
+        Ok(())
     }
 
     #[inline]

+ 11 - 0
frontend/rust-lib/flowy-document/src/services/doc/revision/cache/disk.rs

@@ -38,6 +38,8 @@ pub trait RevisionDiskCache: Sync + Send {
         conn: &SqliteConnection,
     ) -> Result<(), Self::Error>;
 
+    fn reset_with_revisions(&self, doc_id: &str, revision_records: Vec<RevisionRecord>) -> Result<(), Self::Error>;
+
     fn db_pool(&self) -> Arc<ConnectionPool>;
 }
 
@@ -99,6 +101,15 @@ impl RevisionDiskCache for Persistence {
         Ok(())
     }
 
+    fn reset_with_revisions(&self, doc_id: &str, revision_records: Vec<RevisionRecord>) -> Result<(), Self::Error> {
+        let conn = self.db_pool().get().map_err(internal_error)?;
+        conn.immediate_transaction::<_, FlowyError, _>(|| {
+            let _ = self.delete_revision_records(doc_id, None, &*conn)?;
+            let _ = self.write_revision_records(revision_records, &*conn)?;
+            Ok(())
+        })
+    }
+
     fn db_pool(&self) -> Arc<ConnectionPool> { self.pool.clone() }
 }
 

+ 23 - 2
frontend/rust-lib/flowy-document/src/services/doc/revision/cache/memory.rs

@@ -2,7 +2,8 @@ use crate::services::doc::RevisionRecord;
 use dashmap::DashMap;
 use flowy_collaboration::entities::revision::RevisionRange;
 use flowy_error::{FlowyError, FlowyResult};
-use std::{sync::Arc, time::Duration};
+use futures_util::{stream, stream::StreamExt};
+use std::{borrow::Cow, sync::Arc, time::Duration};
 use tokio::{sync::RwLock, task::JoinHandle};
 
 pub(crate) trait RevisionMemoryCacheDelegate: Send + Sync {
@@ -31,7 +32,12 @@ impl RevisionMemoryCache {
 
     pub(crate) fn contains(&self, rev_id: &i64) -> bool { self.revs_map.contains_key(rev_id) }
 
-    pub(crate) async fn add(&self, record: &RevisionRecord) {
+    pub(crate) async fn add<'a>(&'a self, record: Cow<'a, RevisionRecord>) {
+        let record = match record {
+            Cow::Borrowed(record) => record.clone(),
+            Cow::Owned(record) => record,
+        };
+
         if let Some(rev_id) = self.pending_write_revs.read().await.last() {
             if *rev_id >= record.revision.rev_id {
                 tracing::error!("Duplicated revision added to memory_cache");
@@ -71,6 +77,21 @@ impl RevisionMemoryCache {
         Ok(revs)
     }
 
+    pub(crate) async fn reset_with_revisions(&self, revision_records: &[RevisionRecord]) -> FlowyResult<()> {
+        self.revs_map.clear();
+        self.pending_write_revs.write().await.clear();
+        if let Some(handler) = self.defer_save.write().await.take() {
+            handler.abort();
+        }
+        stream::iter(revision_records)
+            .for_each(|record| async move {
+                self.add(Cow::Borrowed(record)).await;
+            })
+            .await;
+
+        Ok(())
+    }
+
     async fn make_checkpoint(&self) {
         // https://github.com/async-graphql/async-graphql/blob/ed8449beec3d9c54b94da39bab33cec809903953/src/dataloader/mod.rs#L362
         if let Some(handler) = self.defer_save.write().await.take() {

+ 26 - 27
frontend/rust-lib/flowy-document/src/services/doc/revision/manager.rs

@@ -7,11 +7,13 @@ use dashmap::DashMap;
 use flowy_collaboration::{
     entities::{
         doc::DocumentInfo,
+        prelude::pair_rev_id_from_revisions,
         revision::{RepeatedRevision, Revision, RevisionRange, RevisionState},
     },
     util::{md5, RevIdCounter},
 };
 use flowy_error::FlowyResult;
+use futures_util::{future, stream, stream::StreamExt};
 use lib_infra::future::FutureResult;
 use lib_ot::{
     core::{Operation, OperationTransformable},
@@ -30,13 +32,13 @@ pub struct RevisionManager {
     user_id: String,
     rev_id_counter: RevIdCounter,
     cache: Arc<RevisionCache>,
-    sync_seq: Arc<RevisionSyncSeq>,
+    sync_seq: Arc<RevisionSyncSequence>,
 }
 
 impl RevisionManager {
     pub fn new(user_id: &str, doc_id: &str, cache: Arc<RevisionCache>) -> Self {
         let rev_id_counter = RevIdCounter::new(0);
-        let sync_seq = Arc::new(RevisionSyncSeq::new());
+        let sync_seq = Arc::new(RevisionSyncSequence::new());
         Self {
             doc_id: doc_id.to_string(),
             user_id: user_id.to_owned(),
@@ -62,10 +64,13 @@ impl RevisionManager {
 
     #[tracing::instrument(level = "debug", skip(self, revisions), err)]
     pub async fn reset_document(&self, revisions: RepeatedRevision) -> FlowyResult<()> {
-        self.cache.reset_document(&self.doc_id, revisions.into_inner())
+        let rev_id = pair_rev_id_from_revisions(&revisions).1;
+        let _ = self.cache.reset_document(&self.doc_id, revisions.into_inner()).await?;
+        self.rev_id_counter.set(rev_id);
+        Ok(())
     }
 
-    #[tracing::instrument(level = "debug", skip(self, revision))]
+    #[tracing::instrument(level = "debug", skip(self, revision), err)]
     pub async fn add_remote_revision(&self, revision: &Revision) -> Result<(), FlowyError> {
         if revision.delta_data.is_empty() {
             return Err(FlowyError::internal().context("Delta data should be empty"));
@@ -98,7 +103,7 @@ impl RevisionManager {
 
     pub fn set_rev_id(&self, rev_id: i64) { self.rev_id_counter.set(rev_id); }
 
-    pub fn next_rev_id(&self) -> (i64, i64) {
+    pub fn next_rev_id_pair(&self) -> (i64, i64) {
         let cur = self.rev_id_counter.value();
         let next = self.rev_id_counter.next();
         (cur, next)
@@ -131,23 +136,23 @@ impl RevisionManager {
     }
 }
 
-struct RevisionSyncSeq {
+struct RevisionSyncSequence {
     revs_map: Arc<DashMap<i64, RevisionRecord>>,
     local_revs: Arc<RwLock<VecDeque<i64>>>,
 }
 
-impl std::default::Default for RevisionSyncSeq {
+impl std::default::Default for RevisionSyncSequence {
     fn default() -> Self {
         let local_revs = Arc::new(RwLock::new(VecDeque::new()));
-        RevisionSyncSeq {
+        RevisionSyncSequence {
             revs_map: Arc::new(DashMap::new()),
             local_revs,
         }
     }
 }
 
-impl RevisionSyncSeq {
-    fn new() -> Self { RevisionSyncSeq::default() }
+impl RevisionSyncSequence {
+    fn new() -> Self { RevisionSyncSequence::default() }
 
     async fn add_revision(&self, record: RevisionRecord) -> Result<(), OTError> {
         // The last revision's rev_id must be greater than the new one.
@@ -216,22 +221,16 @@ impl RevisionLoader {
             let _ = self.cache.add(revision.clone(), RevisionState::Ack, true).await?;
             revisions = vec![revision];
         } else {
-            for record in &records {
-                match record.state {
-                    RevisionState::Local => {
-                        //
-                        match self
-                            .cache
-                            .add(record.revision.clone(), RevisionState::Local, false)
-                            .await
-                        {
-                            Ok(_) => {},
-                            Err(e) => tracing::error!("{}", e),
-                        }
-                    },
-                    RevisionState::Ack => {},
-                }
-            }
+            // Sync the records if their state is RevisionState::Local.
+            stream::iter(records.clone())
+                .filter(|record| future::ready(record.state == RevisionState::Local))
+                .for_each(|record| async move {
+                    match self.cache.add(record.revision, record.state, false).await {
+                        Ok(_) => {},
+                        Err(e) => tracing::error!("{}", e),
+                    }
+                })
+                .await;
             revisions = records.into_iter().map(|record| record.revision).collect::<_>();
         }
 
@@ -274,7 +273,7 @@ fn correct_delta_if_need(delta: &mut RichTextDelta) {
 }
 
 #[cfg(feature = "flowy_unit_test")]
-impl RevisionSyncSeq {
+impl RevisionSyncSequence {
     #[allow(dead_code)]
     pub fn revs_map(&self) -> Arc<DashMap<i64, RevisionRecord>> { self.revs_map.clone() }
     #[allow(dead_code)]

+ 6 - 2
frontend/rust-lib/flowy-document/src/services/doc/web_socket/http_ws_impl.rs

@@ -94,7 +94,7 @@ impl DocumentWSReceiver for HttpWebSocketManager {
     fn receive_ws_data(&self, doc_data: DocumentServerWSData) {
         match self.ws_msg_tx.send(doc_data) {
             Ok(_) => {},
-            Err(e) => tracing::error!("❌Propagate ws message failed. {}", e),
+            Err(e) => tracing::error!("❌ Propagate ws message failed. {}", e),
         }
     }
 
@@ -106,6 +106,10 @@ impl DocumentWSReceiver for HttpWebSocketManager {
     }
 }
 
+impl std::ops::Drop for HttpWebSocketManager {
+    fn drop(&mut self) { tracing::debug!("{} HttpWebSocketManager was drop", self.doc_id) }
+}
+
 pub trait DocumentWSSteamConsumer: Send + Sync {
     fn receive_push_revision(&self, bytes: Bytes) -> FutureResult<(), FlowyError>;
     fn receive_ack(&self, id: String, ty: DocumentServerWSDataType) -> FutureResult<(), FlowyError>;
@@ -177,7 +181,7 @@ impl DocumentWSStream {
             .await
             .map_err(internal_error)?;
 
-        tracing::debug!("[DocumentStream]: receives new message: {:?}", ty);
+        tracing::debug!("[DocumentStream]: new message: {:?}", ty);
         match ty {
             DocumentServerWSDataType::ServerPushRev => {
                 let _ = self.consumer.receive_push_revision(bytes).await?;

+ 102 - 46
frontend/rust-lib/flowy-document/src/services/doc/web_socket/web_socket.rs

@@ -19,7 +19,8 @@ use flowy_error::{internal_error, FlowyError, FlowyResult};
 use lib_infra::future::FutureResult;
 
 use crate::services::doc::web_socket::local_ws_impl::LocalWebSocketManager;
-use flowy_collaboration::entities::ws::DocumentServerWSDataType;
+use flowy_collaboration::entities::{revision::pair_rev_id_from_revisions, ws::DocumentServerWSDataType};
+use lib_ot::rich_text::RichTextDelta;
 use lib_ws::WSConnectState;
 use std::{collections::VecDeque, convert::TryFrom, sync::Arc};
 use tokio::sync::{broadcast, mpsc::UnboundedSender, oneshot, RwLock};
@@ -162,6 +163,68 @@ impl DocumentWSSinkDataProvider for DocumentWSSinkDataProviderAdapter {
     }
 }
 
+async fn transform_pushed_revisions(
+    revisions: &[Revision],
+    edit_cmd: &UnboundedSender<EditorCommand>,
+) -> FlowyResult<TransformDeltas> {
+    let (ret, rx) = oneshot::channel::<CollaborateResult<TransformDeltas>>();
+    // Transform the revision
+    let _ = edit_cmd.send(EditorCommand::TransformRevision {
+        revisions: revisions.to_vec(),
+        ret,
+    });
+    let transformed_delta = rx.await.map_err(internal_error)??;
+    Ok(transformed_delta)
+}
+
+async fn compose_pushed_delta(
+    delta: RichTextDelta,
+    edit_cmd: &UnboundedSender<EditorCommand>,
+) -> FlowyResult<DocumentMD5> {
+    // compose delta
+    let (ret, rx) = oneshot::channel::<CollaborateResult<DocumentMD5>>();
+    let _ = edit_cmd.send(EditorCommand::ComposeDelta { delta, ret });
+    let md5 = rx.await.map_err(internal_error)??;
+    Ok(md5)
+}
+
+async fn override_client_delta(
+    delta: RichTextDelta,
+    edit_cmd: &UnboundedSender<EditorCommand>,
+) -> FlowyResult<DocumentMD5> {
+    let (ret, rx) = oneshot::channel::<CollaborateResult<DocumentMD5>>();
+    let _ = edit_cmd.send(EditorCommand::OverrideDelta { delta, ret });
+    let md5 = rx.await.map_err(internal_error)??;
+    Ok(md5)
+}
+
+async fn make_client_and_server_revision(
+    doc_id: &str,
+    user_id: &str,
+    base_rev_id: i64,
+    rev_id: i64,
+    client_delta: RichTextDelta,
+    server_delta: Option<RichTextDelta>,
+    md5: DocumentMD5,
+) -> (Revision, Option<Revision>) {
+    let client_revision = Revision::new(
+        &doc_id,
+        base_rev_id,
+        rev_id,
+        client_delta.to_bytes(),
+        &user_id,
+        md5.clone(),
+    );
+
+    match server_delta {
+        None => (client_revision, None),
+        Some(server_delta) => {
+            let server_revision = Revision::new(&doc_id, base_rev_id, rev_id, server_delta.to_bytes(), &user_id, md5);
+            (client_revision, Some(server_revision))
+        },
+    }
+}
+
 #[tracing::instrument(level = "debug", skip(edit_cmd_tx, rev_manager, bytes))]
 pub(crate) async fn handle_push_rev(
     doc_id: &str,
@@ -170,67 +233,60 @@ pub(crate) async fn handle_push_rev(
     rev_manager: Arc<RevisionManager>,
     bytes: Bytes,
 ) -> FlowyResult<Option<Revision>> {
-    // Transform the revision
-    let (ret, rx) = oneshot::channel::<CollaborateResult<TransformDeltas>>();
     let mut revisions = RepeatedRevision::try_from(bytes)?.into_inner();
     if revisions.is_empty() {
         return Ok(None);
     }
+
     let first_revision = revisions.first().unwrap();
     if let Some(local_revision) = rev_manager.get_revision(first_revision.rev_id).await {
-        if local_revision.md5 != first_revision.md5 {
+        if local_revision.md5 == first_revision.md5 {
             // The local revision is equal to the pushed revision. Just ignore it.
+            revisions = revisions.split_off(1);
+            if revisions.is_empty() {
+                return Ok(None);
+            }
+        } else {
             return Ok(None);
         }
     }
 
-    let revisions = revisions.split_off(1);
-    if revisions.is_empty() {
-        return Ok(None);
-    }
-
-    let _ = edit_cmd_tx.send(EditorCommand::ProcessRemoteRevision {
-        revisions: revisions.clone(),
-        ret,
-    });
     let TransformDeltas {
         client_prime,
         server_prime,
-    } = rx.await.map_err(internal_error)??;
+    } = transform_pushed_revisions(&revisions, &edit_cmd_tx).await?;
+    match server_prime {
+        None => {
+            // The server_prime is None means the client local revisions conflict with the
+            // server, and it needs to override the client delta.
+            let md5 = override_client_delta(client_prime.clone(), &edit_cmd_tx).await?;
+            let repeated_revision = RepeatedRevision::new(revisions);
+            assert_eq!(repeated_revision.last().unwrap().md5, md5);
+            let _ = rev_manager.reset_document(repeated_revision).await?;
+            Ok(None)
+        },
+        Some(server_prime) => {
+            let md5 = compose_pushed_delta(client_prime.clone(), &edit_cmd_tx).await?;
+            for revision in &revisions {
+                let _ = rev_manager.add_remote_revision(revision).await?;
+            }
+            let (base_rev_id, rev_id) = rev_manager.next_rev_id_pair();
+            let (client_revision, server_revision) = make_client_and_server_revision(
+                doc_id,
+                user_id,
+                base_rev_id,
+                rev_id,
+                client_prime,
+                Some(server_prime),
+                md5,
+            )
+            .await;
 
-    for revision in &revisions {
-        let _ = rev_manager.add_remote_revision(revision).await?;
+            // save the client revision
+            let _ = rev_manager.add_remote_revision(&client_revision).await?;
+            Ok(server_revision)
+        },
     }
-
-    // compose delta
-    let (ret, rx) = oneshot::channel::<CollaborateResult<DocumentMD5>>();
-    let _ = edit_cmd_tx.send(EditorCommand::ComposeDelta {
-        delta: client_prime.clone(),
-        ret,
-    });
-    let md5 = rx.await.map_err(internal_error)??;
-    let (local_base_rev_id, local_rev_id) = rev_manager.next_rev_id();
-
-    // save the revision
-    let revision = Revision::new(
-        &doc_id,
-        local_base_rev_id,
-        local_rev_id,
-        client_prime.to_bytes(),
-        &user_id,
-        md5.clone(),
-    );
-    let _ = rev_manager.add_remote_revision(&revision).await?;
-
-    // send the server_prime delta
-    Ok(Some(Revision::new(
-        &doc_id,
-        local_base_rev_id,
-        local_rev_id,
-        server_prime.to_bytes(),
-        &user_id,
-        md5,
-    )))
 }
 
 #[derive(Clone)]

+ 0 - 116
frontend/rust-lib/flowy-document/src/services/file/file.rs

@@ -1,116 +0,0 @@
-use std::{
-    ffi::OsString,
-    fs,
-    fs::File,
-    io,
-    io::{Read, Write},
-    path::{Path, PathBuf},
-    str,
-    time::SystemTime,
-};
-
-#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
-pub struct FileId(pub(crate) String);
-
-impl std::convert::From<String> for FileId {
-    fn from(s: String) -> Self { FileId(s) }
-}
-
-#[derive(Debug, Clone, Copy)]
-pub enum CharacterEncoding {
-    Utf8,
-    Utf8WithBom,
-}
-
-const UTF8_BOM: &str = "\u{feff}";
-impl CharacterEncoding {
-    pub(crate) fn guess(s: &[u8]) -> Self {
-        if s.starts_with(UTF8_BOM.as_bytes()) {
-            CharacterEncoding::Utf8WithBom
-        } else {
-            CharacterEncoding::Utf8
-        }
-    }
-}
-
-#[derive(Debug)]
-pub enum FileError {
-    Io(io::Error, PathBuf),
-    UnknownEncoding(PathBuf),
-    HasChanged(PathBuf),
-}
-
-#[derive(Clone, Debug)]
-pub struct FileInfo {
-    pub path: PathBuf,
-    pub modified_time: Option<SystemTime>,
-    pub has_changed: bool,
-    pub encoding: CharacterEncoding,
-}
-
-#[allow(dead_code)]
-pub(crate) fn try_load_file<P>(path: P) -> Result<(String, FileInfo), FileError>
-where
-    P: AsRef<Path>,
-{
-    let mut f = File::open(path.as_ref()).map_err(|e| FileError::Io(e, path.as_ref().to_owned()))?;
-    let mut bytes = Vec::new();
-    f.read_to_end(&mut bytes).map_err(|e| FileError::Io(e, path.as_ref().to_owned()))?;
-
-    let encoding = CharacterEncoding::guess(&bytes);
-    let s = try_decode(bytes, encoding, path.as_ref())?;
-    let info = FileInfo {
-        encoding,
-        path: path.as_ref().to_owned(),
-        modified_time: get_modified_time(&path),
-        has_changed: false,
-    };
-    Ok((s, info))
-}
-
-#[allow(dead_code)]
-pub(crate) fn try_save(path: &Path, text: &str, encoding: CharacterEncoding, _file_info: Option<&FileInfo>) -> io::Result<()> {
-    let tmp_extension = path.extension().map_or_else(
-        || OsString::from("swp"),
-        |ext| {
-            let mut ext = ext.to_os_string();
-            ext.push(".swp");
-            ext
-        },
-    );
-    let tmp_path = &path.with_extension(tmp_extension);
-
-    let mut f = File::create(tmp_path)?;
-    match encoding {
-        CharacterEncoding::Utf8WithBom => f.write_all(UTF8_BOM.as_bytes())?,
-        CharacterEncoding::Utf8 => (),
-    }
-
-    f.write_all(text.as_bytes())?;
-    fs::rename(tmp_path, path)?;
-
-    Ok(())
-}
-
-#[allow(dead_code)]
-pub(crate) fn try_decode(bytes: Vec<u8>, encoding: CharacterEncoding, path: &Path) -> Result<String, FileError> {
-    match encoding {
-        CharacterEncoding::Utf8 => Ok(String::from(
-            str::from_utf8(&bytes).map_err(|_e| FileError::UnknownEncoding(path.to_owned()))?,
-        )),
-        CharacterEncoding::Utf8WithBom => {
-            let s = String::from_utf8(bytes).map_err(|_e| FileError::UnknownEncoding(path.to_owned()))?;
-            Ok(String::from(&s[UTF8_BOM.len()..]))
-        },
-    }
-}
-
-#[allow(dead_code)]
-pub(crate) fn create_dir_if_not_exist(dir: &str) -> Result<(), io::Error> {
-    let _ = fs::create_dir_all(dir)?;
-    Ok(())
-}
-
-pub(crate) fn get_modified_time<P: AsRef<Path>>(path: P) -> Option<SystemTime> {
-    File::open(path).and_then(|f| f.metadata()).and_then(|meta| meta.modified()).ok()
-}

+ 0 - 120
frontend/rust-lib/flowy-document/src/services/file/manager.rs

@@ -1,120 +0,0 @@
-use crate::{module::DocumentUser, services::file_manager::*};
-use std::{
-    collections::HashMap,
-    path::{Path, PathBuf},
-    sync::Arc,
-};
-
-pub struct FileManager {
-    pub user: Arc<dyn DocumentUser>,
-    open_files: HashMap<PathBuf, FileId>,
-    file_info: HashMap<FileId, FileInfo>,
-}
-
-impl FileManager {
-    pub(crate) fn new(user: Arc<dyn DocumentUser>) -> Self {
-        Self {
-            user,
-            open_files: HashMap::new(),
-            file_info: HashMap::new(),
-        }
-    }
-
-    #[allow(dead_code)]
-    pub(crate) fn open<T>(&mut self, path: &Path, id: T) -> Result<String, FileError>
-    where
-        T: Into<FileId>,
-    {
-        if !path.exists() {
-            return Ok("".to_string());
-        }
-        let file_id = id.into();
-        let (s, info) = try_load_file(path)?;
-        self.open_files.insert(path.to_owned(), file_id.clone());
-        self.file_info.insert(file_id, info);
-
-        Ok(s)
-    }
-
-    #[allow(dead_code)]
-    pub(crate) fn save<T>(&mut self, path: &Path, text: &String, id: T) -> Result<(), FileError>
-    where
-        T: Into<FileId>,
-    {
-        let file_id = id.into();
-        let is_existing = self.file_info.contains_key(&file_id);
-        if is_existing {
-            self.save_existing(path, text, &file_id)
-        } else {
-            self.save_new(path, text, &file_id)
-        }
-    }
-
-    #[allow(dead_code)]
-    pub(crate) fn close<T>(&mut self, id: T)
-    where
-        T: Into<FileId>,
-    {
-        if let Some(file_info) = self.file_info.remove(&id.into()) {
-            self.open_files.remove(&file_info.path);
-        }
-    }
-
-    #[allow(dead_code)]
-    pub(crate) fn create_file(&mut self, id: &str, dir: &str, text: &str) -> Result<PathBuf, FileError> {
-        let path = PathBuf::from(format!("{}/{}", dir, id));
-        let file_id: FileId = id.to_owned().into();
-        tracing::info!("Create document at: {:?}", path);
-        let _ = self.save_new(&path, text, &file_id)?;
-        Ok(path)
-    }
-
-    #[allow(dead_code)]
-    pub(crate) fn get_info(&self, id: &FileId) -> Option<&FileInfo> { self.file_info.get(id) }
-
-    #[allow(dead_code)]
-    pub(crate) fn get_file_id(&self, path: &Path) -> Option<FileId> { self.open_files.get(path).cloned() }
-
-    #[allow(dead_code)]
-    pub fn check_file(&mut self, path: &Path, id: &FileId) -> bool {
-        if let Some(info) = self.file_info.get_mut(&id) {
-            let modified_time = get_modified_time(path);
-            if modified_time != info.modified_time {
-                info.has_changed = true
-            }
-            return info.has_changed;
-        }
-        false
-    }
-
-    #[allow(dead_code)]
-    fn save_new(&mut self, path: &Path, text: &str, id: &FileId) -> Result<(), FileError> {
-        try_save(path, text, CharacterEncoding::Utf8, self.get_info(id))
-            .map_err(|e| FileError::Io(e, path.to_owned()))?;
-        let info = FileInfo {
-            encoding: CharacterEncoding::Utf8,
-            path: path.to_owned(),
-            modified_time: get_modified_time(path),
-            has_changed: false,
-        };
-        self.open_files.insert(path.to_owned(), id.clone());
-        self.file_info.insert(id.clone(), info);
-        Ok(())
-    }
-
-    #[allow(dead_code)]
-    fn save_existing(&mut self, path: &Path, text: &String, id: &FileId) -> Result<(), FileError> {
-        let prev_path = self.file_info[id].path.clone();
-        if prev_path != path {
-            self.save_new(path, text, id)?;
-            self.open_files.remove(&prev_path);
-        } else if self.file_info[&id].has_changed {
-            return Err(FileError::HasChanged(path.to_owned()));
-        } else {
-            let encoding = self.file_info[&id].encoding;
-            try_save(path, text, encoding, self.get_info(id)).map_err(|e| FileError::Io(e, path.to_owned()))?;
-            self.file_info.get_mut(&id).unwrap().modified_time = get_modified_time(path);
-        }
-        Ok(())
-    }
-}

+ 0 - 5
frontend/rust-lib/flowy-document/src/services/file/mod.rs

@@ -1,5 +0,0 @@
-mod file;
-mod manager;
-
-pub use file::*;
-pub use manager::*;

+ 25 - 25
frontend/rust-lib/flowy-document/tests/editor/attribute_test.rs

@@ -1,6 +1,6 @@
 #![cfg_attr(rustfmt, rustfmt::skip)]
 use crate::editor::{TestBuilder, TestOp::*};
-use flowy_collaboration::document::{FlowyDoc, PlainDoc};
+use flowy_collaboration::document::{NewlineDoc, PlainDoc};
 use lib_ot::core::{Interval, OperationTransformable, NEW_LINE, WHITESPACE, FlowyStr};
 use unicode_segmentation::UnicodeSegmentation;
 use lib_ot::rich_text::RichTextDelta;
@@ -85,7 +85,7 @@ fn attributes_bold_added_with_new_line() {
             r#"[{"insert":"123","attributes":{"bold":"true"}},{"insert":"\na\n"},{"insert":"456","attributes":{"bold":"true"}},{"insert":"\n"}]"#,
         ),
     ];
-    TestBuilder::new().run_scripts::<FlowyDoc>(ops);
+    TestBuilder::new().run_scripts::<NewlineDoc>(ops);
 }
 
 #[test]
@@ -128,7 +128,7 @@ fn attributes_bold_added_italic() {
             r#"[{"insert":"12345678","attributes":{"bold":"true","italic":"true"}},{"insert":"\n"}]"#,
         ),
     ];
-    TestBuilder::new().run_scripts::<FlowyDoc>(ops);
+    TestBuilder::new().run_scripts::<NewlineDoc>(ops);
 }
 
 #[test]
@@ -390,7 +390,7 @@ fn attributes_header_insert_newline_at_middle() {
         ),
     ];
 
-    TestBuilder::new().run_scripts::<FlowyDoc>(ops);
+    TestBuilder::new().run_scripts::<NewlineDoc>(ops);
 }
 
 #[test]
@@ -415,7 +415,7 @@ fn attributes_header_insert_double_newline_at_middle() {
         ),
     ];
 
-    TestBuilder::new().run_scripts::<FlowyDoc>(ops);
+    TestBuilder::new().run_scripts::<NewlineDoc>(ops);
 }
 
 #[test]
@@ -430,7 +430,7 @@ fn attributes_header_insert_newline_at_trailing() {
         ),
     ];
 
-    TestBuilder::new().run_scripts::<FlowyDoc>(ops);
+    TestBuilder::new().run_scripts::<NewlineDoc>(ops);
 }
 
 #[test]
@@ -446,7 +446,7 @@ fn attributes_header_insert_double_newline_at_trailing() {
         ),
     ];
 
-    TestBuilder::new().run_scripts::<FlowyDoc>(ops);
+    TestBuilder::new().run_scripts::<NewlineDoc>(ops);
 }
 
 #[test]
@@ -460,7 +460,7 @@ fn attributes_link_added() {
         ),
     ];
 
-    TestBuilder::new().run_scripts::<FlowyDoc>(ops);
+    TestBuilder::new().run_scripts::<NewlineDoc>(ops);
 }
 
 #[test]
@@ -479,7 +479,7 @@ fn attributes_link_format_with_bold() {
         ),
     ];
 
-    TestBuilder::new().run_scripts::<FlowyDoc>(ops);
+    TestBuilder::new().run_scripts::<NewlineDoc>(ops);
 }
 
 #[test]
@@ -498,7 +498,7 @@ fn attributes_link_insert_char_at_head() {
         ),
     ];
 
-    TestBuilder::new().run_scripts::<FlowyDoc>(ops);
+    TestBuilder::new().run_scripts::<NewlineDoc>(ops);
 }
 
 #[test]
@@ -513,7 +513,7 @@ fn attributes_link_insert_char_at_middle() {
         ),
     ];
 
-    TestBuilder::new().run_scripts::<FlowyDoc>(ops);
+    TestBuilder::new().run_scripts::<NewlineDoc>(ops);
 }
 
 #[test]
@@ -532,7 +532,7 @@ fn attributes_link_insert_char_at_trailing() {
         ),
     ];
 
-    TestBuilder::new().run_scripts::<FlowyDoc>(ops);
+    TestBuilder::new().run_scripts::<NewlineDoc>(ops);
 }
 
 #[test]
@@ -547,7 +547,7 @@ fn attributes_link_insert_newline_at_middle() {
         ),
     ];
 
-    TestBuilder::new().run_scripts::<FlowyDoc>(ops);
+    TestBuilder::new().run_scripts::<NewlineDoc>(ops);
 }
 
 #[test]
@@ -563,7 +563,7 @@ fn attributes_link_auto_format() {
         ),
     ];
 
-    TestBuilder::new().run_scripts::<FlowyDoc>(ops);
+    TestBuilder::new().run_scripts::<NewlineDoc>(ops);
 }
 
 #[test]
@@ -579,7 +579,7 @@ fn attributes_link_auto_format_exist() {
         ),
     ];
 
-    TestBuilder::new().run_scripts::<FlowyDoc>(ops);
+    TestBuilder::new().run_scripts::<NewlineDoc>(ops);
 }
 
 #[test]
@@ -595,7 +595,7 @@ fn attributes_link_auto_format_exist2() {
         ),
     ];
 
-    TestBuilder::new().run_scripts::<FlowyDoc>(ops);
+    TestBuilder::new().run_scripts::<NewlineDoc>(ops);
 }
 
 #[test]
@@ -606,7 +606,7 @@ fn attributes_bullet_added() {
         AssertDocJson(0, r#"[{"insert":"12"},{"insert":"\n","attributes":{"list":"bullet"}}]"#),
     ];
 
-    TestBuilder::new().run_scripts::<FlowyDoc>(ops);
+    TestBuilder::new().run_scripts::<NewlineDoc>(ops);
 }
 
 #[test]
@@ -627,7 +627,7 @@ fn attributes_bullet_added_2() {
         ),
     ];
 
-    TestBuilder::new().run_scripts::<FlowyDoc>(ops);
+    TestBuilder::new().run_scripts::<NewlineDoc>(ops);
 }
 
 #[test]
@@ -644,7 +644,7 @@ fn attributes_bullet_remove_partial() {
         ),
     ];
 
-    TestBuilder::new().run_scripts::<FlowyDoc>(ops);
+    TestBuilder::new().run_scripts::<NewlineDoc>(ops);
 }
 
 #[test]
@@ -660,7 +660,7 @@ fn attributes_bullet_auto_exit() {
         ),
     ];
 
-    TestBuilder::new().run_scripts::<FlowyDoc>(ops);
+    TestBuilder::new().run_scripts::<NewlineDoc>(ops);
 }
 
 #[test]
@@ -700,7 +700,7 @@ fn attributes_preserve_block_when_insert_newline_inside() {
         ),
     ];
 
-    TestBuilder::new().run_scripts::<FlowyDoc>(ops);
+    TestBuilder::new().run_scripts::<NewlineDoc>(ops);
 }
 
 #[test]
@@ -717,7 +717,7 @@ fn attributes_preserve_header_format_on_merge() {
         AssertDocJson(0, r#"[{"insert":"123456"},{"insert":"\n","attributes":{"header":1}}]"#),
     ];
 
-    TestBuilder::new().run_scripts::<FlowyDoc>(ops);
+    TestBuilder::new().run_scripts::<NewlineDoc>(ops);
 }
 
 #[test]
@@ -736,7 +736,7 @@ fn attributes_format_emoji() {
             r#"[{"insert":"👋 "},{"insert":"\n","attributes":{"header":1}}]"#,
         ),
     ];
-    TestBuilder::new().run_scripts::<FlowyDoc>(ops);
+    TestBuilder::new().run_scripts::<NewlineDoc>(ops);
 }
 
 #[test]
@@ -756,7 +756,7 @@ fn attributes_preserve_list_format_on_merge() {
         ),
     ];
 
-    TestBuilder::new().run_scripts::<FlowyDoc>(ops);
+    TestBuilder::new().run_scripts::<NewlineDoc>(ops);
 }
 
 #[test]
@@ -795,5 +795,5 @@ fn delta_compose() {
         ),
     ];
 
-    TestBuilder::new().run_scripts::<FlowyDoc>(ops);
+    TestBuilder::new().run_scripts::<NewlineDoc>(ops);
 }

+ 2 - 2
frontend/rust-lib/flowy-document/tests/editor/mod.rs

@@ -5,7 +5,7 @@ mod serde_test;
 mod undo_redo_test;
 
 use derive_more::Display;
-use flowy_collaboration::document::{CustomDocument, Document};
+use flowy_collaboration::document::{Document, InitialDocumentText};
 use lib_ot::{
     core::*,
     rich_text::{RichTextAttribute, RichTextAttributes, RichTextDelta},
@@ -266,7 +266,7 @@ impl TestBuilder {
         }
     }
 
-    pub fn run_scripts<C: CustomDocument>(mut self, scripts: Vec<TestOp>) {
+    pub fn run_scripts<C: InitialDocumentText>(mut self, scripts: Vec<TestOp>) {
         self.documents = vec![Document::new::<C>(), Document::new::<C>()];
         self.primes = vec![None, None];
         self.deltas = vec![None, None];

+ 2 - 2
frontend/rust-lib/flowy-document/tests/editor/op_test.rs

@@ -1,6 +1,6 @@
 #![allow(clippy::all)]
 use crate::editor::{Rng, TestBuilder, TestOp::*};
-use flowy_collaboration::document::{FlowyDoc, PlainDoc};
+use flowy_collaboration::document::{NewlineDoc, PlainDoc};
 use lib_ot::{
     core::*,
     rich_text::{AttributeBuilder, RichTextAttribute, RichTextAttributes, RichTextDelta},
@@ -731,5 +731,5 @@ fn delta_compose_with_missing_delta() {
         AssertDocJson(0, r#"[{"insert":"1234\n"}]"#),
         AssertStr(1, r#"4\n"#),
     ];
-    TestBuilder::new().run_scripts::<FlowyDoc>(ops);
+    TestBuilder::new().run_scripts::<NewlineDoc>(ops);
 }

+ 21 - 21
frontend/rust-lib/flowy-document/tests/editor/undo_redo_test.rs

@@ -1,11 +1,11 @@
 use crate::editor::{TestBuilder, TestOp::*};
-use flowy_collaboration::document::{FlowyDoc, PlainDoc, RECORD_THRESHOLD};
+use flowy_collaboration::document::{NewlineDoc, PlainDoc, RECORD_THRESHOLD};
 use lib_ot::core::{Interval, NEW_LINE, WHITESPACE};
 
 #[test]
 fn history_insert_undo() {
     let ops = vec![Insert(0, "123", 0), Undo(0), AssertDocJson(0, r#"[{"insert":"\n"}]"#)];
-    TestBuilder::new().run_scripts::<FlowyDoc>(ops);
+    TestBuilder::new().run_scripts::<NewlineDoc>(ops);
 }
 
 #[test]
@@ -19,7 +19,7 @@ fn history_insert_undo_with_lagging() {
         Undo(0),
         AssertDocJson(0, r#"[{"insert":"\n"}]"#),
     ];
-    TestBuilder::new().run_scripts::<FlowyDoc>(ops);
+    TestBuilder::new().run_scripts::<NewlineDoc>(ops);
 }
 
 #[test]
@@ -32,7 +32,7 @@ fn history_insert_redo() {
         Redo(0),
         AssertDocJson(0, r#"[{"insert":"123\n"}]"#),
     ];
-    TestBuilder::new().run_scripts::<FlowyDoc>(ops);
+    TestBuilder::new().run_scripts::<NewlineDoc>(ops);
 }
 
 #[test]
@@ -51,7 +51,7 @@ fn history_insert_redo_with_lagging() {
         Undo(0),
         AssertDocJson(0, r#"[{"insert":"123\n"}]"#),
     ];
-    TestBuilder::new().run_scripts::<FlowyDoc>(ops);
+    TestBuilder::new().run_scripts::<NewlineDoc>(ops);
 }
 
 #[test]
@@ -62,7 +62,7 @@ fn history_bold_undo() {
         Undo(0),
         AssertDocJson(0, r#"[{"insert":"\n"}]"#),
     ];
-    TestBuilder::new().run_scripts::<FlowyDoc>(ops);
+    TestBuilder::new().run_scripts::<NewlineDoc>(ops);
 }
 
 #[test]
@@ -74,7 +74,7 @@ fn history_bold_undo_with_lagging() {
         Undo(0),
         AssertDocJson(0, r#"[{"insert":"123\n"}]"#),
     ];
-    TestBuilder::new().run_scripts::<FlowyDoc>(ops);
+    TestBuilder::new().run_scripts::<NewlineDoc>(ops);
 }
 
 #[test]
@@ -87,7 +87,7 @@ fn history_bold_redo() {
         Redo(0),
         AssertDocJson(0, r#" [{"insert":"123","attributes":{"bold":"true"}},{"insert":"\n"}]"#),
     ];
-    TestBuilder::new().run_scripts::<FlowyDoc>(ops);
+    TestBuilder::new().run_scripts::<NewlineDoc>(ops);
 }
 
 #[test]
@@ -101,7 +101,7 @@ fn history_bold_redo_with_lagging() {
         Redo(0),
         AssertDocJson(0, r#"[{"insert":"123","attributes":{"bold":"true"}},{"insert":"\n"}]"#),
     ];
-    TestBuilder::new().run_scripts::<FlowyDoc>(ops);
+    TestBuilder::new().run_scripts::<NewlineDoc>(ops);
 }
 
 #[test]
@@ -133,7 +133,7 @@ fn history_delete_undo_2() {
         Undo(0),
         AssertDocJson(0, r#"[{"insert":"\n"}]"#),
     ];
-    TestBuilder::new().run_scripts::<FlowyDoc>(ops);
+    TestBuilder::new().run_scripts::<NewlineDoc>(ops);
 }
 
 #[test]
@@ -160,7 +160,7 @@ fn history_delete_undo_with_lagging() {
             "#,
         ),
     ];
-    TestBuilder::new().run_scripts::<FlowyDoc>(ops);
+    TestBuilder::new().run_scripts::<NewlineDoc>(ops);
 }
 
 #[test]
@@ -174,7 +174,7 @@ fn history_delete_redo() {
         Redo(0),
         AssertDocJson(0, r#"[{"insert":"\n"}]"#),
     ];
-    TestBuilder::new().run_scripts::<FlowyDoc>(ops);
+    TestBuilder::new().run_scripts::<NewlineDoc>(ops);
 }
 
 #[test]
@@ -193,7 +193,7 @@ fn history_replace_undo() {
         Undo(0),
         AssertDocJson(0, r#"[{"insert":"\n"}]"#),
     ];
-    TestBuilder::new().run_scripts::<FlowyDoc>(ops);
+    TestBuilder::new().run_scripts::<NewlineDoc>(ops);
 }
 
 #[test]
@@ -214,7 +214,7 @@ fn history_replace_undo_with_lagging() {
         Undo(0),
         AssertDocJson(0, r#"[{"insert":"123","attributes":{"bold":"true"}},{"insert":"\n"}]"#),
     ];
-    TestBuilder::new().run_scripts::<FlowyDoc>(ops);
+    TestBuilder::new().run_scripts::<NewlineDoc>(ops);
 }
 
 #[test]
@@ -233,7 +233,7 @@ fn history_replace_redo() {
             "#,
         ),
     ];
-    TestBuilder::new().run_scripts::<FlowyDoc>(ops);
+    TestBuilder::new().run_scripts::<NewlineDoc>(ops);
 }
 
 #[test]
@@ -252,7 +252,7 @@ fn history_header_added_undo() {
         ),
     ];
 
-    TestBuilder::new().run_scripts::<FlowyDoc>(ops);
+    TestBuilder::new().run_scripts::<NewlineDoc>(ops);
 }
 
 #[test]
@@ -271,7 +271,7 @@ fn history_link_added_undo() {
         ),
     ];
 
-    TestBuilder::new().run_scripts::<FlowyDoc>(ops);
+    TestBuilder::new().run_scripts::<NewlineDoc>(ops);
 }
 
 #[test]
@@ -290,7 +290,7 @@ fn history_link_auto_format_undo_with_lagging() {
         AssertDocJson(0, r#"[{"insert":"https://appflowy.io\n"}]"#),
     ];
 
-    TestBuilder::new().run_scripts::<FlowyDoc>(ops);
+    TestBuilder::new().run_scripts::<NewlineDoc>(ops);
 }
 
 #[test]
@@ -313,7 +313,7 @@ fn history_bullet_undo() {
         ),
     ];
 
-    TestBuilder::new().run_scripts::<FlowyDoc>(ops);
+    TestBuilder::new().run_scripts::<NewlineDoc>(ops);
 }
 
 #[test]
@@ -341,7 +341,7 @@ fn history_bullet_undo_with_lagging() {
         ),
     ];
 
-    TestBuilder::new().run_scripts::<FlowyDoc>(ops);
+    TestBuilder::new().run_scripts::<NewlineDoc>(ops);
 }
 
 #[test]
@@ -368,5 +368,5 @@ fn history_undo_attribute_on_merge_between_line() {
         ),
     ];
 
-    TestBuilder::new().run_scripts::<FlowyDoc>(ops);
+    TestBuilder::new().run_scripts::<NewlineDoc>(ops);
 }

+ 10 - 8
shared-lib/flowy-collaboration/src/document/document.rs

@@ -14,18 +14,18 @@ use crate::{
     errors::CollaborateError,
 };
 
-pub trait CustomDocument {
-    fn init_delta() -> RichTextDelta;
+pub trait InitialDocumentText {
+    fn initial_delta() -> RichTextDelta;
 }
 
 pub struct PlainDoc();
-impl CustomDocument for PlainDoc {
-    fn init_delta() -> RichTextDelta { RichTextDelta::new() }
+impl InitialDocumentText for PlainDoc {
+    fn initial_delta() -> RichTextDelta { RichTextDelta::new() }
 }
 
-pub struct FlowyDoc();
-impl CustomDocument for FlowyDoc {
-    fn init_delta() -> RichTextDelta { initial_delta() }
+pub struct NewlineDoc();
+impl InitialDocumentText for NewlineDoc {
+    fn initial_delta() -> RichTextDelta { initial_delta() }
 }
 
 pub struct Document {
@@ -37,7 +37,7 @@ pub struct Document {
 }
 
 impl Document {
-    pub fn new<C: CustomDocument>() -> Self { Self::from_delta(C::init_delta()) }
+    pub fn new<C: InitialDocumentText>() -> Self { Self::from_delta(C::initial_delta()) }
 
     pub fn from_delta(delta: RichTextDelta) -> Self {
         Document {
@@ -193,6 +193,8 @@ impl Document {
             },
         }
     }
+
+    pub fn is_empty<C: InitialDocumentText>(&self) -> bool { self.delta == C::initial_delta() }
 }
 
 impl Document {

+ 22 - 7
shared-lib/flowy-collaboration/src/entities/revision.rs

@@ -108,13 +108,9 @@ impl std::ops::DerefMut for RepeatedRevision {
 
 impl RepeatedRevision {
     pub fn new(items: Vec<Revision>) -> Self {
-        if cfg!(debug_assertions) {
-            let mut sorted_items = items.clone();
-            sorted_items.sort_by(|a, b| a.rev_id.cmp(&b.rev_id));
-            assert_eq!(sorted_items, items, "The items passed in should be sorted")
-        }
-
-        Self { items }
+        let mut sorted_items = items.clone();
+        sorted_items.sort_by(|a, b| a.rev_id.cmp(&b.rev_id));
+        Self { items: sorted_items }
     }
 
     pub fn empty() -> Self { RepeatedRevision { items: vec![] } }
@@ -122,6 +118,21 @@ impl RepeatedRevision {
     pub fn into_inner(self) -> Vec<Revision> { self.items }
 }
 
+pub fn pair_rev_id_from_revisions(revisions: &[Revision]) -> (i64, i64) {
+    let mut rev_id = 0;
+    revisions.iter().for_each(|revision| {
+        if rev_id < revision.rev_id {
+            rev_id = revision.rev_id;
+        }
+    });
+
+    if rev_id > 0 {
+        (rev_id - 1, rev_id)
+    } else {
+        (0, rev_id)
+    }
+}
+
 #[derive(Clone, Debug, ProtoBuf, Default)]
 pub struct RevId {
     #[pb(index = 1)]
@@ -186,6 +197,10 @@ pub enum RevisionState {
     Ack   = 1,
 }
 
+impl AsRef<RevisionState> for RevisionState {
+    fn as_ref(&self) -> &RevisionState { &self }
+}
+
 #[derive(Debug, ProtoBuf_Enum, Clone, Eq, PartialEq)]
 pub enum RevType {
     DeprecatedLocal  = 0,

+ 6 - 3
shared-lib/flowy-collaboration/src/sync/server.rs

@@ -88,7 +88,10 @@ impl ServerDocumentManager {
         let doc_id = client_data.doc_id.clone();
 
         match self.get_document_handler(&doc_id).await {
-            None => Ok(()),
+            None => {
+                tracing::warn!("Document:{} doesn't exist, ignore pinging", doc_id);
+                Ok(())
+            },
             Some(handler) => {
                 let _ = handler.apply_ping(doc_id.clone(), rev_id, user).await?;
                 Ok(())
@@ -216,7 +219,7 @@ impl OpenDocHandle {
 
 impl std::ops::Drop for OpenDocHandle {
     fn drop(&mut self) {
-        log::debug!("{} OpenDocHandle drop", self.doc_id);
+        log::debug!("{} OpenDocHandle was drop", self.doc_id);
     }
 }
 
@@ -313,7 +316,7 @@ impl DocumentCommandQueue {
 
 impl std::ops::Drop for DocumentCommandQueue {
     fn drop(&mut self) {
-        log::debug!("{} DocumentCommandQueue drop", self.doc_id);
+        log::debug!("{} DocumentCommandQueue was drop", self.doc_id);
     }
 }
 

+ 8 - 2
shared-lib/flowy-collaboration/src/sync/synchronizer.rs

@@ -101,12 +101,15 @@ impl RevisionSynchronizer {
                 // delta.
                 let from_rev_id = first_revision.rev_id;
                 let to_rev_id = server_base_rev_id;
-                let _ = self.push_revisions_to_user(user, persistence, from_rev_id, to_rev_id);
+                let _ = self
+                    .push_revisions_to_user(user, persistence, from_rev_id, to_rev_id)
+                    .await;
             },
         }
         Ok(())
     }
 
+    #[tracing::instrument(level = "debug", skip(self, user, persistence), err)]
     pub async fn pong(
         &self,
         doc_id: String,
@@ -127,7 +130,9 @@ impl RevisionSynchronizer {
                 let from_rev_id = rev_id;
                 let to_rev_id = server_base_rev_id;
                 tracing::trace!("[Pong]: Push revisions to user");
-                let _ = self.push_revisions_to_user(user, persistence, from_rev_id, to_rev_id);
+                let _ = self
+                    .push_revisions_to_user(user, persistence, from_rev_id, to_rev_id)
+                    .await;
             },
         }
         Ok(())
@@ -201,6 +206,7 @@ impl RevisionSynchronizer {
             },
         };
 
+        tracing::debug!("Push revision: {} -> {} to client", from, to);
         let data = DocumentServerWSDataBuilder::build_push_message(&self.doc_id, revisions);
         user.receive(SyncResponse::Push(data));
     }