@loro-dev/flock-wasm 0.1.3 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,13 +1,1288 @@
1
1
  # @loro-dev/flock-wasm
2
2
 
3
- WASM bindings for `flock-rs`.
3
+ WASM bindings for `flock-rs` — a distributed, conflict-free key-value store built on CRDT (Conflict-free Replicated Data Type) principles.
4
4
 
5
- ## Runtime mode
5
+ Flock enables multiple replicas to independently read and write data, merge changes without coordination, and converge deterministically to the same state. It is designed for offline-first, peer-to-peer, and decentralized applications where network partitions are expected and no central authority is required.
6
6
 
7
- - `RawFlock` constructors (`new`, `fromJson`, `fromFile`) use buffered mode by default.
8
- - Buffered mode means memtable is enabled by default.
9
- - Buffered writes are flushed on `txnCommit` (and storage transaction commit), or when exporting file bytes.
10
- - If you need strongest durability semantics, explicitly commit transactions more frequently.
7
+ ## Table of Contents
8
+
9
+ - [Install](#install)
10
+ - [Core Concepts](#core-concepts)
11
+ - [Quick Start](#quick-start)
12
+ - [Runtime Modes and Buffering](#runtime-modes-and-buffering)
13
+ - [API Reference](#api-reference)
14
+ - [Top-Level Exports](#top-level-exports)
15
+ - [Flock Constructors](#flock-constructors)
16
+ - [Identity and Diagnostics](#identity-and-diagnostics)
17
+ - [Data Operations](#data-operations)
18
+ - [Multi-Value Register (MVR)](#multi-value-register-mvr)
19
+ - [Scan Operations](#scan-operations)
20
+ - [Replication and Import/Export](#replication-and-importexport)
21
+ - [Hooks API](#hooks-api)
22
+ - [Events and Subscriptions](#events-and-subscriptions)
23
+ - [Debounced Event Batching](#debounced-event-batching)
24
+ - [Transactions](#transactions)
25
+ - [Binary File Export/Import](#binary-file-exportimport)
26
+ - [Type Reference](#type-reference)
27
+ - [Conflict Resolution Deep Dive](#conflict-resolution-deep-dive)
28
+ - [Version Vectors Explained](#version-vectors-explained)
29
+ - [Best Practices](#best-practices)
30
+ - [Common Pitfalls](#common-pitfalls)
31
+ - [Build](#build)
32
+ - [Test](#test)
33
+ - [License](#license)
34
+
35
+ ## Install
36
+
37
+ ```bash
38
+ npm add @loro-dev/flock-wasm
39
+ # or
40
+ pnpm add @loro-dev/flock-wasm
41
+ # or
42
+ yarn add @loro-dev/flock-wasm
43
+ ```
44
+
45
+ ## Core Concepts
46
+
47
+ ### CRDT with Last-Writer-Wins (LWW) Semantics
48
+
49
+ Every entry in Flock carries a **clock** consisting of three components:
50
+
51
+ 1. **Physical time** — wall-clock timestamp in milliseconds.
52
+ 2. **Logical counter** — monotonic counter to break ties at the same physical time.
53
+ 3. **Peer ID** — unique identifier of the writer.
54
+
55
+ When two replicas have conflicting values for the same key, Flock resolves deterministically:
56
+
57
+ 1. Higher `physicalTime` wins.
58
+ 2. On tie, higher `logicalCounter` wins.
59
+ 3. On tie, lexicographically greater `peerId` wins.
60
+
61
+ All replicas applying the same rule to the same set of entries converge to the identical value — no coordination needed.
62
+
63
+ ### Keys and Values
64
+
65
+ - **Keys** are arrays of JSON-serializable parts: `["users", 42, "name"]`. Keys are encoded into a memcomparable binary format, enabling efficient sorted storage and prefix-based range scans.
66
+ - **Values** are any JSON-serializable type: strings, numbers, booleans, null, arrays, or nested objects.
67
+ - **Metadata** is an optional `Record<string, unknown>` attached to each entry, useful for signatures, policy hints, or audit information.
68
+
69
+ ### Tombstones
70
+
71
+ Deletions create **tombstone** entries rather than removing data. A tombstone carries a clock but no data. Tombstones:
72
+
73
+ - Are visible in `scan()` results (with `value: undefined`).
74
+ - Participate in conflict resolution — a delete with a higher clock wins over an older write.
75
+ - Can be pruned during export via `pruneTombstonesBefore`.
76
+ - Ensure that "deleted" and "never existed" are distinguishable via `getEntry()`.
77
+
78
+ ### Version Vectors
79
+
80
+ A version vector is a compact summary of the visible state: `{ [peerId]: { physicalTime, logicalCounter } }`. Two types exist:
81
+
82
+ - **Exclusive version** (`version()`): Only includes peers that currently own at least one visible entry. Use this as the baseline for incremental sync.
83
+ - **Inclusive version** (`inclusiveVersion()`): Tracks max seen clocks across all peers ever encountered. Use this for completeness checks.
84
+
85
+ ## Quick Start
86
+
87
+ ```ts
88
+ import { Flock } from "@loro-dev/flock-wasm";
89
+
90
+ // Create two independent replicas
91
+ const alice = new Flock("alice");
92
+ const bob = new Flock("bob");
93
+
94
+ // Alice writes some data
95
+ alice.put(["todos", 1], { title: "Write docs", done: false });
96
+ alice.put(["todos", 2], { title: "Ship feature", done: false });
97
+
98
+ // Export Alice's state and import into Bob
99
+ const snapshot = alice.exportJson();
100
+ bob.importJson(snapshot);
101
+
102
+ // Bob sees Alice's data
103
+ console.log(bob.get(["todos", 1])); // { title: "Write docs", done: false }
104
+
105
+ // Both edit concurrently
106
+ alice.put(["todos", 1], { title: "Write docs", done: true });
107
+ bob.put(["todos", 1], { title: "Write better docs", done: false });
108
+
109
+ // Merge resolves conflict via LWW (higher timestamp wins)
110
+ alice.merge(bob);
111
+ bob.merge(alice);
112
+
113
+ // Both converge to the same value
114
+ console.log(alice.get(["todos", 1])); // same result
115
+ console.log(bob.get(["todos", 1])); // same result
116
+ ```
117
+
118
+ ## Runtime Modes and Buffering
119
+
120
+ All `Flock` constructors (`new`, `fromJson`, `fromFile`) use **buffered mode** by default:
121
+
122
+ - **Memtable enabled**: Writes are accumulated in an in-memory buffer.
123
+ - **Flushed on commit**: Buffered writes are flushed when `txnCommit()` is called, when the storage transaction commits, or when exporting file bytes.
124
+ - **Implication**: For strongest durability semantics, explicitly commit transactions or call `exportFile()` more frequently.
125
+
126
+ ## API Reference
127
+
128
+ ### Top-Level Exports
129
+
130
+ ```ts
131
+ import {
132
+ Flock,
133
+ encodeVersionVector,
134
+ decodeVersionVector,
135
+ type VersionVector,
136
+ type VersionVectorEntry,
137
+ type ExportBundle,
138
+ type ImportReport,
139
+ type ScanOptions,
140
+ type ScanRow,
141
+ type ScanBound,
142
+ type EventBatch,
143
+ type Event,
144
+ type EventPayload,
145
+ type EntryInfo,
146
+ type EntryClock,
147
+ type ExportRecord,
148
+ type MetadataMap,
149
+ type Value,
150
+ type KeyPart,
151
+ type ExportPayload,
152
+ type ExportHookContext,
153
+ type ExportHooks,
154
+ type ImportPayload,
155
+ type ImportHookContext,
156
+ type ImportAccept,
157
+ type ImportSkip,
158
+ type ImportDecision,
159
+ type ImportHooks,
160
+ type PutPayload,
161
+ type PutHookContext,
162
+ type PutHooks,
163
+ type PutWithMetaOptions,
164
+ } from "@loro-dev/flock-wasm";
165
+ ```
166
+
167
+ ---
168
+
169
+ ### `encodeVersionVector(vector: VersionVector): Uint8Array`
170
+
171
+ Encodes a version vector into a compact binary representation.
172
+
173
+ - **Input**: A `VersionVector` object mapping peer IDs to `{ physicalTime, logicalCounter }`.
174
+ - **Output**: `Uint8Array` with `VEVE` magic prefix, ULEB128-encoded timestamps, and delta-compressed entries.
175
+ - **Use case**: Persist or transmit version vectors efficiently. The binary format is deterministic — same logical content always produces identical bytes.
176
+ - **Skips**: Entries with invalid peer IDs (non-string or >= 128 UTF-8 bytes) or non-finite timestamps are silently ignored.
177
+
178
+ ### `decodeVersionVector(bytes: Uint8Array): VersionVector`
179
+
180
+ Decodes a binary version vector back into a `VersionVector` object.
181
+
182
+ - **Input**: `Uint8Array` produced by `encodeVersionVector`.
183
+ - **Output**: `VersionVector` object.
184
+ - **Compatibility**: Handles both the current `VEVE`-prefixed format and the legacy format (no magic prefix) transparently.
185
+ - **Throws**: `TypeError` if peer IDs are invalid or timestamps are non-monotonic.
186
+
187
+ ---
188
+
189
+ ### Flock Constructors
190
+
191
+ #### `new Flock(peerId?: string)`
192
+
193
+ Creates a new empty Flock instance.
194
+
195
+ - **`peerId`** (optional): A UTF-8 string under 128 bytes identifying this peer. If omitted, a cryptographically random 64-character hex string is generated.
196
+ - **Returns**: A new `Flock` instance in buffered mode.
197
+ - **Throws**: `TypeError` if `peerId` is not a valid string or exceeds 127 UTF-8 bytes.
198
+ - **Side effects**: None. The instance starts with empty state and no version history.
199
+
200
+ ```ts
201
+ const named = new Flock("peer-alice");
202
+ const anonymous = new Flock(); // random 64-char hex ID
203
+ ```
204
+
205
+ #### `Flock.fromJson(bundle: ExportBundle, peerId: string): Flock`
206
+
207
+ Creates a new Flock instance pre-populated with state from a JSON export bundle.
208
+
209
+ - **`bundle`**: An `ExportBundle` obtained from `exportJson()` on another instance.
210
+ - **`peerId`**: The peer ID for the new instance (required).
211
+ - **Returns**: A new `Flock` with merged state from the bundle.
212
+ - **Throws**: If the bundle format is invalid or import fails.
213
+ - **Use case**: Bootstrap a new replica from a snapshot received over the network.
214
+
215
+ ```ts
216
+ const snapshot = source.exportJson();
217
+ const replica = Flock.fromJson(snapshot, "new-peer");
218
+ ```
219
+
220
+ #### `Flock.fromFile(bytes: Uint8Array, peerId?: string): Flock`
221
+
222
+ Creates a new Flock instance from a binary file export. This is the **fastest way to load state** — the pre-built B+Tree pages are used directly without re-parsing or re-indexing (O(1) open time vs. O(n log n) for `fromJson`).
223
+
224
+ - **`bytes`**: `Uint8Array` produced by `exportFile()`.
225
+ - **`peerId`** (optional): Peer ID for the new instance. Random if omitted.
226
+ - **Returns**: A new `Flock` with full state restored from the binary format.
227
+ - **Throws**: If the binary format is invalid (wrong magic bytes, corrupted pages, CRC mismatch).
228
+ - **Use case**: Fast cold-start, restore from disk, or load a persisted checkpoint. Prefer this over `fromJson` when loading full state.
229
+
230
+ ```ts
231
+ const bytes = fs.readFileSync("state.db");
232
+ const restored = Flock.fromFile(new Uint8Array(bytes), "restored-peer");
233
+ ```
234
+
235
+ ---
236
+
237
+ ### Identity and Diagnostics
238
+
239
+ #### `peerId(): string`
240
+
241
+ Returns the current peer ID of this Flock instance.
242
+
243
+ - **Returns**: The peer ID string (always valid UTF-8, under 128 bytes).
244
+ - **Throws**: `TypeError` if the internal FFI returns an unexpected value (should not happen in normal operation).
245
+ - **Note**: For anonymous instances, returns the auto-generated 64-character hex string.
246
+
247
+ #### `setPeerId(peerId: string): void`
248
+
249
+ Changes the peer ID for subsequent operations.
250
+
251
+ - **`peerId`**: New peer ID (UTF-8, under 128 bytes).
252
+ - **Effect**: All future `put`, `delete`, and `putWithMeta` calls will be attributed to the new peer ID.
253
+ - **Does NOT retroactively change** previous entries — those remain attributed to the original peer.
254
+ - **Throws**: `TypeError` if the peer ID is invalid.
255
+
256
+ ```ts
257
+ flock.setPeerId("peer-a");
258
+ flock.put(["k"], "by-a"); // attributed to "peer-a"
259
+ flock.setPeerId("peer-b");
260
+ flock.put(["k"], "by-b"); // attributed to "peer-b", overwrites "by-a" if clock is higher
261
+ ```
262
+
263
+ #### `checkInvariants(): void`
264
+
265
+ Validates the internal CRDT state for consistency.
266
+
267
+ - **Effect**: Exercises `version()`, `inclusiveVersion()`, and `scan()` to check structural integrity.
268
+ - **Throws**: If any internal invariant is violated.
269
+ - **Use case**: Debugging and testing. Not needed in production.
270
+
271
+ #### `getMaxPhysicalTime(): number`
272
+
273
+ Returns the maximum physical timestamp across all entries.
274
+
275
+ - **Returns**: A `number` (milliseconds). Returns `0` for an empty Flock.
276
+ - **Use case**: Determine a safe threshold for tombstone pruning. For example, `pruneTombstonesBefore: flock.getMaxPhysicalTime() - 86400000` prunes tombstones older than 24 hours.
277
+ - **Note**: This value only increases monotonically during the lifetime of the instance.
278
+
279
+ ---
280
+
281
+ ### Data Operations
282
+
283
+ #### `put(key: KeyPart[], value: Value, now?: number): void`
284
+
285
+ Stores a value at the specified key using LWW semantics.
286
+
287
+ - **`key`**: Array of JSON-serializable parts (e.g., `["users", 42]`).
288
+ - **`value`**: Any JSON-serializable value (string, number, boolean, null, array, object).
289
+ - **`now`** (optional): Explicit physical timestamp in milliseconds. If omitted, `Date.now()` is used.
290
+ - **Effect**: Creates or overwrites the entry at `key`. If the key already exists, the new entry wins only if its clock is higher than the existing entry's clock.
291
+ - **Event**: Emits a change event to all subscribers (subject to debounce/transaction batching).
292
+ - **Throws**: If the key or value is not JSON-serializable.
293
+
294
+ **Important**: `put` always succeeds locally — the LWW comparison happens during merge with remote state. Locally, `put` always overwrites because the local clock is guaranteed to advance.
295
+
296
+ ```ts
297
+ flock.put(["config", "theme"], "dark");
298
+ flock.put(["scores", 1], 100, Date.now());
299
+ flock.put(["nested"], { a: { b: [1, 2, 3] } });
300
+ ```
301
+
302
+ #### `set(key: KeyPart[], value: Value, now?: number): void`
303
+
304
+ Alias for `put()`. Identical behavior.
305
+
306
+ #### `putWithMeta(key: KeyPart[], value: Value, options?: PutWithMetaOptions): void | Promise<void>`
307
+
308
+ Stores a value with optional metadata and/or transform hooks.
309
+
310
+ - **`key`**: Key array.
311
+ - **`value`**: Value to store.
312
+ - **`options.metadata`** (optional): A `MetadataMap` (`Record<string, unknown>`) to associate with this entry. Useful for signatures, audit trails, or policy tags.
313
+ - **`options.now`** (optional): Explicit timestamp.
314
+ - **`options.hooks.transform`** (optional): An async function `(context, payload) => payload | void` that can modify the data and metadata before storage. If provided, the method returns a `Promise<void>`.
315
+ - **Returns**: `void` if no hooks; `Promise<void>` if hooks are present.
316
+ - **Throws**: `TypeError` if the transform hook returns a payload without a `data` field.
317
+ - **Event**: Emits a change event after the (possibly async) operation completes.
318
+
319
+ ```ts
320
+ // Simple metadata
321
+ flock.putWithMeta(["doc", 1], "content", {
322
+ metadata: { author: "alice", signature: "abc123" },
323
+ });
324
+
325
+ // With transform hook (async)
326
+ await flock.putWithMeta(["doc", 1], "content", {
327
+ metadata: { sig: "pending" },
328
+ hooks: {
329
+ transform: async (ctx, payload) => {
330
+ payload.metadata = { ...payload.metadata, sig: await sign(payload.data) };
331
+ return payload;
332
+ },
333
+ },
334
+ });
335
+ ```
336
+
337
+ #### `delete(key: KeyPart[], now?: number): void`
338
+
339
+ Deletes the value at the specified key by creating a tombstone.
340
+
341
+ - **`key`**: Key array.
342
+ - **`now`** (optional): Explicit timestamp.
343
+ - **Effect**: Creates a tombstone entry. `get(key)` will return `undefined` after deletion.
344
+ - **Important**: The tombstone participates in conflict resolution. A delete with a lower clock than an existing write will be ignored during merge. A delete with a higher clock will win.
345
+ - **No-op on missing keys**: Deleting a key that has never been written is a no-op — no tombstone is created and no event is emitted. Deleting an already-deleted key is also a no-op (the existing tombstone is kept).
346
+ - **Event**: Emits a change event with `value: undefined` when an existing entry is deleted.
347
+
348
+ ```ts
349
+ flock.put(["temp"], "data");
350
+ flock.delete(["temp"]);
351
+ flock.get(["temp"]); // undefined
352
+ ```
353
+
354
+ #### `get(key: KeyPart[]): Value | undefined`
355
+
356
+ Retrieves the current value at the specified key.
357
+
358
+ - **`key`**: Key array.
359
+ - **Returns**: The stored value, or `undefined` if the key does not exist or has been deleted.
360
+ - **Note**: Does not distinguish between "never set" and "deleted". Use `getEntry()` for that distinction.
361
+ - **Graceful**: If the key array cannot be decoded (e.g., contains non-serializable types), returns `undefined` instead of throwing.
362
+
363
+ ```ts
364
+ flock.put(["k"], 42);
365
+ flock.get(["k"]); // 42
366
+ flock.get(["missing"]); // undefined
367
+ ```
368
+
369
+ #### `getEntry(key: KeyPart[]): EntryInfo | undefined`
370
+
371
+ Retrieves the full entry including data, metadata, and clock information.
372
+
373
+ - **`key`**: Key array.
374
+ - **Returns**: An `EntryInfo` object `{ data?, metadata, clock }`, or `undefined` if the key has never been written.
375
+ - **Tombstone distinction**: For deleted keys, returns an object with `data` omitted but `clock` and `metadata` present. For never-set keys, returns `undefined`.
376
+ - **Metadata**: Always present as an object (defaults to `{}` when no metadata was stored).
377
+ - **Use case**: Auditing (who wrote when), tombstone detection, signature verification.
378
+
379
+ ```ts
380
+ flock.putWithMeta(["k"], "value", { metadata: { author: "alice" } });
381
+ const entry = flock.getEntry(["k"]);
382
+ // {
383
+ // data: "value",
384
+ // metadata: { author: "alice" },
385
+ // clock: { physicalTime: 1700000000000, logicalCounter: 0, peerId: "alice" }
386
+ // }
387
+
388
+ flock.delete(["k"]);
389
+ const tombstone = flock.getEntry(["k"]);
390
+ // {
391
+ // metadata: {},
392
+ // clock: { physicalTime: 1700000000001, logicalCounter: 0, peerId: "alice" }
393
+ // }
394
+ // Note: `data` field is absent (not `undefined` — it is omitted)
395
+
396
+ flock.getEntry(["never-set"]); // undefined
397
+ ```
398
+
399
+ ---
400
+
401
+ ### Multi-Value Register (MVR)
402
+
403
+ MVR is a pattern built on top of Flock's LWW key-value store that allows concurrent writes from different peers to coexist instead of one winning. It works by appending the value to the key and storing `true` as a marker.
404
+
405
+ #### `putMvr(key: KeyPart[], value: Value, now?: number): void`
406
+
407
+ Stores a scalar value using the MVR pattern.
408
+
409
+ - **`key`**: Base key array (the MVR "slot").
410
+ - **`value`**: Must be a **scalar** JSON value: `string`, `number`, or `boolean`. Objects, arrays, and `null` are rejected.
411
+ - **`now`** (optional): Explicit timestamp.
412
+ - **Effect**: Deletes all existing MVR entries under the base key (where `value === true`), regardless of which peer wrote them, then writes `true` at `[...key, value]`.
413
+ - **Throws**: `TypeError` if `value` is null, an array, or an object.
414
+ - **Event**: Emits delete events for removed entries and a put event for the new entry.
415
+
416
+ **How it works internally**:
417
+
418
+ ```
419
+ putMvr(["mv"], "one") →
420
+ scan prefix ["mv"] and delete all entries where value === true
421
+ put(["mv", "one"], true)
422
+ ```
423
+
424
+ #### `getMvr(key: KeyPart[]): Value[]`
425
+
426
+ Retrieves all concurrent MVR values for a base key.
427
+
428
+ - **`key`**: Base key array.
429
+ - **Returns**: Array of scalar values. Empty `[]` if no values exist.
430
+ - **Behavior**: Scans all entries with prefix `key`, filters for entries at depth `key.length + 1` with `value === true`, and extracts the appended value part.
431
+
432
+ ```ts
433
+ // Single writer
434
+ flock.putMvr(["color"], "red");
435
+ flock.getMvr(["color"]); // ["red"]
436
+
437
+ // Concurrent writers merge
438
+ const a = new Flock("a");
439
+ const b = new Flock("b");
440
+ a.putMvr(["color"], "red");
441
+ b.putMvr(["color"], "blue");
442
+ a.merge(b);
443
+ a.getMvr(["color"]); // ["red", "blue"] (order may vary)
444
+
445
+ // Calling putMvr replaces ALL values (not just the local peer's)
446
+ a.putMvr(["color"], "green");
447
+ a.getMvr(["color"]); // ["green"] — both "red" and "blue" were deleted
448
+ ```
449
+
450
+ ---
451
+
452
+ ### Scan Operations
453
+
454
+ #### `scan(options?: ScanOptions): ScanRow[]`
455
+
456
+ Scans entries within the specified key range and/or prefix.
457
+
458
+ - **`options.start`** (optional): Start bound for the scan range.
459
+ - **`options.end`** (optional): End bound for the scan range.
460
+ - **`options.prefix`** (optional): Key prefix to filter by. Only entries whose key starts with this prefix are returned.
461
+ - **Returns**: Array of `ScanRow` objects sorted by key in lexicographic order.
462
+ - **Tombstones**: Included in results with `value: undefined`. Use `row.value === undefined` to detect them.
463
+ - **Empty result**: Returns `[]` when no entries match.
464
+
465
+ **Bound types**:
466
+
467
+ ```ts
468
+ // Inclusive: includes the boundary key
469
+ { kind: "inclusive", key: ["b"] }
470
+
471
+ // Exclusive: excludes the boundary key
472
+ { kind: "exclusive", key: ["d"] }
473
+
474
+ // Unbounded: no limit in this direction
475
+ { kind: "unbounded" }
476
+ ```
477
+
478
+ **Examples**:
479
+
480
+ ```ts
481
+ // Full scan (all entries including tombstones)
482
+ const all = flock.scan();
483
+
484
+ // Prefix scan
485
+ const users = flock.scan({ prefix: ["users"] });
486
+
487
+ // Range scan with bounds
488
+ const range = flock.scan({
489
+ start: { kind: "inclusive", key: ["b"] },
490
+ end: { kind: "exclusive", key: ["d"] },
491
+ });
492
+
493
+ // Combine prefix and bounds
494
+ const page = flock.scan({
495
+ prefix: ["users"],
496
+ start: { kind: "exclusive", key: ["users", 100] },
497
+ end: { kind: "inclusive", key: ["users", 200] },
498
+ });
499
+ ```
500
+
501
+ **ScanRow structure**:
502
+
503
+ ```ts
504
+ {
505
+ key: KeyPart[]; // The full key
506
+ raw: ExportRecord; // Raw record with clock string and optional data/metadata
507
+ value?: Value; // The data value; undefined for tombstones
508
+ }
509
+ ```
510
+
511
+ ---
512
+
513
+ ### Replication and Import/Export
514
+
515
+ #### `version(): VersionVector`
516
+
517
+ Returns the **exclusive** (visible) version vector.
518
+
519
+ - **Returns**: `VersionVector` — only includes peers that currently own at least one visible entry (not overwritten by a later entry from another peer).
520
+ - **Use case**: Pass this to a remote peer as the `from` parameter of `exportJson()` to get only the updates the local replica is missing.
521
+ - **Behavior after overwrites**: If peer A's only entry is overwritten by peer B, peer A is removed from the exclusive version.
522
+
523
+ ```ts
524
+ flock.setPeerId("a");
525
+ flock.put(["k"], "v1");
526
+ flock.version(); // { a: { physicalTime: ..., logicalCounter: ... } }
527
+
528
+ flock.setPeerId("b");
529
+ flock.put(["k"], "v2"); // overwrites peer a's entry
530
+ flock.version(); // { b: { ... } } — peer a is gone
531
+ ```
532
+
533
+ #### `inclusiveVersion(): VersionVector`
534
+
535
+ Returns the **inclusive** (max-seen) version vector.
536
+
537
+ - **Returns**: `VersionVector` — tracks the highest clock seen from every peer during this instance's lifetime, including peers whose entries have been overwritten.
538
+ - **Use case**: Completeness checks, debugging, or determining whether a remote update has been "seen" even if it was subsequently overwritten.
539
+ - **Guarantee**: Always a superset of `version()`.
540
+
541
+ ```ts
542
+ flock.setPeerId("a");
543
+ flock.put(["k"], "v1");
544
+ flock.setPeerId("b");
545
+ flock.put(["k"], "v2");
546
+
547
+ flock.version(); // { b: { ... } }
548
+ flock.inclusiveVersion(); // { a: { ... }, b: { ... } }
549
+ ```
550
+
551
+ #### `merge(other: Flock): void`
552
+
553
+ Merges all state from another Flock instance into this one.
554
+
555
+ - **`other`**: The source Flock instance.
556
+ - **Effect**: Applies all entries from `other` into `this` using LWW conflict resolution. After merge, `this` contains the union of both states.
557
+ - **Idempotent**: Calling `merge(other)` multiple times produces the same result as calling it once.
558
+ - **Events**: Emits an event batch for imported entries.
559
+ - **Note**: This is a one-directional merge. To fully sync two replicas, call `a.merge(b)` and `b.merge(a)`.
560
+
561
+ #### `exportJson(): ExportBundle`
562
+
563
+ Exports the full state as a JSON bundle.
564
+
565
+ - **Returns**: `ExportBundle` containing all entries (including tombstones), keyed by stringified key arrays.
566
+ - **Format**: `{ version: number, entries: { [key: string]: ExportRecord } }`.
567
+ - **Use case**: Full snapshot for backup, initial sync, or cold storage.
568
+
569
+ #### `exportJson(from: VersionVector): ExportBundle`
570
+
571
+ Exports only entries newer than the given version vector (incremental/delta export).
572
+
573
+ - **`from`**: The remote peer's current version vector (obtained via `version()`).
574
+ - **Returns**: Bundle containing only entries the remote peer hasn't seen.
575
+ - **Use case**: Bandwidth-efficient incremental sync. Send your `version()` to the remote, get back only what's new.
576
+
577
+ ```ts
578
+ // Incremental sync pattern
579
+ const remoteVersion = remotePeer.version();
580
+ const delta = localPeer.exportJson(remoteVersion);
581
+ remotePeer.importJson(delta);
582
+ ```
583
+
584
+ #### `exportJson(from: VersionVector, pruneTombstonesBefore: number): ExportBundle`
585
+
586
+ Exports with tombstone pruning.
587
+
588
+ - **`pruneTombstonesBefore`**: Millisecond timestamp. Tombstones with `physicalTime` older than this value are excluded from the export.
589
+ - **Use case**: Reduce export size by omitting old tombstones that all replicas have already seen.
590
+ - **Caution**: If a replica imports a pruned export, it may not know about deletions that happened before the prune threshold. Only use when you're confident all replicas have already processed those deletions.
591
+
592
+ #### `exportJson(options: ExportOptions): Promise<ExportBundle>`
593
+
594
+ Exports with hooks and/or additional options (async).
595
+
596
+ - **`options.from`** (optional): Version vector for incremental export.
597
+ - **`options.pruneTombstonesBefore`** (optional): Tombstone prune timestamp.
598
+ - **`options.peerId`** (optional): Export only entries written by this peer.
599
+ - **`options.hooks.transform`** (optional): Async function to transform each entry during export.
600
+ - **Returns**: `Promise<ExportBundle>`.
601
+ - **Use case**: Sign entries, redact sensitive data, or filter entries during export.
602
+
603
+ ```ts
604
+ const signed = await flock.exportJson({
605
+ hooks: {
606
+ transform: async (ctx, payload) => {
607
+ return {
608
+ ...payload,
609
+ metadata: { ...payload.metadata, sig: await sign(payload.data) },
610
+ };
611
+ },
612
+ },
613
+ });
614
+ ```
615
+
616
+ #### `importJson(bundle: ExportBundle): ImportReport`
617
+
618
+ Imports a JSON bundle, merging entries using LWW semantics.
619
+
620
+ - **`bundle`**: An `ExportBundle` from `exportJson()`.
621
+ - **Returns**: `ImportReport` with `{ accepted: number, skipped: Array<{ key, reason }> }`.
622
+ - **`accepted` counts records processed**, not records that changed state. Re-importing the same bundle may still report a positive `accepted` count even though no state changed and no events are emitted. Do not use `accepted` as a reliable indicator of whether new data was applied — use event listeners for that.
623
+ - **Events**: Emits event batches for newly accepted entries.
624
+ - **Effect on debounce**: If `autoDebounceCommit` is active, any pending local events are flushed before the import events are delivered.
625
+
626
+ #### `importJson(options: ImportOptions): Promise<ImportReport>`
627
+
628
+ Imports with preprocessing hooks (async).
629
+
630
+ - **`options.bundle`**: The export bundle.
631
+ - **`options.hooks.preprocess`**: Async function `(context, payload) => ImportDecision` to validate each entry before import.
632
+ - **Returns**: `Promise<ImportReport>`.
633
+ - **Use case**: Validate signatures, check authorization, or reject untrusted entries.
634
+
635
+ ```ts
636
+ const report = await flock.importJson({
637
+ bundle,
638
+ hooks: {
639
+ preprocess: async (ctx, payload) => {
640
+ if (!await verify(payload.metadata?.sig, payload.data)) {
641
+ return { accept: false, reason: "invalid signature" };
642
+ }
643
+ return { accept: true };
644
+ },
645
+ },
646
+ });
647
+ console.log(report.skipped); // entries rejected by the hook
648
+ ```
649
+
650
+ #### `importJsonStr(bundle: string): ImportReport`
651
+
652
+ Convenience method that parses a JSON string and imports it.
653
+
654
+ - **`bundle`**: JSON string representation of an `ExportBundle`.
655
+ - **Returns**: `ImportReport`.
656
+ - **Use case**: Import directly from a network response or file read without manual parsing.
657
+
658
+ #### `kvToJson(): ExportBundle`
659
+
660
+ Alias for `exportJson()` with no arguments. Returns the full state as a JSON bundle.
661
+
662
+ ---
663
+
664
+ ### Hooks API
665
+
666
+ Hooks provide extension points for signing, validating, transforming, or filtering data during put, export, and import operations.
667
+
668
+ #### Put Hooks (`PutWithMetaOptions`)
669
+
670
+ ```ts
671
+ type PutWithMetaOptions = {
672
+ metadata?: MetadataMap;
673
+ now?: number;
674
+ hooks?: {
675
+ transform?: (context: PutHookContext, payload: PutPayload) => MaybePromise<PutPayload | void>;
676
+ };
677
+ };
678
+ ```
679
+
680
+ - **`context`**: `{ key: KeyPart[], now?: number }`.
681
+ - **`payload`**: `{ data?: Value, metadata?: MetadataMap }` — a working copy.
682
+ - **Behavior**: The hook receives a cloned payload. It can mutate it in-place or return a new payload. If it returns `void`, the (possibly mutated) working copy is used.
683
+ - **Constraint**: The final payload must have a `data` field; omitting it throws `TypeError`.
684
+
685
+ #### Export Hooks (`ExportOptions`)
686
+
687
+ ```ts
688
+ type ExportOptions = {
689
+ from?: VersionVector;
690
+ pruneTombstonesBefore?: number;
691
+ peerId?: string;
692
+ hooks?: {
693
+ transform?: (context: ExportHookContext, payload: ExportPayload) => MaybePromise<ExportPayload | void>;
694
+ };
695
+ };
696
+ ```
697
+
698
+ - **`context`**: `{ key: KeyPart[], clock: EntryClock, raw: ExportRecord }`.
699
+ - **`payload`**: `{ data?: Value, metadata?: MetadataMap }` — a working copy.
700
+ - **Behavior**: Called once per entry in the export. Does not modify stored state; only affects the exported bundle.
701
+ - **Use case**: Encrypt data, redact PII, or append signatures before sharing.
702
+
703
+ #### Import Hooks (`ImportOptions`)
704
+
705
+ ```ts
706
+ type ImportOptions = {
707
+ bundle: ExportBundle;
708
+ hooks?: {
709
+ preprocess?: (context: ImportHookContext, payload: ImportPayload) => MaybePromise<ImportDecision>;
710
+ };
711
+ };
712
+ ```
713
+
714
+ - **`context`**: `{ key: KeyPart[], clock: EntryClock, raw: ExportRecord }`.
715
+ - **`payload`**: `{ data?: Value, metadata?: MetadataMap }` — a cloned copy for inspection (not mutation).
716
+ - **`ImportDecision`**: Return `{ accept: true }`, `{ accept: false, reason: string }`, or `void` (treated as accept).
717
+ - **Behavior**: Called once per entry before import. Rejected entries are excluded from the merge and reported in `ImportReport.skipped`.
718
+ - **Use case**: Validate signatures, enforce ACLs, or reject entries from untrusted peers.
719
+
720
+ ---
721
+
722
+ ### Events and Subscriptions
723
+
724
+ #### `subscribe(listener: (batch: EventBatch) => void): () => void`
725
+
726
+ Subscribes to change events.
727
+
728
+ - **`listener`**: Callback receiving `EventBatch` objects.
729
+ - **Returns**: An unsubscribe function. Call it to stop receiving events.
730
+ - **Event delivery**: Events are delivered synchronously after each state-changing operation (unless debounce or transactions are active).
731
+ - **Re-entrancy safe**: You can call `put`, `delete`, `get`, `merge`, or `importJson` from within a listener. New events from re-entrant operations are queued and delivered after the current batch completes.
732
+ - **Error isolation**: If a listener throws, other listeners still receive the event. The error does not propagate to the caller of the operation that triggered the event.
733
+ - **Automatic cleanup**: When all listeners are removed, the native WASM subscription is cleaned up.
734
+
735
+ **EventBatch structure**:
736
+
737
+ ```ts
738
+ {
739
+ source: string; // "local" for put/delete/set, or peer identifier for import/merge
740
+ events: Event[]; // Array of individual change events
741
+ }
742
+ ```
743
+
744
+ **Event structure**:
745
+
746
+ ```ts
747
+ {
748
+ key: KeyPart[]; // The affected key
749
+ value?: Value; // New value; undefined for deletions
750
+ metadata?: MetadataMap; // Associated metadata
751
+ payload: EventPayload; // { data?: Value, metadata?: MetadataMap }
752
+ }
753
+ ```
754
+
755
+ ```ts
756
+ const unsub = flock.subscribe((batch) => {
757
+ for (const event of batch.events) {
758
+ console.log(`${batch.source}: ${event.key} → ${event.value}`);
759
+ }
760
+ });
761
+
762
+ flock.put(["x"], 1); // listener fires: source="local", key=["x"], value=1
763
+ unsub(); // stop listening
764
+ ```
765
+
766
+ ---
767
+
768
+ ### Debounced Event Batching
769
+
770
+ Debouncing accumulates local events and delivers them as a single batch after a period of inactivity, reducing event noise during rapid-fire edits.
771
+
772
+ #### `autoDebounceCommit(timeout: number, options?: { maxDebounceTime?: number }): void`
773
+
774
+ Enables debounced event batching.
775
+
776
+ - **`timeout`**: Debounce window in milliseconds. Events are held until `timeout` ms of inactivity pass.
777
+ - **`options.maxDebounceTime`** (default: `10000`): Maximum time in ms that events can be held, even if operations keep coming. This prevents unbounded delays during sustained writes.
778
+ - **Effect**: Local `put`/`delete`/`set` events are buffered instead of immediately delivered. Import and merge events are NOT debounced — they flush pending local events first, then deliver immediately.
779
+ - **Throws**: If a transaction is currently active, or if debounce is already enabled.
780
+
781
+ ```ts
782
+ flock.autoDebounceCommit(100, { maxDebounceTime: 5000 });
783
+ flock.put(["a"], 1); // buffered
784
+ flock.put(["b"], 2); // buffered, timer reset
785
+ // After 100ms of inactivity → single batch with both events
786
+ // OR after 5000ms even if operations keep coming
787
+ ```
788
+
789
+ #### `commit(): void`
790
+
791
+ Forces immediate emission of any pending debounced events.
792
+
793
+ - **Effect**: Flushes the debounce buffer and delivers all pending events as a batch.
794
+ - **Does NOT disable** debounce mode — subsequent operations continue to be debounced.
795
+ - **No-op**: If debounce is not active or no events are pending.
796
+
797
+ #### `disableAutoDebounceCommit(): void`
798
+
799
+ Disables debounce mode and flushes any pending events.
800
+
801
+ - **Effect**: Pending events are delivered immediately. Future operations emit events synchronously.
802
+ - **No-op**: If debounce is not active.
803
+
804
+ #### `isAutoDebounceActive(): boolean`
805
+
806
+ Returns whether debounce mode is currently enabled.
807
+
808
+ ---
809
+
810
+ ### Transactions
811
+
812
+ Transactions batch multiple operations into a single atomic event delivery. All put/delete operations inside a transaction are delivered as one `EventBatch` on commit.
813
+
814
+ #### `txn<T>(callback: () => T): T`
815
+
816
+ Executes a synchronous callback within a transaction.
817
+
818
+ - **`callback`**: Synchronous function containing put/delete operations.
819
+ - **Returns**: The return value of the callback.
820
+ - **Event batching**: All events from operations inside the callback are collected and delivered as a single `EventBatch` when the transaction commits.
821
+ - **Error handling**: If the callback throws, the transaction is rolled back (event emission is canceled) and the error re-thrown. **Important caveat**: Data changes (puts/deletes) may NOT be rolled back — only event emission is affected. The underlying writes may have already been applied to the store.
822
+ - **Throws**: `Error` if called while `autoDebounceCommit` is active (mutual exclusion).
823
+ - **Throws**: `Error` if nested transactions are attempted.
824
+ - **Constraint**: The callback must be synchronous. Async operations inside `txn()` are not supported.
825
+
826
+ ```ts
827
+ flock.subscribe((batch) => {
828
+ console.log(`Received ${batch.events.length} events`);
829
+ });
830
+
831
+ flock.txn(() => {
832
+ flock.put(["a"], 1);
833
+ flock.put(["b"], 2);
834
+ flock.put(["c"], 3);
835
+ });
836
+ // Listener receives: "Received 3 events" — single batch
837
+ ```
838
+
839
+ #### `isInTxn(): boolean`
840
+
841
+ Returns whether a transaction is currently active.
842
+
843
+ ---
844
+
845
+ ### Binary File Export/Import
846
+
847
+ The binary file format is Flock's most efficient serialization. Unlike JSON export (which requires parsing every entry and re-inserting them one by one into a B+Tree), the binary format stores pre-built B+Tree pages directly. This makes **loading from binary dramatically faster** — especially for large datasets.
848
+
849
+ | Operation | `fromJson` (JSON bundle) | `fromFile` (binary) |
850
+ |-----------|--------------------------|---------------------|
851
+ | Open cost | O(n log n) — parse JSON, encode each key, insert into B+Tree one by one | O(1) — read file header, done |
852
+ | Size | Larger (JSON text, repeated key strings, base-10 numbers) | Smaller (binary-encoded keys, prefix-compressed leaves, compact integers) |
853
+ | Use case | Network sync, incremental delta exchange | Persistence, checkpoints, full-state transfer |
854
+
855
+ **Rule of thumb**: Use `exportJson`/`importJson` for replication and sync. Use `exportFile`/`fromFile` for persistence and fast cold-start loading.
856
+
857
+ #### `exportFile(): Uint8Array`
858
+
859
+ Exports the entire Flock state as a binary file.
860
+
861
+ - **Returns**: `Uint8Array` containing the complete state in Flock's B+Tree page format.
862
+ - **Format**: Binary format with `FLOK` magic header, B+Tree pages with prefix-compressed keys, and optional WAL data. Significantly more compact than JSON export.
863
+ - **Performance**: The exported bytes contain ready-to-use B+Tree pages. When loaded via `Flock.fromFile()`, the pages are used directly without any re-indexing or re-encoding — opening is O(1) regardless of data size.
864
+ - **Use case**: Persist state to disk, transfer as a file, create compact backups, or achieve the fastest possible cold-start.
865
+ - **Side effect**: Flushes any pending memtable writes before exporting.
866
+
867
+ ```ts
868
+ const bytes = flock.exportFile();
869
+ fs.writeFileSync("state.flock", bytes);
870
+ ```
871
+
872
+ Restoration is done via `Flock.fromFile()`:
873
+
874
+ ```ts
875
+ const bytes = fs.readFileSync("state.flock");
876
+ const flock = Flock.fromFile(new Uint8Array(bytes), "my-peer");
877
+ // Ready instantly — no re-parsing or re-indexing needed
878
+ ```
879
+
880
+ ---
881
+
882
+ ## Type Reference
883
+
884
+ ### Version Vector Types
885
+
886
+ ```ts
887
+ type VersionVectorEntry = {
888
+ physicalTime: number; // Wall-clock timestamp in milliseconds
889
+ logicalCounter: number; // Monotonic tie-breaking counter
890
+ };
891
+
892
+ interface VersionVector {
893
+ [peer: string]: VersionVectorEntry | undefined;
894
+ }
895
+ ```
896
+
897
+ ### Data Types
898
+
899
+ ```ts
900
+ // Any JSON-serializable value
901
+ type Value = string | number | boolean | null | Array<Value> | { [key: string]: Value };
902
+
903
+ // A single part of a compound key
904
+ type KeyPart = Value;
905
+
906
+ // Arbitrary metadata stored alongside an entry
907
+ type MetadataMap = Record<string, unknown>;
908
+ ```
909
+
910
+ ### Entry Types
911
+
912
+ ```ts
913
+ type EntryClock = {
914
+ physicalTime: number; // When the entry was written
915
+ logicalCounter: number; // Tie-breaking counter
916
+ peerId: string; // Who wrote the entry
917
+ };
918
+
919
+ type EntryInfo = {
920
+ data?: Value; // Omitted for tombstones
921
+ metadata: MetadataMap; // Always present (defaults to {})
922
+ clock: EntryClock; // Clock of the winning write
923
+ };
924
+ ```
925
+
926
+ ### Export/Import Types
927
+
928
+ ```ts
929
+ type ExportRecord = {
930
+ c: string; // Clock string: "physicalTime,logicalCounter,peerId"
931
+ d?: Value; // Data value (omitted for tombstones)
932
+ m?: MetadataMap; // Metadata (omitted if empty)
933
+ };
934
+
935
+ type ExportBundle = {
936
+ version: number; // Format version number
937
+ entries: Record<string, ExportRecord>; // Key (stringified) → record
938
+ };
939
+
940
+ type ImportReport = {
941
+ accepted: number; // Number of entries processed (not necessarily changed)
942
+ skipped: Array<{ key: KeyPart[]; reason: string }>; // Rejected entries with reasons
943
+ };
944
+ ```
945
+
946
+ ### Hook Types
947
+
948
+ ```ts
949
+ type ExportPayload = { data?: Value; metadata?: MetadataMap };
950
+ type ExportHookContext = { key: KeyPart[]; clock: EntryClock; raw: ExportRecord };
951
+ type ExportHooks = {
952
+ transform?: (context: ExportHookContext, payload: ExportPayload) => MaybePromise<ExportPayload | void>;
953
+ };
954
+
955
+ type ImportPayload = ExportPayload;
956
+ type ImportHookContext = ExportHookContext;
957
+ type ImportAccept = { accept: true };
958
+ type ImportSkip = { accept: false; reason: string };
959
+ type ImportDecision = ImportAccept | ImportSkip | ImportPayload | void;
960
+ type ImportHooks = {
961
+ preprocess?: (context: ImportHookContext, payload: ImportPayload) => MaybePromise<ImportDecision>;
962
+ };
963
+
964
+ type PutPayload = ExportPayload;
965
+ type PutHookContext = { key: KeyPart[]; now?: number };
966
+ type PutHooks = {
967
+ transform?: (context: PutHookContext, payload: PutPayload) => MaybePromise<PutPayload | void>;
968
+ };
969
+
970
+ type PutWithMetaOptions = {
971
+ metadata?: MetadataMap;
972
+ now?: number;
973
+ hooks?: PutHooks;
974
+ };
975
+ ```
976
+
977
+ ### Scan Types
978
+
979
+ ```ts
980
+ type ScanBound =
981
+ | { kind: "inclusive"; key: KeyPart[] }
982
+ | { kind: "exclusive"; key: KeyPart[] }
983
+ | { kind: "unbounded" };
984
+
985
+ type ScanOptions = {
986
+ start?: ScanBound;
987
+ end?: ScanBound;
988
+ prefix?: KeyPart[];
989
+ };
990
+
991
+ type ScanRow = {
992
+ key: KeyPart[];
993
+ raw: ExportRecord;
994
+ value?: Value;
995
+ };
996
+ ```
997
+
998
+ ### Event Types
999
+
1000
+ ```ts
1001
+ type EventPayload = { data?: Value; metadata?: MetadataMap };
1002
+
1003
+ type Event = {
1004
+ key: KeyPart[];
1005
+ value?: Value;
1006
+ metadata?: MetadataMap;
1007
+ payload: EventPayload;
1008
+ };
1009
+
1010
+ type EventBatch = {
1011
+ source: string; // "local" or peer identifier
1012
+ events: Event[];
1013
+ };
1014
+ ```
1015
+
1016
+ ---
1017
+
1018
+ ## Conflict Resolution Deep Dive
1019
+
1020
+ ### How LWW Works in Practice
1021
+
1022
+ ```ts
1023
+ // Both peers start with the same state
1024
+ const a = new Flock("peer-a");
1025
+ const b = new Flock("peer-b");
1026
+
1027
+ // Concurrent writes at the same physical time
1028
+ a.put(["key"], "alice-value", 1000);
1029
+ b.put(["key"], "bob-value", 1000);
1030
+
1031
+ // After merge, both converge
1032
+ a.merge(b);
1033
+ b.merge(a);
1034
+
1035
+ // Who wins? Clock comparison:
1036
+ // 1. physicalTime: both 1000 → tie
1037
+ // 2. logicalCounter: both 0 → tie
1038
+ // 3. peerId: "peer-b" > "peer-a" lexicographically → peer-b wins
1039
+ console.log(a.get(["key"])); // "bob-value"
1040
+ console.log(b.get(["key"])); // "bob-value"
1041
+ ```
1042
+
1043
+ ### Delete vs. Write Conflicts
1044
+
1045
+ ```ts
1046
+ // A writes, B deletes at a later time
1047
+ a.put(["key"], "value", 1000);
1048
+ b.delete(["key"], 2000);
1049
+
1050
+ a.merge(b);
1051
+ // Delete wins because physicalTime 2000 > 1000
1052
+ a.get(["key"]); // undefined
1053
+
1054
+ // A writes again at an even later time
1055
+ a.put(["key"], "new-value", 3000);
1056
+ a.get(["key"]); // "new-value" — write wins because 3000 > 2000
1057
+ ```
1058
+
1059
+ ### Version Vector After Overwrites
1060
+
1061
+ When a peer's entries are fully overwritten by another peer, the exclusive version vector drops the original peer:
1062
+
1063
+ ```ts
1064
+ const f = new Flock("a");
1065
+ f.put(["k1"], "v1"); // version: { a: {...} }
1066
+ f.setPeerId("b");
1067
+ f.put(["k1"], "v2"); // version: { b: {...} } — a is dropped
1068
+
1069
+ // But inclusive version retains both:
1070
+ f.inclusiveVersion(); // { a: {...}, b: {...} }
1071
+ ```
1072
+
1073
+ ---
1074
+
1075
+ ## Version Vectors Explained
1076
+
1077
+ ### Incremental Sync Protocol
1078
+
1079
+ ```ts
1080
+ // Step 1: Bob sends his version to Alice
1081
+ const bobVersion = bob.version();
1082
+
1083
+ // Step 2: Alice exports only what Bob hasn't seen
1084
+ const delta = alice.exportJson(bobVersion);
1085
+
1086
+ // Step 3: Bob imports the delta
1087
+ const report = bob.importJson(delta);
1088
+ console.log(`Imported ${report.accepted} new entries`);
1089
+
1090
+ // Step 4: Reverse direction
1091
+ const aliceVersion = alice.version();
1092
+ const reverseDelta = bob.exportJson(aliceVersion);
1093
+ alice.importJson(reverseDelta);
1094
+
1095
+ // Both are now fully synced
1096
+ ```
1097
+
1098
+ ### When to Use Which Version
1099
+
1100
+ | Method | Includes overwritten peers | Use for |
1101
+ |---------------------|---------------------------|----------------------------------|
1102
+ | `version()` | No | Incremental sync, delta export |
1103
+ | `inclusiveVersion()`| Yes | Completeness checks, debugging |
1104
+
1105
+ ---
1106
+
1107
+ ## Best Practices
1108
+
1109
+ ### 1. Use Explicit Timestamps for Deterministic Testing
1110
+
1111
+ ```ts
1112
+ // In tests, always provide explicit timestamps for reproducible results
1113
+ flock.put(["k"], "v", 1000);
1114
+
1115
+ // In production, omit the timestamp to use Date.now()
1116
+ flock.put(["k"], "v");
1117
+ ```
1118
+
1119
+ ### 2. Use Transactions for Batch Updates
1120
+
1121
+ ```ts
1122
+ // Bad: subscribers receive 100 separate event batches
1123
+ for (let i = 0; i < 100; i++) {
1124
+ flock.put(["item", i], data[i]);
1125
+ }
1126
+
1127
+ // Good: subscribers receive one batch with 100 events
1128
+ flock.txn(() => {
1129
+ for (let i = 0; i < 100; i++) {
1130
+ flock.put(["item", i], data[i]);
1131
+ }
1132
+ });
1133
+ ```
1134
+
1135
+ ### 3. Use Debounce for Interactive Editing
1136
+
1137
+ ```ts
1138
+ // Accumulate rapid keystrokes into periodic batches
1139
+ flock.autoDebounceCommit(200, { maxDebounceTime: 2000 });
1140
+
1141
+ // On each keystroke
1142
+ flock.put(["doc", "content"], currentContent);
1143
+
1144
+ // Listeners receive updates at most every 200ms,
1145
+ // or at least every 2000ms during sustained typing
1146
+ ```
1147
+
1148
+ ### 4. Prune Tombstones Periodically
1149
+
1150
+ ```ts
1151
+ // When syncing, prune tombstones older than 7 days
1152
+ const sevenDaysAgo = Date.now() - 7 * 24 * 60 * 60 * 1000;
1153
+ const bundle = flock.exportJson(
1154
+ remoteVersion,
1155
+ sevenDaysAgo,
1156
+ );
1157
+
1158
+ // Only safe when you know all replicas have synced within 7 days
1159
+ ```
1160
+
1161
+ ### 5. Use Metadata for Audit Trails
1162
+
1163
+ ```ts
1164
+ flock.putWithMeta(["transaction", txId], amount, {
1165
+ metadata: {
1166
+ author: currentUser,
1167
+ sig: await sign(amount),
1168
+ timestamp: Date.now(),
1169
+ },
1170
+ });
1171
+
1172
+ // Verify on import
1173
+ const report = await flock.importJson({
1174
+ bundle,
1175
+ hooks: {
1176
+ preprocess: async (ctx, payload) => {
1177
+ if (!await verify(payload.metadata?.sig, payload.data)) {
1178
+ return { accept: false, reason: "signature verification failed" };
1179
+ }
1180
+ },
1181
+ },
1182
+ });
1183
+ ```
1184
+
1185
+ ### 6. Use Incremental Sync for Bandwidth Efficiency
1186
+
1187
+ ```ts
1188
+ // Don't do this (sends everything every time):
1189
+ const full = flock.exportJson();
1190
+ remotePeer.importJson(full);
1191
+
1192
+ // Do this instead:
1193
+ const delta = flock.exportJson(remotePeer.version());
1194
+ remotePeer.importJson(delta);
1195
+ ```
1196
+
1197
+ ### 7. Design Keys for Scan Efficiency
1198
+
1199
+ ```ts
1200
+ // Good: hierarchical keys enable prefix scans
1201
+ flock.put(["users", userId, "name"], "Alice");
1202
+ flock.put(["users", userId, "email"], "alice@example.com");
1203
+ const userFields = flock.scan({ prefix: ["users", userId] });
1204
+
1205
+ // Bad: flat keys require full scans
1206
+ flock.put(["user_name_" + userId], "Alice");
1207
+ flock.put(["user_email_" + userId], "alice@example.com");
1208
+ ```
1209
+
1210
+ ---
1211
+
1212
+ ## Common Pitfalls
1213
+
1214
+ ### 1. Debounce and Transactions Are Mutually Exclusive
1215
+
1216
+ ```ts
1217
+ flock.autoDebounceCommit(100);
1218
+ flock.txn(() => { /* ... */ }); // Throws Error!
1219
+
1220
+ // Disable debounce first, or don't use both at the same time
1221
+ ```
1222
+
1223
+ ### 2. Transaction Rollback Does NOT Undo Data Changes
1224
+
1225
+ ```ts
1226
+ flock.txn(() => {
1227
+ flock.put(["k"], "value");
1228
+ throw new Error("oops");
1229
+ });
1230
+ // Error is re-thrown, events are NOT emitted
1231
+ // But flock.get(["k"]) may still return "value" — data persists
1232
+ ```
1233
+
1234
+ ### 3. MVR Only Accepts Scalar Values
1235
+
1236
+ ```ts
1237
+ flock.putMvr(["k"], "string"); // OK
1238
+ flock.putMvr(["k"], 42); // OK
1239
+ flock.putMvr(["k"], true); // OK
1240
+ flock.putMvr(["k"], null); // Throws TypeError
1241
+ flock.putMvr(["k"], [1, 2]); // Throws TypeError
1242
+ flock.putMvr(["k"], { a: 1 }); // Throws TypeError
1243
+ ```
1244
+
1245
+ ### 4. merge() Is One-Directional
1246
+
1247
+ ```ts
1248
+ a.merge(b); // a now has everything from b
1249
+ // But b does NOT have a's changes — you need:
1250
+ b.merge(a);
1251
+ ```
1252
+
1253
+ ### 5. Tombstones Appear in Scan Results
1254
+
1255
+ ```ts
1256
+ flock.put(["k"], "value");
1257
+ flock.delete(["k"]);
1258
+ const rows = flock.scan();
1259
+ // rows includes { key: ["k"], value: undefined, raw: {...} }
1260
+ // Filter tombstones if you only want live data:
1261
+ const live = rows.filter(row => row.value !== undefined);
1262
+ ```
1263
+
1264
+ ### 6. Import Flushes Debounced Events First
1265
+
1266
+ ```ts
1267
+ flock.autoDebounceCommit(5000);
1268
+ flock.put(["local"], 1); // Buffered, not emitted yet
1269
+
1270
+ flock.importJson(remoteDelta); // Forces flush of local batch FIRST,
1271
+ // then delivers import batch
1272
+ // Result: listener receives 2 batches in sequence
1273
+ ```
1274
+
1275
+ ### 7. Peer ID Changes Don't Affect Past Entries
1276
+
1277
+ ```ts
1278
+ flock.setPeerId("a");
1279
+ flock.put(["k"], "by-a");
1280
+ flock.setPeerId("b");
1281
+ // The entry at ["k"] is still attributed to peer "a"
1282
+ // Only new operations will use peer "b"
1283
+ ```
1284
+
1285
+ ---
11
1286
 
12
1287
  ## Build
13
1288