mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
config ping test
This commit is contained in:
parent
12e8424e8a
commit
1a869c0003
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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?;
|
||||
|
@ -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>,
|
||||
}
|
||||
|
@ -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]
|
||||
|
@ -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() }
|
||||
}
|
||||
|
||||
|
@ -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() {
|
||||
|
@ -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)]
|
||||
|
@ -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?;
|
||||
|
@ -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)]
|
||||
|
@ -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()
|
||||
}
|
@ -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(())
|
||||
}
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
mod file;
|
||||
mod manager;
|
||||
|
||||
pub use file::*;
|
||||
pub use manager::*;
|
@ -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);
|
||||
}
|
||||
|
@ -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];
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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));
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user