@loro-dev/flock
v3.1.0
Published
TypeScript bindings for the Flock CRDT with mergeable export/import utilities.
Readme
@loro-dev/flock
TypeScript bindings for the Flock Conflict-free Replicated Data Type (CRDT). Flock stores JSON-like values at composite keys, keeps causal metadata, and synchronises peers through mergeable export bundles. This package wraps the core MoonBit implementation and exposes a typed Flock class for JavaScript runtimes (Node.js ≥18, modern browsers, and workers).
Installation
pnpm add @loro-dev/flock
# or
npm install @loro-dev/flock
# or
yarn add @loro-dev/flockThe library ships ESM, CommonJS, and TypeScript declaration files. It has no runtime dependencies beyond the standard Web Crypto API for secure peer identifiers (falls back to Math.random when unavailable).
Quick Start
import { Flock } from "@loro-dev/flock";
// Each replica uses a stable UTF-8 peer id (<128 bytes) to maintain version vectors.
const peerId = crypto.randomUUID();
const store = new Flock(peerId);
store.put(["users", "42"], { name: "Ada", online: true });
console.log(store.get(["users", "42"]));
// Share incremental updates with a remote replica.
const bundle = store.exportJson();
// On another node, merge and materialise the same data.
const other = new Flock();
other.importJson(bundle);
other.merge(store);
// Subscribe to local or remote mutations.
const unsubscribe = store.subscribe(({ source, events }) => {
for (const { key, value } of events) {
console.log(`[${source}]`, key, value ?? "<deleted>");
}
});
store.delete(["users", "42"]);
unsubscribe();Replication Workflow
- Call
exportJson()to serialise local changes since an optional version vector. - Distribute bundles via your transport of choice (HTTP, WebSocket, etc.).
- Apply remote bundles with
importJson()and reconcile replicas usingmerge(). - Track causal progress with
version()andgetMaxPhysicalTime()when orchestrating incremental syncs. - Use
Flock.checkConsistency()in tests to assert that two replicas converged to the same state.
Type Basics
Value: JSON-compatible data (string,number,boolean,null, nested arrays/objects).KeyPart:string | number | boolean. Keys are arrays of parts (e.g.["users", 42]). Invalid keys raise at runtime.ExportRecord:{ c: string; d?: Value }– CRDT payload with hybrid logical clock data.ExportBundle:Record<string, ExportRecord>mapping composite keys to last-writer metadata.VersionVector:Record<string, { physicalTime: number; logicalCounter: number }>indexed by peer identifiers (UTF-8 strings ordered by their byte representation).ScanRow:{ key: KeyPart[]; raw: ExportRecord; value?: Value }returned byscan().EventBatch:{ source: string; events: Array<{ key: KeyPart[]; value?: Value }> }emitted to subscribers.
All types are exported from the package entry point for use in TypeScript projects.
API Reference
Constructor
new Flock(peerId?: string) – creates a replica. When omitted, a random 64-character hex peer id is generated. The id persists only in memory; persist it yourself for durable replicas.
Static Members
Flock.fromJson(bundle: ExportBundle, peerId: string): Flock– instantiate directly from a full snapshot bundle.Flock.checkConsistency(a: Flock, b: Flock): boolean– deep equality check useful for tests; returnstruewhen both replicas expose the same key/value pairs and metadata.
Replica Management
setPeerId(peerId: string): void– replace the current peer id. Use cautiously; changing ids affects causality tracking.peerId(): string– returns the identifier for the replica.getMaxPhysicalTime(): number– highest physical timestamp observed by this replica (same units as the timestamps you pass tonow, e.g.Date.now()output). Helpful for synchronising clocks and diagnosing divergence.version(): VersionVector– current version vector including logical counters per peer.checkInvariants(): void– throws if internal CRDT invariants are violated. Intended for assertions in tests or diagnostics, not for production control flow.
Mutations
put(key: KeyPart[], value: Value, now?: number): void– write a JSON value. The optionalnowoverrides the physical time (numeric timestamp such asDate.now()output) used for the replica’s hybrid logical clock. Invalid keys or non-finite timestamps throw.set(key: KeyPart[], value: Value, now?: number): void– alias ofputfor frameworks that prefer “set” terminology.delete(key: KeyPart[], now?: number): void– tombstone a key. The optional time override follows the same rules asput.putMvr(key: KeyPart[], value: Value, now?: number): void– attach a value to the key’s Multi-Value Register. Unlikeput, concurrent writes remain alongside each other.
Reads
get(key: KeyPart[]): Value | undefined– fetch the latest visible value. Deleted keys resolve toundefined.getMvr(key: KeyPart[]): Value[]– read all concurrent values associated with a key’s Multi-Value Register (empty array when unset).kvToJson(): ExportBundle– snapshot of every key/value pair including tombstones. Useful for debugging or serialising the entire store.scan(options?: ScanOptions): ScanRow[]– in-order range scan. Supports:start/end:{ kind: "inclusive" | "exclusive"; key: KeyPart[] }or{ kind: "unbounded" }.prefix: restrict results to keys beginning with the provided prefix. Results include the materialised value (if any) and raw CRDT record.
Replication
exportJson(from?: VersionVector): ExportBundle– export causal updates. Provide aVersionVectorfrom a remote replica to stream only novel changes; omit to export the entire dataset.importJson(bundle: ExportBundle): void– apply a bundle received from another replica. Invalid payloads throw.merge(other: Flock): void– merge another replica instance directly (both replicas end up with the joined state).
Events
subscribe(listener: (batch: EventBatch) => void): () => void– register for mutation batches. Each callback receives thesource("local" for writes on this replica, peer id string for remote batches when available) and an ordered list of events. Return value unsubscribes the listener.
Utilities
exportJson,importJson,kvToJson, andfromJsonall work with plain JavaScript objects, so they serialise cleanly through JSON or structured clone.- Keys are encoded using MoonBit’s memcomparable format; use simple scalars and natural ordering for predictable scans.
Error Handling
- Methods that accept keys validate them at runtime. Passing unsupported key parts (like objects or
undefined) throws aTypeError. - Passing malformed bundles or version vectors throws a
TypeErroror propagates an underlying runtime error from the native module. - Always wrap replication IO in
try/catchwhen dealing with untrusted data.
Testing Tips
- Use
Flock.checkConsistency()to assert two replicas match after a sequence of operations. checkInvariants()is safe inside unit tests to catch bugs in integration layers.- Vitest users can combine
exportJsonsnapshots withexpectto capture deterministic state transitions.
License
MIT © Loro contributors
