dson/lib.rs
1// (c) Copyright 2025 Helsing GmbH. All rights reserved.
2//! # DSON: A Delta-State CRDT for JSON-like Data Structures
3//!
4//! This crate provides a Rust implementation of **DSON**, a space-efficient,
5//! delta-state Conflict-Free Replicated Datatype (CRDT) for JSON-like data structures.
6//! It is based on the research paper ["DSON: JSON CRDT Using Delta-Mutations For Document Stores"][paper]
7//! and inspired by the original author's [JavaScript implementation][js-impl].
8//!
9//! The primary goal of this library is to enable robust, and efficient
10//! multi-writer collaboration in extremely constrained environments (high
11//! latency and low bandwidth; opportunistic networking).
12//!
13//! Unlike other CRDT libraries that expose a single "Document" type, DSON provides a set of
14//! composable primitives. This allows you to build the exact data structure you need. The most
15//! common top-level structure is an [`OrMap`], which can contain other CRDTs, enabling nested,
16//! JSON-like objects. The entire state is typically wrapped in a [`CausalDotStore`], which
17//! tracks the causal history.
18//!
19//! [paper]: https://dl.acm.org/doi/10.14778/3510397.3510403
20//! [oppnet]: https://hal.science/hal-03405138/document "Frédéric Guidec, Yves Mahéo, Camille Noûs. Delta-State-Based Synchronization of CRDTs in Opportunistic Networks. In 2021 IEEE 46th Conference on Local Computer Networks (LCN). doi:10.1109/LCN52139.2021.9524978"
21//! [js-impl]: https://github.com/crdt-ibm-research/json-delta-crdt
22//!
23//! ## Core Concepts
24//!
25//! DSON provides three fundamental, composable CRDTs:
26//!
27//! - [`OrMap`]: An **Observed-Remove Map**, mapping arbitrary keys to other CRDT values.
28//! - [`OrArray`]: An **Observed-Remove Array**, providing a list-like structure.
29//! - [`MvReg`]: A **Multi-Value Register**, for storing primitive values. When
30//! concurrent writes occur, the register holds all conflicting values. This is
31//! the only CRDT in this library that can represent value conflicts.
32//!
33//! These primitives can be nested to create arbitrarily complex data structures, such as a map
34//! containing an array of other maps.
35//!
36//! All modifications produce a **delta**. Instead of sending the entire state after each
37//! change, only this small delta needs to be transmitted to other replicas.
38//!
39//! ## Observed-Remove Semantics
40//!
41//! DSON uses **Observed-Remove (OR)** semantics for its collections. This means
42//! an element can only be removed if its addition has been observed. If an
43//! element is updated concurrently with its removal, the update "wins," and the
44//! element is preserved. OR-semantics are intuitive, and this is often the
45//! desired behavior.
46//! Consider a collaborative shopping list:
47//!
48//! 1. **Initial State**: Both Alice and Bob see `["apples", "bananas"]`.
49//! 2. **Alice's Action**: Alice updates "bananas" to "blueberries".
50//! 3. **Bob's Action**: Concurrently, Bob removes "bananas".
51//!
52//! With OR-semantics, the final list will be `["apples", "blueberries"]`. Bob's removal
53//! is overridden by Alice's concurrent update because the update implies the continued
54//! existence of the item.
55//!
56//! DSON can be extended with special CRDTs providing different semantics
57//! for specific use cases though.
58//!
59//! ## Causal CRDTs and Tombstone-Free Removals
60//!
61//! DSON is a **causal** CRDT, meaning it uses causal history to resolve conflicts.
62//! This history is tracked in a [`CausalContext`], which contains a set of "dots"—unique
63//! identifiers for every operation.
64//!
65//! ### Dots
66//!
67//! A **dot** is a globally unique identifier for an operation (for example, adding or updating a
68//! value). It is the fundamental unit for tracking causality.
69//!
70//! A [`Dot`] is a tuple `(Identifier, Sequence)`:
71//!
72//! - **[`Identifier`]**: A unique ID for the actor (a specific application instance on a
73//! specific node) that performed the operation. It is composed of a `NodeId` and an
74//! `ApplicationId`. This structure allows multiple applications on the same machine to
75//! collaborate without their histories conflicting.
76//! - **`Sequence`**: A monotonically increasing number (effectively a Lamport timestamp)
77//! that is unique to that actor.
78//!
79//! When a replica makes a change, it generates a new dot. This dot is broadcast to other
80//! replicas along with the **delta** describing the change.
81//!
82//! The collection of all dots a replica has observed forms its [`CausalContext`]. This
83//! context represents the replica's knowledge of the document's history. By comparing its
84//! local `CausalContext` with the context from a received delta, a replica can determine
85//! which operations are new, which are concurrent, and which have already been seen. This
86//! allows DSON to merge changes correctly and guarantee convergence.
87//!
88//! A key advantage of this model is the elimination of **tombstones**. In many other
89//! CRDTs, when an item is deleted, a "tombstone" marker is left behind to signify
90//! its removal. These tombstones are never garbage-collected and can cause unbounded
91//! metadata growth in long-lived documents.
92//!
93//! DSON avoids this growth by tracking which operations are "live". A removal is simply the
94//! absence of an operation's dot from the causal context. When replicas sync, they
95//! can determine which items have been deleted by comparing their causal contexts,
96//! without needing explicit tombstone markers. This ensures that the metadata size
97//! remains proportional to the size of the live data, not the entire history of
98//! operations.
99//!
100//! ## Scope of this Crate
101//!
102//! This crate provides the core data structures and algorithms for DSON. It is
103//! responsible for generating deltas from mutations and merging them to ensure
104//! eventual consistency. It is up to you to build your document structure by
105//! composing the provided CRDT primitives, most commonly by wrapping an [`OrMap`]
106//! in a [`CausalDotStore`].
107//!
108//! Note that this is a low-level library. You will likely want to build a
109//! typed abstraction layer on top of `dson` rather than use it directly in your
110//! application code.
111//!
112//! **It does not include any networking protocols.**
113//!
114//! You are responsible for implementing the transport layer to broadcast deltas
115//! to other replicas. The correctness of this library, particularly its
116//! **causal consistency** guarantees, relies on the transport layer delivering
117//! deltas in an order that respects the causal history of events. This is typically
118//! achieved with an anti-entropy algorithm that exchanges deltas and their
119//! causal metadata ([`CausalContext`]).
120//!
121//! ## Getting Started: A Simple Conflict
122//!
123//! This example demonstrates how two users (Alice and Bob) concurrently edit the same
124//! data, creating a conflict that DSON resolves gracefully using the transaction API.
125//!
126//! ```rust
127//! use dson::{
128//! crdts::{mvreg::MvRegValue, snapshot::ToValue},
129//! CausalDotStore, Identifier, OrMap,
130//! };
131//!
132//! // 1. SETUP: TWO REPLICAS
133//! // Create two replicas, Alice and Bob, each with a unique ID.
134//! let alice_id = Identifier::new(0, 0);
135//! let mut alice_store = CausalDotStore::<OrMap<String>>::default();
136//!
137//! let bob_id = Identifier::new(1, 0);
138//! let mut bob_store = CausalDotStore::<OrMap<String>>::default();
139//!
140//! // 2. INITIAL STATE
141//! // Alice creates an initial value using the transaction API.
142//! let key = "document".to_string();
143//! let delta_from_alice = {
144//! let mut tx = alice_store.transact(alice_id);
145//! tx.write_register(&key, MvRegValue::String("initial value".to_string()));
146//! tx.commit()
147//! };
148//!
149//! // 3. SYNC
150//! // Bob receives Alice's initial change.
151//! bob_store.join_or_replace_with(delta_from_alice.0.store, &delta_from_alice.0.context);
152//! assert_eq!(alice_store, bob_store);
153//!
154//! // 4. CONCURRENT EDITS
155//! // Now Alice and Bob make changes without syncing.
156//!
157//! // Alice updates the value to "from Alice".
158//! let delta_alice_edit = {
159//! let mut tx = alice_store.transact(alice_id);
160//! tx.write_register(&key, MvRegValue::String("from Alice".to_string()));
161//! tx.commit()
162//! };
163//!
164//! // Concurrently, Bob updates the value to "from Bob".
165//! let delta_bob_edit = {
166//! let mut tx = bob_store.transact(bob_id);
167//! tx.write_register(&key, MvRegValue::String("from Bob".to_string()));
168//! tx.commit()
169//! };
170//!
171//! // 5. MERGE
172//! // The replicas exchange their changes.
173//! alice_store.join_or_replace_with(delta_bob_edit.0.store, &delta_bob_edit.0.context);
174//! bob_store.join_or_replace_with(delta_alice_edit.0.store, &delta_alice_edit.0.context);
175//!
176//! // After merging, both stores are identical.
177//! assert_eq!(alice_store, bob_store);
178//!
179//! // 6. VERIFY CONFLICT
180//! // The concurrent writes are preserved as a conflict in the register.
181//! // The transaction API exposes this through the CrdtValue enum.
182//! use dson::transaction::CrdtValue;
183//!
184//! let tx = alice_store.transact(alice_id);
185//! match tx.get(&key) {
186//! Some(CrdtValue::Register(reg)) => {
187//! // Read all concurrent values
188//! let values: Vec<_> = reg.values().into_iter().collect();
189//! assert_eq!(values.len(), 2);
190//! assert!(values.contains(&&MvRegValue::String("from Alice".to_string())));
191//! assert!(values.contains(&&MvRegValue::String("from Bob".to_string())));
192//! }
193//! _ => panic!("Expected register with conflict"),
194//! }
195//! ```
196//!
197//! For more examples of the transaction API, including nested structures and performance
198//! considerations, see the [`transaction`] module documentation.
199//!
200//! ## Advanced Topics
201//!
202//! ### The Extension System
203//!
204//! DSON includes an extension system that allows developers to define custom CRDTs by
205//! implementing the [`ExtensionType`] trait. This is for building domain-specific data
206//! structures that go beyond the standard JSON-like primitives.
207//!
208//! By implementing the [`ExtensionType`] trait, you define how your custom type should be
209//! serialized, deserialized, and merged. The system handles conflict resolution based on
210//! the rules you define.
211//!
212//! This can be used to implement custom data structures like counters, text objects, or
213//! more efficient state representation.
214//!
215//! ### Validation and Observation
216//!
217//! DSON provides a [`Sentinel`](crate::sentinel::Sentinel) trait that allows you to observe or
218//! validate changes as they are applied during a merge. This can be used for implementing
219//! authorization, logging, or triggering side effects.
220//!
221//! ## Network and Consistency
222//!
223//! DSON's delta-based approach minimizes the amount of data that needs to be transmitted
224//! between replicas, making it efficient for low-bandwidth or high-latency networks.
225//!
226//! However, much of the complexity of using DSON in practice lies in the correct design and
227//! implementation of the gossip protocol used to exchange deltas between replicas. An
228//! efficient gossip protocol is not trivial to implement. For guidance, refer to the
229//! research on [opportunistic networking (oppnet)][oppnet].
230//!
231//! It is also important to understand that DSON's causal consistency guarantees are provided on
232//! a per-register basis. This means that while individual values are guaranteed to be causally
233//! consistent, the relationships between different values are not. This can lead to very
234//! unintuitive behavior.
235//! For example, if you have two registers, `x` and `y`, you write to `x` and then to `y`,
236//! another replica might see the write to `y` before the write to `x`.
237//!
238//! ## License
239//!
240//! This project is licensed under either of
241//!
242//! - Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0)
243//! - MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT)
244//!
245//! at your option.
246//!
247//! ## Features
248//!
249//! - `json`: Enables serialization and deserialization of DSON documents to and from
250//! `serde_json::Value`. This feature is enabled by default.
251//! - `serde`: Provides `serde` support for all CRDT types.
252//! - `arbitrary`: Implements `quickcheck::Arbitrary` for CRDT types, useful for property-based testing.
253//! - `chrono`: Enables `chrono` support for `Timestamp`. This feature is enabled by default.
254//! - `ulid`: Enables registers to hold ulids. This feature is enabled by default.
255#[cfg(test)]
256#[macro_use(quickcheck)]
257extern crate quickcheck_macros;
258
259use ahash::RandomState;
260use std::{
261 fmt,
262 hash::BuildHasher,
263 ops::BitAnd,
264 sync::atomic::{AtomicBool, Ordering},
265};
266
267// Use a constant seed for hashing to make performance benchmarks have less variance.
268pub(crate) const DETERMINISTIC_HASHER: RandomState = RandomState::with_seeds(48, 1516, 23, 42);
269
270pub mod causal_context;
271pub use causal_context::{
272 CausalContext, Dot, Identifier, MAX_APPLICATION_ID, NodeId, Priority, ROOT_APP_ID,
273};
274mod dotstores;
275pub use dotstores::{
276 CausalDotStore, DotChange, DotFun, DotFunMap, DotFunValueIter, DotMap, DotStore, DotStoreJoin,
277 DryJoinOutput,
278};
279pub mod crdts;
280pub use crdts::{mvreg::MvReg, orarray::OrArray, ormap::OrMap};
281pub mod api;
282/// Transaction-based API for ergonomic CRDT mutations.
283///
284/// See [`transaction`] module documentation for details and examples.
285pub mod transaction;
286pub use transaction::Delta;
287#[cfg(feature = "chrono")]
288pub mod datetime_literal;
289pub mod either;
290#[cfg(feature = "json")]
291mod json;
292/// Macros usable for tests and initialization
293pub mod macros;
294pub mod sentinel;
295
296// re-export for the datetime-literal macro
297#[cfg(feature = "chrono")]
298pub use chrono;
299
300// for [``] auto-linking
301#[cfg(doc)]
302use crdts::TypeVariantValue;
303
304static ENABLE_DETERMINISM: AtomicBool = AtomicBool::new(false);
305
306/// Makes all data structures behave deterministically.
307///
308/// This should only be enabled for testing, as it increases the odds of DoS
309/// scenarios.
310#[doc(hidden)]
311pub fn enable_determinism() {
312 ENABLE_DETERMINISM.store(true, Ordering::Release);
313}
314
315/// Checks if determinism is enabled.
316///
317/// Should be used internally and for testing.
318#[doc(hidden)]
319pub fn determinism_enabled() -> bool {
320 ENABLE_DETERMINISM.load(Ordering::Acquire)
321}
322
323/// Create a random state for a hashmap.
324/// If `enable_determinism` has been used, this will return a deterministic
325/// decidedly non-random RandomState, useful in tests.
326#[inline]
327fn make_random_state() -> RandomState {
328 if determinism_enabled() {
329 DETERMINISTIC_HASHER
330 } else {
331 // Create an instance of the standard ahash random state.
332 // This will be random, and will not be the same for any two runs.
333 RandomState::new()
334 }
335}
336
337fn create_map<K, V>() -> std::collections::HashMap<K, V, DsonRandomState> {
338 std::collections::HashMap::with_hasher(DsonRandomState::default())
339}
340
341fn create_map_with_capacity<K, V>(
342 capacity: usize,
343) -> std::collections::HashMap<K, V, DsonRandomState> {
344 std::collections::HashMap::with_capacity_and_hasher(capacity, DsonRandomState::default())
345}
346
347/// This is a small wrapper around the standard RandomState.
348/// This allows us to easily switch to a non-random RandomState for use in tests.
349#[derive(Clone)]
350pub struct DsonRandomState {
351 inner: RandomState,
352}
353
354// Implement default, falling back on regular ahash::RandomState except
355// when 'enable_determinism' has been called, in which case a static
356// only-for-test RandomState is used.
357impl Default for DsonRandomState {
358 #[inline]
359 fn default() -> Self {
360 Self {
361 inner: make_random_state(),
362 }
363 }
364}
365
366// We implement BuildHasher for DsonRandomState, but all we do is delegate to
367// the wrapped 'inner' RandomState.
368//
369// This construct allows us to easily use a deterministic RandomState (i.e, not random :-) ),
370// for tests.
371//
372// Since DsonRandomState implements default, the user doesn't have to do anything more than
373// specialize their hashmap using DsonRandomState instead of RandomState.
374impl BuildHasher for DsonRandomState {
375 type Hasher = <RandomState as BuildHasher>::Hasher;
376
377 #[inline]
378 fn build_hasher(&self) -> Self::Hasher {
379 self.inner.build_hasher()
380 }
381}
382
383/// A type that extends [`TypeVariantValue`] and friends with additional value types.
384///
385/// If you are looking for an implementor of this trait to stick with the standard DSON/JSON types,
386/// use [`crdts::NoExtensionTypes`].
387///
388/// The compiler should guide you towards all the various other traits and types you need in order
389/// to satisfy this trait once you add an impl of it.
390///
391/// In terms of mental model, think of the type that directly implements this trait as a direct
392/// analogue of [`TypeVariantValue`]. That is, it should generally be a struct with one `Option`
393/// field for each possible kind of custom value type. It needs to be a struct, not an enum, so
394/// that it can represent conflicts in type changes (for example, one writer sets a value to custom kind A
395/// and another sets it to custom kind B concurrently). [`ExtensionType::Value`] is used in
396/// situations where it is known that only a single kind is held.
397/// [`ExtensionType::coerce_to_value_ref`] is the main way in which such type conflicts are
398/// resolved.
399///
400/// The sub-types ("kinds") of a custom extension type must all be CRDTs, which in turn makes the
401/// implementing type also a CRDT assuming it follows the directions above. This is represented by
402/// the requirement that both `Self` and `ExtensionType::Value` implement [`DotStore`].
403///
404/// Implementors of this trait are generally used wherever `<Custom>` or `<C>` appears.
405pub trait ExtensionType: DotStore + Default {
406 /// Represents the kind of the underlying type without holding any data.
407 ///
408 /// This is the extension equivalent of [`crdts::ValueType`], and will likely be a simple
409 /// data-less enum.
410 type ValueKind: Copy + fmt::Debug;
411
412 /// Type that holds a known, single kind of this type.
413 ///
414 /// This is the extension equivalent of [`crdts::Value`], and will likely be an enum where each
415 /// variant holds one of the field types of `Self`.
416 ///
417 /// Since each sub-type should be a CRDT, this type should trivially implement [`DotStore`] by
418 /// forwarding to the [`DotStore`] implementation of the contained sub-type.
419 ///
420 /// Since `Self` is expected to be able to hold all sub-types (potentially more than one at a
421 /// time), this type should be trivial to turn into `Self`.
422 type Value: fmt::Debug + Clone + PartialEq + DotStore + Into<Self>;
423
424 /// Type that holds a reference to a known, single kind of this type.
425 ///
426 /// This is the extension equivalent of [`crdts::ValueRef`], and will likely be an enum where
427 /// each variant holds a `&` to one of the field types of `Self` (as indicated by the
428 /// `From<&Self::Value>` requirement).
429 ///
430 /// This type is generally used to represent a view into sub-tree of a DSON document. That
431 /// sub-tree is then read using [`crdts::snapshot::ToValue`].
432 ///
433 /// Since this type is required to implement `Copy` (it is supposed to just be a reference
434 /// type), it is expected to directly implement [`Into`] for [`ExtensionType::ValueKind`] as
435 /// opposed to going via a `&self` method.
436 ///
437 /// The requirement of `Into<Self::Value>` may seem odd, but serves as a replacement for
438 /// [`Clone`]. We can't use `Clone` since `Clone` is "special" when it comes to `&` -- the
439 /// compiler knows that when you call `Clone` on a `&T`, you want a `T` back, but it wouldn't
440 /// be as smart for `ValueRef`.
441 type ValueRef<'doc>: Copy
442 + fmt::Debug
443 + From<&'doc Self::Value>
444 + Into<Self::Value>
445 + crdts::snapshot::ToValue
446 + Into<Self::ValueKind>
447 where
448 Self: 'doc;
449
450 /// Coerces the potentially type-conflicted value in `self` into a single-typed
451 /// [`Self::ValueRef`].
452 ///
453 /// This is an inherently lossy operation -- if a type conflict exists in `self`, this has to
454 /// pick which type should be exposed when the document is read. This is required since the
455 /// types in [`crdts::snapshot`] cannot represent type conflicts, only value conflicts.
456 ///
457 /// This is the extension equivalent of [`TypeVariantValue::coerce_to_value_ref`], and will
458 /// generally be an `if-let` chain that returns a [`Self::ValueRef`] for the "first" sub-type
459 /// of `self` that is set. The ordering of the fields checked in the chain dictates the
460 /// inference-precedence for coercion in type conflicts.
461 fn coerce_to_value_ref(&self) -> Self::ValueRef<'_>;
462
463 /// Gives a short name to describe a given custom value type.
464 ///
465 /// Called by [`crdts::Value::type_name`] and [`crdts::ValueRef::type_name`].
466 fn type_name(value: &Self::ValueRef<'_>) -> &'static str;
467
468 /// Get the bottom value of this type
469 fn bottom() -> Self;
470}
471
472// NOTE: three arguments all of the same type -- big nope to have them be regular fn args.
473pub struct ComputeDeletionsArg<'a> {
474 /// Should be the causal context (ie, `.context`) of the more up to date `CausalDotStore`.
475 pub known_dots: &'a CausalContext,
476
477 /// Should be `store.dots()` of the more up to date `CausalDotStore`.
478 pub live_dots: &'a CausalContext,
479
480 /// Should be `store.dots()` of the `CausalDotStore` that may be missing deletes.
481 pub ignorant: &'a CausalContext,
482}
483
484/// Returns dots that `known_dots` has deleted (by virtue of not being in `live_dots`) that
485/// are still present in `ignorant`.
486///
487/// Conceptually computes `(known_dots - live_dots) & ignorant`.
488pub fn compute_deletions_unknown_to(
489 ComputeDeletionsArg {
490 known_dots,
491 live_dots,
492 ignorant,
493 }: ComputeDeletionsArg,
494) -> CausalContext {
495 // conceptually, this is:
496 //
497 // let deletes_ever = known_dots - live_dots;
498 // let relevant_deletes = deletes_ever & ignorant;
499 //
500 // however, deletes_ever ends up quite large, as it holds all deletes ever, which is
501 // wasteful since most of those dots then go away in the following set-intersection.
502 // we can use set theory to our advantage here[1], which states that (with \ denoting
503 // set subtraction):
504 //
505 // (L \ M) ∩ R = (L ∩ R) \ (M ∩ R)
506 // = (L ∩ R) \ M
507 // = L ∩ (R \ M)
508 //
509 // with
510 //
511 // L = known_dots
512 // M = live_dots
513 // R = ignorant
514 //
515 // [1]: https://en.wikipedia.org/wiki/List_of_set_identities_and_relations#(L\M)_%E2%81%8E_R
516 //
517 // many of these are significantly cheaper to compute than the original (both in memory
518 // and compute), especially when we take into account that intersection and subtraction
519 // are both O(left operand size). in particular, since ∩ is commutative, we can compute:
520 let only_in_ignorant = ignorant - live_dots;
521 only_in_ignorant.bitand(known_dots)
522 // the first part will be O(.store.dots()), and should result in a very small set. the
523 // second part iterates only over that small set, which should be cheap. at no point do
524 // we materialize a big set. its worth noting that all the sets involved here _should_
525 // already be fully compacted, but if that weren't the case we'd want compacted sets to
526 // be on the left-hand side.
527}