pub mod backend;
pub mod sync;
pub mod types;
#[cfg(test)]
mod tests;
pub use backend::{CrdtBackend, CrdtFileBackend, MemoryCrdtBackend};
pub use types::{CrdtDocError, CrdtDocEvent, CrdtSyncMessage};
use std::sync::Arc;
use loro::LoroDoc;
use tokio::sync::{broadcast, mpsc};
use tokio::task::JoinHandle;
use crate::network::NetworkProvider;
use crate::node::Node;
pub struct CrdtDoc {
doc_id: String,
doc: Arc<std::sync::Mutex<LoroDoc>>,
event_tx: broadcast::Sender<CrdtDocEvent>,
task_handle: tokio::sync::Mutex<Option<JoinHandle<()>>>,
backend: Arc<dyn CrdtBackend>,
compact_tx: mpsc::UnboundedSender<()>,
_local_update_sub: loro::Subscription,
}
impl CrdtDoc {
pub async fn new<N: NetworkProvider + 'static>(
node: Arc<Node<N>>,
doc_id: &str,
) -> Result<Arc<Self>, CrdtDocError> {
Self::new_with_backend(node, doc_id, Arc::new(MemoryCrdtBackend)).await
}
pub async fn new_with_backend<N: NetworkProvider + 'static>(
node: Arc<Node<N>>,
doc_id: &str,
backend: Arc<dyn CrdtBackend>,
) -> Result<Arc<Self>, CrdtDocError> {
let doc = LoroDoc::new();
if let Some(snapshot) = backend.load_snapshot(doc_id) {
doc.import(&snapshot)
.map_err(|e| CrdtDocError::Decode(format!("failed to import snapshot: {e}")))?;
}
for update in backend.load_updates(doc_id) {
doc.import(&update)
.map_err(|e| CrdtDocError::Decode(format!("failed to import update: {e}")))?;
}
let doc = Arc::new(std::sync::Mutex::new(doc));
let (event_tx, _) = broadcast::channel(256);
let (local_update_tx, local_update_rx) = mpsc::unbounded_channel::<Vec<u8>>();
let (compact_tx, compact_rx) = mpsc::unbounded_channel::<()>();
let sub = {
let d = doc.lock().unwrap();
d.subscribe_local_update(Box::new(move |bytes| {
let _ = local_update_tx.send(bytes.to_vec());
true }))
};
let task_handle = sync::spawn_sync_task(
node,
doc.clone(),
doc_id.to_string(),
backend.clone(),
event_tx.clone(),
local_update_rx,
compact_rx,
);
Ok(Arc::new(Self {
doc_id: doc_id.to_string(),
doc,
event_tx,
task_handle: tokio::sync::Mutex::new(Some(task_handle)),
backend,
compact_tx,
_local_update_sub: sub,
}))
}
pub fn map(&self, name: &str) -> loro::LoroMap {
let doc = self.doc.lock().unwrap();
doc.get_map(name)
}
pub fn list(&self, name: &str) -> loro::LoroList {
let doc = self.doc.lock().unwrap();
doc.get_list(name)
}
pub fn text(&self, name: &str) -> loro::LoroText {
let doc = self.doc.lock().unwrap();
doc.get_text(name)
}
pub fn tree(&self, name: &str) -> loro::LoroTree {
let doc = self.doc.lock().unwrap();
doc.get_tree(name)
}
pub fn movable_list(&self, name: &str) -> loro::LoroMovableList {
let doc = self.doc.lock().unwrap();
doc.get_movable_list(name)
}
pub fn counter(&self, name: &str) -> loro::LoroCounter {
let doc = self.doc.lock().unwrap();
doc.get_counter(name)
}
pub fn get_deep_value(&self) -> loro::LoroValue {
let doc = self.doc.lock().unwrap();
doc.get_deep_value()
}
pub fn commit(&self) {
let doc = self.doc.lock().unwrap();
doc.commit();
}
pub fn export_snapshot(&self) -> Result<Vec<u8>, CrdtDocError> {
let doc = self.doc.lock().unwrap();
doc.export(loro::ExportMode::Snapshot)
.map_err(|e| CrdtDocError::Encode(format!("snapshot export failed: {e}")))
}
pub fn export_updates_since(&self, vv: &loro::VersionVector) -> Result<Vec<u8>, CrdtDocError> {
let doc = self.doc.lock().unwrap();
doc.export(loro::ExportMode::updates(vv))
.map_err(|e| CrdtDocError::Encode(format!("updates export failed: {e}")))
}
pub fn import(&self, data: &[u8]) -> Result<(), CrdtDocError> {
let doc = self.doc.lock().unwrap();
doc.import(data)
.map(|_| ())
.map_err(|e| CrdtDocError::Decode(format!("import failed: {e}")))
}
pub fn version_vector(&self) -> loro::VersionVector {
let doc = self.doc.lock().unwrap();
doc.state_vv()
}
pub fn subscribe(&self) -> broadcast::Receiver<CrdtDocEvent> {
self.event_tx.subscribe()
}
pub fn doc_id(&self) -> &str {
&self.doc_id
}
pub fn compact(&self) -> Result<(), CrdtDocError> {
let snapshot = {
let doc = self.doc.lock().unwrap();
doc.export(loro::ExportMode::Snapshot)
.map_err(|e| CrdtDocError::Encode(format!("compact snapshot failed: {e}")))?
};
self.backend.save_snapshot(&self.doc_id, &snapshot);
let _ = self.compact_tx.send(());
Ok(())
}
pub fn checkout(&self, frontiers: &loro::Frontiers) -> Result<(), CrdtDocError> {
let doc = self.doc.lock().unwrap();
doc.checkout(frontiers)
.map_err(|e| CrdtDocError::Loro(format!("checkout failed: {e}")))
}
pub fn checkout_to_latest(&self) {
let doc = self.doc.lock().unwrap();
doc.checkout_to_latest();
}
pub fn is_detached(&self) -> bool {
let doc = self.doc.lock().unwrap();
doc.is_detached()
}
pub fn fork(&self) -> Result<LoroDoc, CrdtDocError> {
let doc = self.doc.lock().unwrap();
Ok(doc.fork())
}
pub fn with_doc<F, R>(&self, f: F) -> R
where
F: FnOnce(&LoroDoc) -> R,
{
let doc = self.doc.lock().unwrap();
f(&doc)
}
pub async fn stop(&self) {
let mut handle = self.task_handle.lock().await;
if let Some(h) = handle.take() {
h.abort();
tracing::info!(doc_id = self.doc_id.as_str(), "crdt_doc: stopped");
}
}
}
impl Drop for CrdtDoc {
fn drop(&mut self) {
if let Ok(mut handle) = self.task_handle.try_lock() {
if let Some(h) = handle.take() {
h.abort();
}
}
}
}