# CRDTs
CRDT (Conflict-free Replicated Data Types) are essential for healthy peer-to-peer applications. They handle scattered, unordered updates and ensure all peers eventually converge to the same state, regardless of the order updates are received.
# Built-in CRDT Types
Tool Db includes three CRDT implementations:
| Type | Operations | Use Case |
|---|---|---|
| MapCRDT | SET, DEL | Key-value stores with per-key conflict resolution |
| ListCRDT | INS, PUSH, DEL | Ordered lists with concurrent insert support |
| CounterCRDT | ADD, SUB | Distributed counters that merge correctly |
# MapCRDT
A key-value map where each key tracks its own version history.
import { MapCrdt } from "tool-db";
// Create with the author's address
const map = new MapCrdt<number>(db.userAccount.getAddress() || "");
// Set values
map.SET("dogs", 3);
map.SET("cats", 2);
map.SET("birds", 1);
// Delete a key
map.DEL("birds");
// Get the current value
console.log(map.value); // { dogs: 3, cats: 2 }
# MapCRDT Operations
SET(key: string, value: T)— Sets a key to a valueDEL(key: string)— Deletes a key
# MapCRDT Resolution
When concurrent updates happen:
- Changes are sorted by index (version number)
- For equal indices, SET wins over DEL
- For equal indices and operation types, author address breaks ties
# ListCRDT
An ordered list that supports concurrent insertions.
import { ListCrdt } from "tool-db";
const list = new ListCrdt<string>(db.userAccount.getAddress() || "");
// Push items to the end
list.PUSH("first");
list.PUSH("second");
// Insert at a specific position
list.INS("inserted", 1); // Insert at index 1
// Delete by index
list.DEL(0); // Remove first item
console.log(list.value); // ["inserted", "second"]
# ListCRDT Operations
PUSH(value: T)— Appends an item to the endINS(value: T, index: number)— Inserts an item at a specific positionDEL(index: number)— Removes the item at the index (tombstones it)
# ListCRDT Resolution
The list uses position references (prev/next indices) to maintain order during concurrent edits. Deleted items are tombstoned (marked as deleted) rather than removed, ensuring convergence.
# CounterCRDT
A distributed counter that correctly merges additions and subtractions.
import { CounterCrdt } from "tool-db";
const counter = new CounterCrdt(db.userAccount.getAddress() || "");
// Add to the counter
counter.ADD(10);
counter.ADD(5);
// Subtract from the counter
counter.SUB(3);
console.log(counter.value); // 12
# CounterCRDT Operations
ADD(value: number)— Adds to the counterSUB(value: number)— Subtracts from the counter
# CounterCRDT Resolution
Each operation is tracked with the author and index, preventing duplicate application. All ADD/SUB operations are summed to get the final value.
# Storing CRDTs
Use putCrdt() to store CRDT changes:
const map = new MapCrdt<string>(db.userAccount.getAddress() || "");
map.SET("name", "Alice");
map.SET("status", "online");
// Store in user namespace
await db.putCrdt("my-profile", map, true);
# Retrieving CRDTs
Use getCrdt() to fetch and merge changes:
const map = new MapCrdt<string>(db.userAccount.getAddress() || "");
// Fetch and merge changes from network
await db.getCrdt("my-profile", map, true);
console.log(map.value); // { name: "Alice", status: "online" }
# Subscribing to CRDT Updates
Combine key listeners with subscriptions for real-time updates:
import { MapCrdt, MapChanges } from "tool-db";
const sharedMap = new MapCrdt<string>(db.userAccount.getAddress() || "");
const key = db.getUserNamespacedKey("shared-data");
// Listen for incoming changes
db.addKeyListener<MapChanges<string>[]>(key, (msg) => {
// Merge incoming changes
sharedMap.mergeChanges(msg.v);
console.log("Updated:", sharedMap.value);
});
// Subscribe to receive updates
db.subscribeData("shared-data", true);
// Initial fetch
await db.getCrdt("shared-data", sharedMap, true);
# Custom Verification
Filter incoming CRDT changes with custom verificators:
import { MapChanges, VerificationData } from "tool-db";
// Only allow SET operations, reject DEL
function noDeletesAllowed(
msg: VerificationData<MapChanges<number>[]>
): Promise<boolean> {
return new Promise((resolve) => {
const hasDelete = msg.v.some(change => change.t === "DEL");
resolve(!hasDelete); // Reject if any DEL found
});
}
// Apply to a key
db.addCustomVerification<MapChanges<number>[]>(
db.getUserNamespacedKey("protected-data"),
noDeletesAllowed
);
WARNING
Custom verification filters the entire message. If one change in the array fails verification, all changes in that message are rejected (to preserve hash integrity).
# CRDT Change Types
# MapChanges
type MapOperations = "SET" | "DEL";
interface SetMapChange<T> {
t: "SET"; // Operation type
k: string; // Key
v: T; // Value
a: string; // Author address
i: number; // Index/version
}
interface DelMapChange<T> {
t: "DEL";
k: string;
a: string;
i: number;
}
type MapChanges<T> = SetMapChange<T> | DelMapChange<T>;
# ListChanges
type ListOperations = "INS" | "DEL";
interface InsListChange<T> {
t: "INS"; // Insert operation
v: T; // Value
i: string; // Unique index (author + sequence)
p: string | undefined; // Previous item index
n: string | undefined; // Next item index
}
interface DelListChange<T> {
t: "DEL";
v: string; // Target index to tombstone
i: string;
}
type ListChanges<T> = InsListChange<T> | DelListChange<T>;
# CounterChanges
type CounterOperations = "ADD" | "SUB";
interface CounterChange {
t: CounterOperations; // ADD or SUB
v: number; // Value to add/subtract
a: string; // Author address
i: number; // Index/sequence
}
# Creating Custom CRDTs
Extend BaseCrdt to create custom CRDT types:
import { BaseCrdt } from "tool-db";
interface MyChange {
t: "SET";
v: string;
a: string;
i: number;
}
class MyCrdt extends BaseCrdt<string, MyChange, string> {
public type = "MY_CRDT";
private _changes: MyChange[] = [];
private _value = "";
private _author: string;
constructor(author: string) {
super();
this._author = author;
}
mergeChanges(changes: MyChange[]) {
// Add unique changes
changes.forEach(change => {
if (!this._changes.some(c => c.i === change.i && c.a === change.a)) {
this._changes.push(change);
}
});
this.calculate();
}
getChanges(): MyChange[] {
return this._changes;
}
get value(): string {
return this._value;
}
private calculate() {
// Sort and compute value
this._changes.sort((a, b) => a.i - b.i);
this._value = this._changes[this._changes.length - 1]?.v || "";
}
SET(value: string) {
const ourChanges = this._changes.filter(c => c.a === this._author);
this._changes.push({
t: "SET",
v: value,
a: this._author,
i: ourChanges.length,
});
this.calculate();
}
}
# Performance Considerations
- CRDTs store a history of all changes, which grows over time
- Consider periodic compaction for long-lived CRDTs
- MapCRDT only stores the latest version per key after sorting
- ListCRDT tombstones deleted items (they're marked but retained)
- CounterCRDT retains all operations for correct merge behavior
TIP
For high-frequency updates, consider batching changes and calling putCrdt() less frequently.
# Using External CRDT Libraries
You can use libraries like Automerge (opens new window) or Yjs (opens new window) by wrapping them in a BaseCrdt class:
import { BaseCrdt } from "tool-db";
import * as Automerge from "@automerge/automerge";
class AutomergeCrdt<T> extends BaseCrdt<T, Uint8Array, T> {
public type = "AUTOMERGE";
private doc: Automerge.Doc<T>;
constructor(initial: T) {
super();
this.doc = Automerge.init<T>();
this.doc = Automerge.change(this.doc, d => Object.assign(d, initial));
}
mergeChanges(changes: Uint8Array[]) {
changes.forEach(change => {
this.doc = Automerge.applyChanges(this.doc, [change])[0];
});
}
getChanges(): Uint8Array[] {
return Automerge.getAllChanges(this.doc);
}
get value(): T {
return this.doc;
}
}
← Namespaces Adapters →