mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2025-04-25 07:07:32 -04:00
refactor synchoronizer
This commit is contained in:
parent
13aba928c3
commit
02201c238c
19 changed files with 363 additions and 63 deletions
|
@ -13,7 +13,8 @@ use flowy_collaboration::{
|
||||||
ClientRevisionWSDataType as ClientRevisionWSDataTypePB,
|
ClientRevisionWSDataType as ClientRevisionWSDataTypePB,
|
||||||
Revision as RevisionPB,
|
Revision as RevisionPB,
|
||||||
},
|
},
|
||||||
server_document::{RevisionSyncResponse, RevisionUser, ServerDocumentManager},
|
server_document::ServerDocumentManager,
|
||||||
|
synchronizer::{RevisionSyncResponse, RevisionUser},
|
||||||
};
|
};
|
||||||
use futures::stream::StreamExt;
|
use futures::stream::StreamExt;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
|
@ -11,7 +11,8 @@ use flowy_collaboration::{
|
||||||
ClientRevisionWSData as ClientRevisionWSDataPB,
|
ClientRevisionWSData as ClientRevisionWSDataPB,
|
||||||
ClientRevisionWSDataType as ClientRevisionWSDataTypePB,
|
ClientRevisionWSDataType as ClientRevisionWSDataTypePB,
|
||||||
},
|
},
|
||||||
server_document::{RevisionSyncResponse, RevisionUser, ServerDocumentManager},
|
server_document::ServerDocumentManager,
|
||||||
|
synchronizer::{RevisionSyncResponse, RevisionUser},
|
||||||
};
|
};
|
||||||
use futures::stream::StreamExt;
|
use futures::stream::StreamExt;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use actix::Message;
|
use actix::Message;
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use flowy_collaboration::entities::ws::{ClientRevisionWSData, ServerRevisionWSData};
|
use flowy_collaboration::entities::ws_data::{ClientRevisionWSData, ServerRevisionWSData};
|
||||||
use lib_ws::{WSModule, WebSocketRawMessage};
|
use lib_ws::{WSModule, WebSocketRawMessage};
|
||||||
use std::convert::TryInto;
|
use std::convert::TryInto;
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ use dashmap::DashMap;
|
||||||
use flowy_collaboration::entities::{
|
use flowy_collaboration::entities::{
|
||||||
doc::{DocumentDelta, DocumentId},
|
doc::{DocumentDelta, DocumentId},
|
||||||
revision::{md5, RepeatedRevision, Revision},
|
revision::{md5, RepeatedRevision, Revision},
|
||||||
ws::ServerRevisionWSData,
|
ws_data::ServerRevisionWSData,
|
||||||
};
|
};
|
||||||
use flowy_database::ConnectionPool;
|
use flowy_database::ConnectionPool;
|
||||||
use flowy_error::FlowyResult;
|
use flowy_error::FlowyResult;
|
||||||
|
|
|
@ -7,7 +7,7 @@ use bytes::Bytes;
|
||||||
use flowy_collaboration::{
|
use flowy_collaboration::{
|
||||||
entities::{
|
entities::{
|
||||||
revision::{RepeatedRevision, Revision, RevisionRange},
|
revision::{RepeatedRevision, Revision, RevisionRange},
|
||||||
ws::{ClientRevisionWSData, NewDocumentUser, ServerRevisionWSData, ServerRevisionWSDataType},
|
ws_data::{ClientRevisionWSData, NewDocumentUser, ServerRevisionWSData, ServerRevisionWSDataType},
|
||||||
},
|
},
|
||||||
errors::CollaborateResult,
|
errors::CollaborateResult,
|
||||||
};
|
};
|
||||||
|
|
|
@ -27,25 +27,25 @@ pub trait RevisionCloudStorage: Send + Sync {
|
||||||
) -> BoxResultFuture<(), CollaborateError>;
|
) -> BoxResultFuture<(), CollaborateError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) struct LocalRevisionCloudPersistence {
|
pub(crate) struct LocalDocumentCloudPersistence {
|
||||||
// For the moment, we use memory to cache the data, it will be implemented with other storage.
|
// For the moment, we use memory to cache the data, it will be implemented with other storage.
|
||||||
// Like the Firestore,Dropbox.etc.
|
// Like the Firestore,Dropbox.etc.
|
||||||
storage: Arc<dyn RevisionCloudStorage>,
|
storage: Arc<dyn RevisionCloudStorage>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Debug for LocalRevisionCloudPersistence {
|
impl Debug for LocalDocumentCloudPersistence {
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.write_str("LocalRevisionCloudPersistence") }
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.write_str("LocalRevisionCloudPersistence") }
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::default::Default for LocalRevisionCloudPersistence {
|
impl std::default::Default for LocalDocumentCloudPersistence {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
LocalRevisionCloudPersistence {
|
LocalDocumentCloudPersistence {
|
||||||
storage: Arc::new(MemoryDocumentCloudStorage::default()),
|
storage: Arc::new(MemoryDocumentCloudStorage::default()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DocumentCloudPersistence for LocalRevisionCloudPersistence {
|
impl DocumentCloudPersistence for LocalDocumentCloudPersistence {
|
||||||
fn read_document(&self, doc_id: &str) -> BoxResultFuture<DocumentInfo, CollaborateError> {
|
fn read_document(&self, doc_id: &str) -> BoxResultFuture<DocumentInfo, CollaborateError> {
|
||||||
let storage = self.storage.clone();
|
let storage = self.storage.clone();
|
||||||
let doc_id = doc_id.to_owned();
|
let doc_id = doc_id.to_owned();
|
||||||
|
|
|
@ -1,15 +1,16 @@
|
||||||
use crate::local_server::persistence::LocalRevisionCloudPersistence;
|
use crate::local_server::persistence::LocalDocumentCloudPersistence;
|
||||||
use async_stream::stream;
|
use async_stream::stream;
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use flowy_collaboration::{
|
use flowy_collaboration::{
|
||||||
client_document::default::initial_delta_string,
|
client_document::default::initial_delta_string,
|
||||||
entities::{
|
entities::{
|
||||||
doc::{CreateDocParams, DocumentId, DocumentInfo, ResetDocumentParams},
|
doc::{CreateDocParams, DocumentId, DocumentInfo, ResetDocumentParams},
|
||||||
ws::{ClientRevisionWSData, ClientRevisionWSDataType},
|
ws_data::{ClientRevisionWSData, ClientRevisionWSDataType},
|
||||||
},
|
},
|
||||||
errors::CollaborateError,
|
errors::CollaborateError,
|
||||||
protobuf::ClientRevisionWSData as ClientRevisionWSDataPB,
|
protobuf::ClientRevisionWSData as ClientRevisionWSDataPB,
|
||||||
server_document::*,
|
server_document::ServerDocumentManager,
|
||||||
|
synchronizer::{RevisionSyncResponse, RevisionUser},
|
||||||
};
|
};
|
||||||
use flowy_core::module::WorkspaceCloudService;
|
use flowy_core::module::WorkspaceCloudService;
|
||||||
use flowy_error::{internal_error, FlowyError};
|
use flowy_error::{internal_error, FlowyError};
|
||||||
|
@ -35,7 +36,7 @@ impl LocalServer {
|
||||||
client_ws_sender: mpsc::UnboundedSender<WebSocketRawMessage>,
|
client_ws_sender: mpsc::UnboundedSender<WebSocketRawMessage>,
|
||||||
client_ws_receiver: broadcast::Sender<WebSocketRawMessage>,
|
client_ws_receiver: broadcast::Sender<WebSocketRawMessage>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let persistence = Arc::new(LocalRevisionCloudPersistence::default());
|
let persistence = Arc::new(LocalDocumentCloudPersistence::default());
|
||||||
let doc_manager = Arc::new(ServerDocumentManager::new(persistence));
|
let doc_manager = Arc::new(ServerDocumentManager::new(persistence));
|
||||||
let stop_tx = RwLock::new(None);
|
let stop_tx = RwLock::new(None);
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use backend_service::configuration::ClientServerConfiguration;
|
use backend_service::configuration::ClientServerConfiguration;
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use flowy_collaboration::entities::ws::ClientRevisionWSData;
|
use flowy_collaboration::entities::ws_data::ClientRevisionWSData;
|
||||||
use flowy_core::{
|
use flowy_core::{
|
||||||
controller::FolderManager,
|
controller::FolderManager,
|
||||||
errors::{internal_error, FlowyError},
|
errors::{internal_error, FlowyError},
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use backend_service::configuration::ClientServerConfiguration;
|
use backend_service::configuration::ClientServerConfiguration;
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use flowy_collaboration::entities::ws::ClientRevisionWSData;
|
use flowy_collaboration::entities::ws_data::ClientRevisionWSData;
|
||||||
use flowy_database::ConnectionPool;
|
use flowy_database::ConnectionPool;
|
||||||
use flowy_document::{
|
use flowy_document::{
|
||||||
context::{DocumentContext, DocumentUser},
|
context::{DocumentContext, DocumentUser},
|
||||||
|
|
|
@ -2,7 +2,7 @@ use async_stream::stream;
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use flowy_collaboration::entities::{
|
use flowy_collaboration::entities::{
|
||||||
revision::{RevId, RevisionRange},
|
revision::{RevId, RevisionRange},
|
||||||
ws::{ClientRevisionWSData, NewDocumentUser, ServerRevisionWSData, ServerRevisionWSDataType},
|
ws_data::{ClientRevisionWSData, NewDocumentUser, ServerRevisionWSData, ServerRevisionWSDataType},
|
||||||
};
|
};
|
||||||
use flowy_error::{internal_error, FlowyError, FlowyResult};
|
use flowy_error::{internal_error, FlowyError, FlowyResult};
|
||||||
use futures_util::stream::StreamExt;
|
use futures_util::stream::StreamExt;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
pub mod doc;
|
pub mod doc;
|
||||||
pub mod parser;
|
pub mod parser;
|
||||||
pub mod revision;
|
pub mod revision;
|
||||||
pub mod ws;
|
pub mod ws_data;
|
||||||
|
|
|
@ -3,5 +3,7 @@ pub mod entities;
|
||||||
pub mod errors;
|
pub mod errors;
|
||||||
pub mod protobuf;
|
pub mod protobuf;
|
||||||
pub mod server_document;
|
pub mod server_document;
|
||||||
|
pub mod synchronizer;
|
||||||
pub mod util;
|
pub mod util;
|
||||||
|
|
||||||
pub use lib_ot::rich_text::RichTextDelta;
|
pub use lib_ot::rich_text::RichTextDelta;
|
||||||
|
|
|
@ -1,20 +1,23 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
entities::{doc::DocumentInfo, ws::ServerRevisionWSDataBuilder},
|
entities::{doc::DocumentInfo, ws_data::ServerRevisionWSDataBuilder},
|
||||||
errors::{internal_error, CollaborateError, CollaborateResult},
|
errors::{internal_error, CollaborateError, CollaborateResult},
|
||||||
protobuf::{ClientRevisionWSData, RepeatedRevision as RepeatedRevisionPB, Revision as RevisionPB},
|
protobuf::{ClientRevisionWSData, RepeatedRevision as RepeatedRevisionPB, Revision as RevisionPB},
|
||||||
server_document::{document_pad::ServerDocument, RevisionSyncResponse, RevisionSynchronizer, RevisionUser},
|
server_document::document_pad::ServerDocument,
|
||||||
|
synchronizer::{RevisionSyncPersistence, RevisionSyncResponse, RevisionSynchronizer, RevisionUser},
|
||||||
};
|
};
|
||||||
use async_stream::stream;
|
use async_stream::stream;
|
||||||
use dashmap::DashMap;
|
use dashmap::DashMap;
|
||||||
use futures::stream::StreamExt;
|
use futures::stream::StreamExt;
|
||||||
use lib_infra::future::BoxResultFuture;
|
use lib_infra::future::BoxResultFuture;
|
||||||
use lib_ot::rich_text::RichTextDelta;
|
use lib_ot::rich_text::{RichTextAttributes, RichTextDelta};
|
||||||
use std::{collections::HashMap, fmt::Debug, sync::Arc};
|
use std::{collections::HashMap, fmt::Debug, sync::Arc};
|
||||||
use tokio::{
|
use tokio::{
|
||||||
sync::{mpsc, oneshot, RwLock},
|
sync::{mpsc, oneshot, RwLock},
|
||||||
task::spawn_blocking,
|
task::spawn_blocking,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type RichTextRevisionSynchronizer = RevisionSynchronizer<RichTextAttributes>;
|
||||||
|
|
||||||
pub trait DocumentCloudPersistence: Send + Sync + Debug {
|
pub trait DocumentCloudPersistence: Send + Sync + Debug {
|
||||||
fn read_document(&self, doc_id: &str) -> BoxResultFuture<DocumentInfo, CollaborateError>;
|
fn read_document(&self, doc_id: &str) -> BoxResultFuture<DocumentInfo, CollaborateError>;
|
||||||
|
|
||||||
|
@ -173,6 +176,28 @@ struct OpenDocHandle {
|
||||||
users: DashMap<String, Arc<dyn RevisionUser>>,
|
users: DashMap<String, Arc<dyn RevisionUser>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl RevisionSyncPersistence for Arc<dyn DocumentCloudPersistence> {
|
||||||
|
fn read_revisions(
|
||||||
|
&self,
|
||||||
|
object_id: &str,
|
||||||
|
rev_ids: Option<Vec<i64>>,
|
||||||
|
) -> BoxResultFuture<Vec<RevisionPB>, CollaborateError> {
|
||||||
|
(**self).read_revisions(object_id, rev_ids)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_revisions(&self, repeated_revision: RepeatedRevisionPB) -> BoxResultFuture<(), CollaborateError> {
|
||||||
|
(**self).save_revisions(repeated_revision)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset_object(
|
||||||
|
&self,
|
||||||
|
object_id: &str,
|
||||||
|
repeated_revision: RepeatedRevisionPB,
|
||||||
|
) -> BoxResultFuture<(), CollaborateError> {
|
||||||
|
(**self).reset_document(object_id, repeated_revision)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl OpenDocHandle {
|
impl OpenDocHandle {
|
||||||
fn new(doc: DocumentInfo, persistence: Arc<dyn DocumentCloudPersistence>) -> Result<Self, CollaborateError> {
|
fn new(doc: DocumentInfo, persistence: Arc<dyn DocumentCloudPersistence>) -> Result<Self, CollaborateError> {
|
||||||
let doc_id = doc.doc_id.clone();
|
let doc_id = doc.doc_id.clone();
|
||||||
|
@ -180,12 +205,8 @@ impl OpenDocHandle {
|
||||||
let users = DashMap::new();
|
let users = DashMap::new();
|
||||||
|
|
||||||
let delta = RichTextDelta::from_bytes(&doc.text)?;
|
let delta = RichTextDelta::from_bytes(&doc.text)?;
|
||||||
let synchronizer = Arc::new(RevisionSynchronizer::new(
|
let sync_object = ServerDocument::from_delta(&doc_id, delta);
|
||||||
&doc.doc_id,
|
let synchronizer = Arc::new(RichTextRevisionSynchronizer::new(doc.rev_id, sync_object, persistence));
|
||||||
doc.rev_id,
|
|
||||||
ServerDocument::from_delta(delta),
|
|
||||||
persistence,
|
|
||||||
));
|
|
||||||
|
|
||||||
let queue = DocumentCommandQueue::new(&doc.doc_id, receiver, synchronizer)?;
|
let queue = DocumentCommandQueue::new(&doc.doc_id, receiver, synchronizer)?;
|
||||||
tokio::task::spawn(queue.run());
|
tokio::task::spawn(queue.run());
|
||||||
|
@ -263,14 +284,14 @@ enum DocumentCommand {
|
||||||
struct DocumentCommandQueue {
|
struct DocumentCommandQueue {
|
||||||
pub doc_id: String,
|
pub doc_id: String,
|
||||||
receiver: Option<mpsc::Receiver<DocumentCommand>>,
|
receiver: Option<mpsc::Receiver<DocumentCommand>>,
|
||||||
synchronizer: Arc<RevisionSynchronizer>,
|
synchronizer: Arc<RichTextRevisionSynchronizer>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DocumentCommandQueue {
|
impl DocumentCommandQueue {
|
||||||
fn new(
|
fn new(
|
||||||
doc_id: &str,
|
doc_id: &str,
|
||||||
receiver: mpsc::Receiver<DocumentCommand>,
|
receiver: mpsc::Receiver<DocumentCommand>,
|
||||||
synchronizer: Arc<RevisionSynchronizer>,
|
synchronizer: Arc<RichTextRevisionSynchronizer>,
|
||||||
) -> Result<Self, CollaborateError> {
|
) -> Result<Self, CollaborateError> {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
doc_id: doc_id.to_owned(),
|
doc_id: doc_id.to_owned(),
|
||||||
|
|
|
@ -1,39 +1,42 @@
|
||||||
use crate::{client_document::InitialDocumentText, errors::CollaborateError};
|
use crate::{client_document::InitialDocumentText, errors::CollaborateError, synchronizer::RevisionSyncObject};
|
||||||
use lib_ot::{core::*, rich_text::RichTextDelta};
|
use lib_ot::{
|
||||||
|
core::*,
|
||||||
|
rich_text::{RichTextAttributes, RichTextDelta},
|
||||||
|
};
|
||||||
|
|
||||||
pub struct ServerDocument {
|
pub struct ServerDocument {
|
||||||
|
doc_id: String,
|
||||||
delta: RichTextDelta,
|
delta: RichTextDelta,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ServerDocument {
|
impl ServerDocument {
|
||||||
pub fn new<C: InitialDocumentText>() -> Self { Self::from_delta(C::initial_delta()) }
|
#[allow(dead_code)]
|
||||||
|
pub fn new<C: InitialDocumentText>(doc_id: &str) -> Self { Self::from_delta(doc_id, C::initial_delta()) }
|
||||||
|
|
||||||
pub fn from_delta(delta: RichTextDelta) -> Self { ServerDocument { delta } }
|
pub fn from_delta(doc_id: &str, delta: RichTextDelta) -> Self {
|
||||||
|
let doc_id = doc_id.to_owned();
|
||||||
pub fn from_json(json: &str) -> Result<Self, CollaborateError> {
|
ServerDocument { doc_id, delta }
|
||||||
let delta = RichTextDelta::from_json(json)?;
|
|
||||||
Ok(Self::from_delta(delta))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn to_json(&self) -> String { self.delta.to_json() }
|
|
||||||
|
|
||||||
pub fn to_bytes(&self) -> Vec<u8> { self.delta.clone().to_bytes().to_vec() }
|
|
||||||
|
|
||||||
pub fn to_plain_string(&self) -> String { self.delta.apply("").unwrap() }
|
|
||||||
|
|
||||||
pub fn delta(&self) -> &RichTextDelta { &self.delta }
|
|
||||||
|
|
||||||
pub fn md5(&self) -> String {
|
|
||||||
let bytes = self.to_bytes();
|
|
||||||
format!("{:x}", md5::compute(bytes))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn compose_delta(&mut self, delta: RichTextDelta) -> Result<(), CollaborateError> {
|
|
||||||
// tracing::trace!("{} compose {}", &self.delta.to_json(), delta.to_json());
|
|
||||||
let composed_delta = self.delta.compose(&delta)?;
|
|
||||||
self.delta = composed_delta;
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_empty<C: InitialDocumentText>(&self) -> bool { self.delta == C::initial_delta() }
|
pub fn is_empty<C: InitialDocumentText>(&self) -> bool { self.delta == C::initial_delta() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl RevisionSyncObject<RichTextAttributes> for ServerDocument {
|
||||||
|
fn id(&self) -> &str { &self.doc_id }
|
||||||
|
|
||||||
|
fn compose(&mut self, other: &RichTextDelta) -> Result<(), CollaborateError> {
|
||||||
|
tracing::trace!("{} compose {}", &self.delta.to_json(), other.to_json());
|
||||||
|
let new_delta = self.delta.compose(other)?;
|
||||||
|
self.delta = new_delta;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn transform(&self, other: &RichTextDelta) -> Result<(RichTextDelta, RichTextDelta), CollaborateError> {
|
||||||
|
let value = self.delta.transform(other)?;
|
||||||
|
Ok(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_json(&self) -> String { self.delta.to_json() }
|
||||||
|
|
||||||
|
fn set_delta(&mut self, new_delta: Delta<RichTextAttributes>) { self.delta = new_delta; }
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
mod document_manager;
|
mod document_manager;
|
||||||
mod document_pad;
|
mod document_pad;
|
||||||
mod revision_sync;
|
|
||||||
|
|
||||||
pub use document_manager::*;
|
pub use document_manager::*;
|
||||||
pub use revision_sync::*;
|
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
entities::{
|
entities::{
|
||||||
revision::RevisionRange,
|
revision::RevisionRange,
|
||||||
ws::{ServerRevisionWSData, ServerRevisionWSDataBuilder},
|
ws_data::{ServerRevisionWSData, ServerRevisionWSDataBuilder},
|
||||||
},
|
},
|
||||||
errors::CollaborateError,
|
errors::CollaborateError,
|
||||||
protobuf::{RepeatedRevision as RepeatedRevisionPB, Revision as RevisionPB},
|
protobuf::{RepeatedRevision as RepeatedRevisionPB, Revision as RevisionPB},
|
||||||
server_document::{document_pad::ServerDocument, DocumentCloudPersistence},
|
server_document::{document_pad::ServerDocument, DocumentCloudPersistence},
|
||||||
util::*,
|
util::*,
|
||||||
};
|
};
|
||||||
|
use lib_infra::future::BoxResultFuture;
|
||||||
use lib_ot::{core::OperationTransformable, rich_text::RichTextDelta};
|
use lib_ot::{core::OperationTransformable, rich_text::RichTextDelta};
|
||||||
use parking_lot::RwLock;
|
use parking_lot::RwLock;
|
||||||
use std::{
|
use std::{
|
||||||
|
@ -25,6 +26,16 @@ pub trait RevisionUser: Send + Sync + Debug {
|
||||||
fn receive(&self, resp: RevisionSyncResponse);
|
fn receive(&self, resp: RevisionSyncResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub trait RevisionSyncObject {
|
||||||
|
type SyncObject;
|
||||||
|
|
||||||
|
fn read_revisions(&self, rev_ids: Option<Vec<i64>>) -> BoxResultFuture<Vec<RevisionPB>, CollaborateError>;
|
||||||
|
|
||||||
|
fn save_revisions(&self, repeated_revision: RepeatedRevisionPB) -> BoxResultFuture<(), CollaborateError>;
|
||||||
|
|
||||||
|
fn reset_object(&self, repeated_revision: RepeatedRevisionPB) -> BoxResultFuture<(), CollaborateError>;
|
||||||
|
}
|
||||||
|
|
||||||
pub enum RevisionSyncResponse {
|
pub enum RevisionSyncResponse {
|
||||||
Pull(ServerRevisionWSData),
|
Pull(ServerRevisionWSData),
|
||||||
Push(ServerRevisionWSData),
|
Push(ServerRevisionWSData),
|
||||||
|
|
260
shared-lib/flowy-collaboration/src/synchronizer.rs
Normal file
260
shared-lib/flowy-collaboration/src/synchronizer.rs
Normal file
|
@ -0,0 +1,260 @@
|
||||||
|
use crate::{
|
||||||
|
entities::{
|
||||||
|
revision::RevisionRange,
|
||||||
|
ws_data::{ServerRevisionWSData, ServerRevisionWSDataBuilder},
|
||||||
|
},
|
||||||
|
errors::CollaborateError,
|
||||||
|
protobuf::{RepeatedRevision as RepeatedRevisionPB, Revision as RevisionPB},
|
||||||
|
util::*,
|
||||||
|
};
|
||||||
|
use lib_infra::future::BoxResultFuture;
|
||||||
|
use lib_ot::core::{Attributes, Delta, OperationTransformable};
|
||||||
|
use parking_lot::RwLock;
|
||||||
|
use serde::de::DeserializeOwned;
|
||||||
|
use std::{
|
||||||
|
cmp::Ordering,
|
||||||
|
fmt::Debug,
|
||||||
|
sync::{
|
||||||
|
atomic::{AtomicI64, Ordering::SeqCst},
|
||||||
|
Arc,
|
||||||
|
},
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub trait RevisionUser: Send + Sync + Debug {
|
||||||
|
fn user_id(&self) -> String;
|
||||||
|
fn receive(&self, resp: RevisionSyncResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait RevisionSyncPersistence: Send + Sync + 'static {
|
||||||
|
fn read_revisions(
|
||||||
|
&self,
|
||||||
|
object_id: &str,
|
||||||
|
rev_ids: Option<Vec<i64>>,
|
||||||
|
) -> BoxResultFuture<Vec<RevisionPB>, CollaborateError>;
|
||||||
|
|
||||||
|
fn save_revisions(&self, repeated_revision: RepeatedRevisionPB) -> BoxResultFuture<(), CollaborateError>;
|
||||||
|
|
||||||
|
fn reset_object(
|
||||||
|
&self,
|
||||||
|
object_id: &str,
|
||||||
|
repeated_revision: RepeatedRevisionPB,
|
||||||
|
) -> BoxResultFuture<(), CollaborateError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait RevisionSyncObject<T: Attributes>: Send + Sync + 'static {
|
||||||
|
fn id(&self) -> &str;
|
||||||
|
fn compose(&mut self, other: &Delta<T>) -> Result<(), CollaborateError>;
|
||||||
|
fn transform(&self, other: &Delta<T>) -> Result<(Delta<T>, Delta<T>), CollaborateError>;
|
||||||
|
fn to_json(&self) -> String;
|
||||||
|
fn set_delta(&mut self, new_delta: Delta<T>);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum RevisionSyncResponse {
|
||||||
|
Pull(ServerRevisionWSData),
|
||||||
|
Push(ServerRevisionWSData),
|
||||||
|
Ack(ServerRevisionWSData),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct RevisionSynchronizer<T: Attributes> {
|
||||||
|
object_id: String,
|
||||||
|
rev_id: AtomicI64,
|
||||||
|
object: Arc<RwLock<dyn RevisionSyncObject<T>>>,
|
||||||
|
persistence: Arc<dyn RevisionSyncPersistence>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> RevisionSynchronizer<T>
|
||||||
|
where
|
||||||
|
T: Attributes + DeserializeOwned + serde::Serialize + 'static,
|
||||||
|
{
|
||||||
|
pub fn new<S, P>(rev_id: i64, sync_object: S, persistence: P) -> RevisionSynchronizer<T>
|
||||||
|
where
|
||||||
|
S: RevisionSyncObject<T>,
|
||||||
|
P: RevisionSyncPersistence,
|
||||||
|
{
|
||||||
|
let object = Arc::new(RwLock::new(sync_object));
|
||||||
|
let persistence = Arc::new(persistence);
|
||||||
|
let object_id = object.read().id().to_owned();
|
||||||
|
RevisionSynchronizer {
|
||||||
|
object_id,
|
||||||
|
rev_id: AtomicI64::new(rev_id),
|
||||||
|
object,
|
||||||
|
persistence,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(level = "debug", skip(self, user, repeated_revision), err)]
|
||||||
|
pub async fn sync_revisions(
|
||||||
|
&self,
|
||||||
|
user: Arc<dyn RevisionUser>,
|
||||||
|
repeated_revision: RepeatedRevisionPB,
|
||||||
|
) -> Result<(), CollaborateError> {
|
||||||
|
let doc_id = self.object_id.clone();
|
||||||
|
if repeated_revision.get_items().is_empty() {
|
||||||
|
// Return all the revisions to client
|
||||||
|
let revisions = self.persistence.read_revisions(&doc_id, None).await?;
|
||||||
|
let repeated_revision = repeated_revision_from_revision_pbs(revisions)?;
|
||||||
|
let data = ServerRevisionWSDataBuilder::build_push_message(&doc_id, repeated_revision);
|
||||||
|
user.receive(RevisionSyncResponse::Push(data));
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let server_base_rev_id = self.rev_id.load(SeqCst);
|
||||||
|
let first_revision = repeated_revision.get_items().first().unwrap().clone();
|
||||||
|
if self.is_applied_before(&first_revision, &self.persistence).await {
|
||||||
|
// Server has received this revision before, so ignore the following revisions
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
match server_base_rev_id.cmp(&first_revision.rev_id) {
|
||||||
|
Ordering::Less => {
|
||||||
|
let server_rev_id = next(server_base_rev_id);
|
||||||
|
if server_base_rev_id == first_revision.base_rev_id || server_rev_id == first_revision.rev_id {
|
||||||
|
// The rev is in the right order, just compose it.
|
||||||
|
for revision in repeated_revision.get_items() {
|
||||||
|
let _ = self.compose_revision(revision)?;
|
||||||
|
}
|
||||||
|
let _ = self.persistence.save_revisions(repeated_revision).await?;
|
||||||
|
} else {
|
||||||
|
// The server document is outdated, pull the missing revision from the client.
|
||||||
|
let range = RevisionRange {
|
||||||
|
object_id: self.object_id.clone(),
|
||||||
|
start: server_rev_id,
|
||||||
|
end: first_revision.rev_id,
|
||||||
|
};
|
||||||
|
let msg = ServerRevisionWSDataBuilder::build_pull_message(&self.object_id, range);
|
||||||
|
user.receive(RevisionSyncResponse::Pull(msg));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Ordering::Equal => {
|
||||||
|
// Do nothing
|
||||||
|
tracing::warn!("Applied revision rev_id is the same as cur_rev_id");
|
||||||
|
},
|
||||||
|
Ordering::Greater => {
|
||||||
|
// The client document is outdated. Transform the client revision delta and then
|
||||||
|
// send the prime delta to the client. Client should compose the this prime
|
||||||
|
// delta.
|
||||||
|
let from_rev_id = first_revision.rev_id;
|
||||||
|
let to_rev_id = server_base_rev_id;
|
||||||
|
let _ = self.push_revisions_to_user(user, from_rev_id, to_rev_id).await;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(level = "trace", skip(self, user), fields(server_rev_id), err)]
|
||||||
|
pub async fn pong(&self, user: Arc<dyn RevisionUser>, client_rev_id: i64) -> Result<(), CollaborateError> {
|
||||||
|
let doc_id = self.object_id.clone();
|
||||||
|
let server_rev_id = self.rev_id();
|
||||||
|
tracing::Span::current().record("server_rev_id", &server_rev_id);
|
||||||
|
|
||||||
|
match server_rev_id.cmp(&client_rev_id) {
|
||||||
|
Ordering::Less => {
|
||||||
|
tracing::error!("Client should not send ping and the server should pull the revisions from the client")
|
||||||
|
},
|
||||||
|
Ordering::Equal => tracing::trace!("{} is up to date.", doc_id),
|
||||||
|
Ordering::Greater => {
|
||||||
|
// The client document is outdated. Transform the client revision delta and then
|
||||||
|
// send the prime delta to the client. Client should compose the this prime
|
||||||
|
// delta.
|
||||||
|
let from_rev_id = client_rev_id;
|
||||||
|
let to_rev_id = server_rev_id;
|
||||||
|
tracing::trace!("Push revisions to user");
|
||||||
|
let _ = self.push_revisions_to_user(user, from_rev_id, to_rev_id).await;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(level = "debug", skip(self, repeated_revision), fields(doc_id), err)]
|
||||||
|
pub async fn reset(&self, repeated_revision: RepeatedRevisionPB) -> Result<(), CollaborateError> {
|
||||||
|
let doc_id = self.object_id.clone();
|
||||||
|
tracing::Span::current().record("doc_id", &doc_id.as_str());
|
||||||
|
let revisions: Vec<RevisionPB> = repeated_revision.get_items().to_vec();
|
||||||
|
let (_, rev_id) = pair_rev_id_from_revision_pbs(&revisions);
|
||||||
|
let delta = make_delta_from_revision_pb(revisions)?;
|
||||||
|
let _ = self.persistence.reset_object(&doc_id, repeated_revision).await?;
|
||||||
|
self.object.write().set_delta(delta);
|
||||||
|
let _ = self.rev_id.fetch_update(SeqCst, SeqCst, |_e| Some(rev_id));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn object_json(&self) -> String { self.object.read().to_json() }
|
||||||
|
|
||||||
|
fn compose_revision(&self, revision: &RevisionPB) -> Result<(), CollaborateError> {
|
||||||
|
let delta = Delta::<T>::from_bytes(&revision.delta_data)?;
|
||||||
|
let _ = self.compose_delta(delta)?;
|
||||||
|
let _ = self.rev_id.fetch_update(SeqCst, SeqCst, |_e| Some(revision.rev_id));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(level = "debug", skip(self, revision))]
|
||||||
|
fn transform_revision(&self, revision: &RevisionPB) -> Result<(Delta<T>, Delta<T>), CollaborateError> {
|
||||||
|
let cli_delta = Delta::<T>::from_bytes(&revision.delta_data)?;
|
||||||
|
let result = self.object.read().transform(&cli_delta)?;
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn compose_delta(&self, delta: Delta<T>) -> Result<(), CollaborateError> {
|
||||||
|
if delta.is_empty() {
|
||||||
|
log::warn!("Composed delta is empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
match self.object.try_write_for(Duration::from_millis(300)) {
|
||||||
|
None => log::error!("Failed to acquire write lock of document"),
|
||||||
|
Some(mut write_guard) => {
|
||||||
|
let _ = write_guard.compose(&delta)?;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn rev_id(&self) -> i64 { self.rev_id.load(SeqCst) }
|
||||||
|
|
||||||
|
async fn is_applied_before(
|
||||||
|
&self,
|
||||||
|
new_revision: &RevisionPB,
|
||||||
|
persistence: &Arc<dyn RevisionSyncPersistence>,
|
||||||
|
) -> bool {
|
||||||
|
let rev_ids = Some(vec![new_revision.rev_id]);
|
||||||
|
if let Ok(revisions) = persistence.read_revisions(&self.object_id, rev_ids).await {
|
||||||
|
if let Some(revision) = revisions.first() {
|
||||||
|
if revision.md5 == new_revision.md5 {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn push_revisions_to_user(&self, user: Arc<dyn RevisionUser>, from: i64, to: i64) {
|
||||||
|
let rev_ids: Vec<i64> = (from..=to).collect();
|
||||||
|
let revisions = match self.persistence.read_revisions(&self.object_id, Some(rev_ids)).await {
|
||||||
|
Ok(revisions) => {
|
||||||
|
assert_eq!(
|
||||||
|
revisions.is_empty(),
|
||||||
|
false,
|
||||||
|
"revisions should not be empty if the doc exists"
|
||||||
|
);
|
||||||
|
revisions
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("{}", e);
|
||||||
|
vec![]
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
tracing::debug!("Push revision: {} -> {} to client", from, to);
|
||||||
|
match repeated_revision_from_revision_pbs(revisions) {
|
||||||
|
Ok(repeated_revision) => {
|
||||||
|
let data = ServerRevisionWSDataBuilder::build_push_message(&self.object_id, repeated_revision);
|
||||||
|
user.receive(RevisionSyncResponse::Push(data));
|
||||||
|
},
|
||||||
|
Err(e) => tracing::error!("{}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn next(rev_id: i64) -> i64 { rev_id + 1 }
|
|
@ -12,6 +12,8 @@ use std::{
|
||||||
convert::TryInto,
|
convert::TryInto,
|
||||||
sync::atomic::{AtomicI64, Ordering::SeqCst},
|
sync::atomic::{AtomicI64, Ordering::SeqCst},
|
||||||
};
|
};
|
||||||
|
use serde::de::DeserializeOwned;
|
||||||
|
use lib_ot::core::{Attributes, Delta};
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn find_newline(s: &str) -> Option<usize> { s.find(NEW_LINE) }
|
pub fn find_newline(s: &str) -> Option<usize> { s.find(NEW_LINE) }
|
||||||
|
@ -57,10 +59,10 @@ pub fn make_delta_from_revisions(revisions: Vec<Revision>) -> CollaborateResult<
|
||||||
Ok(delta)
|
Ok(delta)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn make_delta_from_revision_pb(revisions: Vec<RevisionPB>) -> CollaborateResult<RichTextDelta> {
|
pub fn make_delta_from_revision_pb<T>(revisions: Vec<RevisionPB>) -> CollaborateResult<Delta<T>> where T: Attributes + DeserializeOwned {
|
||||||
let mut new_delta = RichTextDelta::new();
|
let mut new_delta = Delta::<T>::new();
|
||||||
for revision in revisions {
|
for revision in revisions {
|
||||||
let delta = RichTextDelta::from_bytes(revision.delta_data).map_err(|e| {
|
let delta = Delta::<T>::from_bytes(revision.delta_data).map_err(|e| {
|
||||||
let err_msg = format!("Deserialize remote revision failed: {:?}", e);
|
let err_msg = format!("Deserialize remote revision failed: {:?}", e);
|
||||||
CollaborateError::internal().context(err_msg)
|
CollaborateError::internal().context(err_msg)
|
||||||
})?;
|
})?;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue