npm package: @loro-dev/flock
Description: TypeScript bindings for the Flock CRDT with mergeable export/import utilities.
Author: Loro contributors
Latest version: 4.4.4

# @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

```sh
pnpm add @loro-dev/flock
# or
npm install @loro-dev/flock
# or
yarn add @loro-dev/flock
```

The 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

```ts
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 using `merge()`.
- Track causal progress with `version()` and `getMaxPhysicalTime()` 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 by `scan()`.
- `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; returns `true` when 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 to `now`, e.g. `Date.now()` output). Helpful for synchronising clocks and diagnosing divergence.
- `version(): VersionVector` – exclusive version vector for currently visible peers. Complexity is `O(M + V log M + R (log L + log S))` (`M=memtablePeerCount`, `V=vvPeerCount`, `L=memtableLen`, `R=scanned candidate rows`, `S=storage key count`) and it does not perform a full `O(memtableSize)` pre-scan.
- `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 optional `now` overrides the physical time (numeric timestamp such as `Date.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 of `put` for frameworks that prefer “set” terminology.
- `delete(key: KeyPart[], now?: number): void` – tombstone a key. The optional time override follows the same rules as `put`.
- `putMvr(key: KeyPart[], value: Value, now?: number): void` – attach a value to the key’s Multi-Value Register. Unlike `put`, concurrent writes remain alongside each other.

### Reads

- `get(key: KeyPart[]): Value | undefined` – fetch the latest visible value. Deleted keys resolve to `undefined`.
- `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 a `VersionVector` from 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 the `source` ("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`, and `fromJson` all 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 a `TypeError`.
- Passing malformed bundles or version vectors throws a `TypeError` or propagates an underlying runtime error from the native module.
- Always wrap replication IO in `try/catch` when 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 `exportJson` snapshots with `expect` to capture deterministic state transitions.

## License

MIT © Loro contributors