cloudillo_types/crdt_adapter.rs
1// SPDX-FileCopyrightText: Szilárd Hajba
2// SPDX-License-Identifier: LGPL-3.0-or-later
3
4//! CRDT Document Adapter
5//!
6//! Trait and types for pluggable CRDT document backends that store binary updates
7//! for collaborative documents using Yjs/yrs (Rust port of Yjs).
8//!
9//! The adapter handles:
10//! - Persistence of binary CRDT updates (Yjs sync protocol format)
11//! - Change subscriptions for real-time updates
12//! - Document lifecycle (creation, deletion)
13//!
14//! Each adapter implementation provides its own constructor handling backend-specific
15//! initialization (database path, connection settings, etc.).
16//!
17//! The adapter works with binary updates (Uint8Array) rather than typed documents,
18//! allowing flexibility in how updates are stored and reconstructed into Y.Doc instances.
19
20use async_trait::async_trait;
21use futures_core::Stream;
22use serde::{Deserialize, Serialize};
23use std::fmt::Debug;
24use std::pin::Pin;
25
26use crate::prelude::*;
27use crate::types::CompactReport;
28
29/// A binary CRDT update (serialized Yjs sync protocol message).
30///
31/// These updates are the fundamental unit of change in CRDT systems.
32/// They can be applied in any order and are commutative.
33#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct CrdtUpdate {
35 /// Raw bytes of the update in Yjs sync protocol format
36 pub data: Vec<u8>,
37
38 /// Optional user/client ID that created this update
39 pub client_id: Option<Box<str>>,
40
41 /// Storage sequence number (populated by get_updates, used by compact_updates)
42 #[serde(skip)]
43 pub seq: Option<u64>,
44}
45
46impl CrdtUpdate {
47 /// Create a new CRDT update from raw bytes.
48 pub fn new(data: Vec<u8>) -> Self {
49 Self { data, client_id: None, seq: None }
50 }
51
52 /// Create a new CRDT update with client ID.
53 pub fn with_client(data: Vec<u8>, client_id: impl Into<Box<str>>) -> Self {
54 Self { data, client_id: Some(client_id.into()), seq: None }
55 }
56}
57
58/// Real-time change notification for a CRDT document.
59#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct CrdtChangeEvent {
61 /// Document ID
62 pub doc_id: Box<str>,
63
64 /// The update that caused this change
65 pub update: CrdtUpdate,
66}
67
68/// Options for subscribing to CRDT document changes.
69#[derive(Debug, Clone)]
70pub struct CrdtSubscriptionOptions {
71 /// Document ID to subscribe to
72 pub doc_id: Box<str>,
73
74 /// If true, send existing updates as initial snapshot
75 pub send_snapshot: bool,
76}
77
78impl CrdtSubscriptionOptions {
79 /// Create a subscription to a document with snapshot.
80 pub fn with_snapshot(doc_id: impl Into<Box<str>>) -> Self {
81 Self { doc_id: doc_id.into(), send_snapshot: true }
82 }
83
84 /// Create a subscription to future updates only (no snapshot).
85 pub fn updates_only(doc_id: impl Into<Box<str>>) -> Self {
86 Self { doc_id: doc_id.into(), send_snapshot: false }
87 }
88}
89
90/// CRDT Document statistics.
91#[derive(Debug, Clone, Serialize, Deserialize)]
92pub struct CrdtDocStats {
93 /// Document ID
94 pub doc_id: Box<str>,
95
96 /// Total size of stored updates in bytes
97 pub size_bytes: u64,
98
99 /// Number of updates stored
100 pub update_count: u32,
101}
102
103/// CRDT Adapter trait.
104///
105/// Unified interface for CRDT document backends. Handles persistence of binary updates
106/// and real-time subscriptions.
107///
108/// # Multi-Tenancy
109///
110/// All operations are tenant-aware (tn_id parameter). Adapters must ensure:
111/// - Updates from different tenants are stored separately
112/// - Subscriptions only receive updates for the subscribing tenant
113#[async_trait]
114pub trait CrdtAdapter: Debug + Send + Sync {
115 /// Get all stored updates for a document.
116 ///
117 /// Returns updates in the order they were stored. These can be applied
118 /// to a fresh Y.Doc to reconstruct the current state.
119 ///
120 /// Returns empty vec if document doesn't exist (safe to treat as new doc).
121 async fn get_updates(&self, tn_id: TnId, doc_id: &str) -> ClResult<Vec<CrdtUpdate>>;
122
123 /// Store a new update for a document.
124 ///
125 /// The update is persisted immediately. For high-frequency updates,
126 /// implementations may batch or compress updates.
127 ///
128 /// If the document doesn't exist, it's implicitly created.
129 async fn store_update(&self, tn_id: TnId, doc_id: &str, update: CrdtUpdate) -> ClResult<()>;
130
131 /// Subscribe to updates for a document.
132 ///
133 /// Returns a stream of updates. Depending on subscription options,
134 /// may include a snapshot of existing updates followed by new updates.
135 async fn subscribe(
136 &self,
137 tn_id: TnId,
138 opts: CrdtSubscriptionOptions,
139 ) -> ClResult<Pin<Box<dyn Stream<Item = CrdtChangeEvent> + Send>>>;
140
141 /// Get statistics for a document.
142 async fn stats(&self, tn_id: TnId, doc_id: &str) -> ClResult<CrdtDocStats> {
143 let updates = self.get_updates(tn_id, doc_id).await?;
144 let update_count = u32::try_from(updates.len()).unwrap_or_default();
145 let size_bytes: u64 = updates.iter().map(|u| u.data.len() as u64).sum();
146
147 Ok(CrdtDocStats { doc_id: doc_id.into(), size_bytes, update_count })
148 }
149
150 /// Atomically replace specific updates with a single compacted update.
151 ///
152 /// Deletes the updates identified by `remove_seqs` and inserts the
153 /// `replacement` update, all in a single transaction. Updates not listed
154 /// in `remove_seqs` (e.g., ones that failed to decode) are preserved.
155 ///
156 /// Non-existent seqs in `remove_seqs` are silently ignored.
157 ///
158 /// **Important:** This method does not broadcast a change event. It should
159 /// only be called when no active subscribers exist (e.g., after the last
160 /// connection to the document has closed).
161 async fn compact_updates(
162 &self,
163 tn_id: TnId,
164 doc_id: &str,
165 remove_seqs: &[u64],
166 replacement: CrdtUpdate,
167 ) -> ClResult<()>;
168
169 /// Delete a document and all its updates.
170 ///
171 /// This removes all stored data for the document. Use with caution.
172 async fn delete_doc(&self, tn_id: TnId, doc_id: &str) -> ClResult<()>;
173
174 /// Close/flush a document instance, ensuring all updates are persisted.
175 ///
176 /// Some implementations may keep documents in-memory and need explicit
177 /// flush before shutdown. Others may be no-op.
178 async fn close_doc(&self, _tn_id: TnId, _doc_id: &str) -> ClResult<()> {
179 // Default: no-op. Implementations can override.
180 Ok(())
181 }
182
183 /// List all document IDs for a tenant.
184 ///
185 /// Useful for administrative tasks and migrations.
186 async fn list_docs(&self, tn_id: TnId) -> ClResult<Vec<Box<str>>>;
187
188 /// Delete every CRDT document owned by the tenant.
189 ///
190 /// Used by tenant purge orchestration. Implementations should treat a
191 /// missing tenant store as success.
192 async fn delete_tenant_documents(&self, tn_id: TnId) -> ClResult<()>;
193
194 /// Rewrite every storage file, returning the space already freed inside them
195 /// to the filesystem.
196 ///
197 /// Called from the nightly maintenance task, never from a request path: a
198 /// backend may have to close and reopen its files, which blocks every reader
199 /// and writer of the one being rewritten.
200 ///
201 /// Only space already dead *inside* the file is given back. A CRDT document's
202 /// update log is trimmed by `compact_updates`, which runs only when the last
203 /// WebSocket client of a document disconnects — so a long-lived document frees
204 /// nothing here.
205 ///
206 /// Defaults to a no-op report, the honest answer for a backend with nothing
207 /// to compact.
208 async fn compact_storage(&self) -> ClResult<CompactReport> {
209 Ok(CompactReport::default())
210 }
211}
212
213// vim: ts=4