json-patch-to-crdt 0.1.0 → 0.1.2
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 +76 -2
- package/dist/{merge-BqROEw61.mjs → compact-BJBGW9tC.mjs} +938 -202
- package/dist/{merge-CtJfKEt1.js → compact-CkLd4Yh5.js} +979 -201
- package/dist/{merge-BrNGGkXj.d.mts → depth-p6fX9Ak7.d.ts} +93 -4
- package/dist/{merge-DW1-p9Hj.d.ts → depth-wDeQ1hO1.d.mts} +93 -4
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +20 -16
- package/dist/index.mjs +2 -2
- package/dist/internals.d.mts +15 -3
- package/dist/internals.d.ts +15 -3
- package/dist/internals.js +65 -58
- package/dist/internals.mjs +2 -2
- package/package.json +9 -2
package/README.md
CHANGED
|
@@ -30,7 +30,25 @@ npm install json-patch-to-crdt
|
|
|
30
30
|
|
|
31
31
|
- Node.js `>= 18` (for package consumers).
|
|
32
32
|
- TypeScript `^5` when type-checking in your project.
|
|
33
|
-
- Bun is
|
|
33
|
+
- Bun `1.3.7` is used for this repo's own build/test scripts.
|
|
34
|
+
|
|
35
|
+
## Testing (Repo)
|
|
36
|
+
|
|
37
|
+
Run all tests:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
bun run test
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Run targeted domain suites:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
bun run test:state-core
|
|
47
|
+
bun run test:patch-diff-doc
|
|
48
|
+
bun run test:merge-compaction
|
|
49
|
+
bun run test:replica-session
|
|
50
|
+
bun run test:perf-regression
|
|
51
|
+
```
|
|
34
52
|
|
|
35
53
|
## Quick Start (Recommended API)
|
|
36
54
|
|
|
@@ -64,6 +82,7 @@ import { applyPatch, createState, forkState, mergeState, toJson } from "json-pat
|
|
|
64
82
|
const origin = createState({ count: 0, items: ["a"] }, { actor: "origin" });
|
|
65
83
|
|
|
66
84
|
// Fork shared-origin replicas with local actor identities.
|
|
85
|
+
// Actor IDs must be unique per live peer (same-actor reuse is rejected by default).
|
|
67
86
|
const peerA = forkState(origin, "A");
|
|
68
87
|
const peerB = forkState(origin, "B");
|
|
69
88
|
|
|
@@ -219,6 +238,7 @@ const fullPatch = crdtToFullReplace(doc);
|
|
|
219
238
|
### Array Delta Strategy
|
|
220
239
|
|
|
221
240
|
By default, arrays are diffed with deterministic LCS edits.
|
|
241
|
+
To prevent pathological `O(n*m)` matrix growth on very large arrays, LCS falls back to atomic array replacement when matrix cells exceed `250_000` by default.
|
|
222
242
|
|
|
223
243
|
If you want atomic array replacement, pass `{ arrayStrategy: "atomic" }`:
|
|
224
244
|
|
|
@@ -226,10 +246,22 @@ If you want atomic array replacement, pass `{ arrayStrategy: "atomic" }`:
|
|
|
226
246
|
const delta = diffJsonPatch(baseJson, nextJson, { arrayStrategy: "atomic" });
|
|
227
247
|
```
|
|
228
248
|
|
|
249
|
+
If you want to tune the LCS fallback threshold, pass `lcsMaxCells`:
|
|
250
|
+
|
|
251
|
+
```ts
|
|
252
|
+
const delta = diffJsonPatch(baseJson, nextJson, {
|
|
253
|
+
arrayStrategy: "lcs",
|
|
254
|
+
lcsMaxCells: 500_000,
|
|
255
|
+
});
|
|
256
|
+
```
|
|
257
|
+
|
|
229
258
|
Notes:
|
|
230
259
|
|
|
231
260
|
- LCS diffs are deterministic but not necessarily minimal.
|
|
232
261
|
- Reorders are expressed as remove/add pairs.
|
|
262
|
+
- LCS complexity is `O(n*m)` in time and memory.
|
|
263
|
+
- `lcsMaxCells` sets the matrix cap: `(base.length + 1) * (next.length + 1)`.
|
|
264
|
+
- Set `lcsMaxCells: Number.POSITIVE_INFINITY` to always allow LCS.
|
|
233
265
|
|
|
234
266
|
## Merging
|
|
235
267
|
|
|
@@ -263,6 +295,39 @@ Resolution rules:
|
|
|
263
295
|
`mergeDoc` is commutative (`merge(a, b)` equals `merge(b, a)`) and idempotent.
|
|
264
296
|
For `mergeState`, pass the local actor explicitly (or as the first argument) so each peer keeps a stable actor ID.
|
|
265
297
|
|
|
298
|
+
## Tombstone Compaction
|
|
299
|
+
|
|
300
|
+
Long-lived documents can accumulate object/array tombstones.
|
|
301
|
+
You can compact causally-stable tombstones with:
|
|
302
|
+
|
|
303
|
+
```ts
|
|
304
|
+
import { compactStateTombstones } from "json-patch-to-crdt";
|
|
305
|
+
|
|
306
|
+
const { state: compacted, stats } = compactStateTombstones(state, {
|
|
307
|
+
stable: { A: 120, B: 98, C: 77 },
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
console.log(stats);
|
|
311
|
+
// { objectTombstonesRemoved: number, sequenceTombstonesRemoved: number }
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
For server-side workflows operating on raw docs, use internals:
|
|
315
|
+
|
|
316
|
+
```ts
|
|
317
|
+
import { compactDocTombstones } from "json-patch-to-crdt/internals";
|
|
318
|
+
|
|
319
|
+
compactDocTombstones(doc, {
|
|
320
|
+
stable: checkpointVv,
|
|
321
|
+
mutate: true, // optional in-place compaction
|
|
322
|
+
});
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
Safety conditions:
|
|
326
|
+
|
|
327
|
+
- Only compact at checkpoints that are causally stable across all peers you still merge with.
|
|
328
|
+
- Do not merge compacted replicas with peers that may be behind that checkpoint.
|
|
329
|
+
- Compaction preserves materialized JSON output for the compacted document/state.
|
|
330
|
+
|
|
266
331
|
## Serialization
|
|
267
332
|
|
|
268
333
|
```ts
|
|
@@ -315,7 +380,7 @@ Internals helpers like `jsonPatchToCrdtSafe` and `tryMergeDoc` return the same s
|
|
|
315
380
|
### State helpers
|
|
316
381
|
|
|
317
382
|
- `createState(initial, { actor, start? })` - Create a new CRDT state from JSON.
|
|
318
|
-
- `forkState(origin, actor)` - Fork a shared-origin replica with a new local actor ID.
|
|
383
|
+
- `forkState(origin, actor, options?)` - Fork a shared-origin replica with a new local actor ID. Reusing `origin` actor IDs is rejected by default (`options.allowActorReuse: true` to opt in explicitly).
|
|
319
384
|
- `applyPatch(state, patch, options?)` - Apply a patch immutably, returning a new state (`semantics: "sequential"` by default).
|
|
320
385
|
- `applyPatchInPlace(state, patch, options?)` - Apply a patch by mutating state in place (`atomic: true` by default).
|
|
321
386
|
- `tryApplyPatch(state, patch, options?)` - Non-throwing immutable apply (`{ ok: true, state }` or `{ ok: false, error }`).
|
|
@@ -388,15 +453,24 @@ Use `crdtToFullReplace(doc)` from `json-patch-to-crdt/internals`, which emits a
|
|
|
388
453
|
**Why do array deltas look bigger than expected?**
|
|
389
454
|
LCS diffs are deterministic, not minimal. If you prefer one-op array replacement, use `{ arrayStrategy: "atomic" }`.
|
|
390
455
|
|
|
456
|
+
**Why did my array delta become a full `replace` even with LCS?**
|
|
457
|
+
For scalability, LCS falls back to atomic replacement when arrays exceed the `lcsMaxCells` guardrail (default `250_000` matrix cells). Increase `lcsMaxCells` to allow larger LCS runs.
|
|
458
|
+
|
|
391
459
|
**Does LCS guarantee the smallest patch?**
|
|
392
460
|
No. It is deterministic and usually compact, but not guaranteed to be minimal.
|
|
393
461
|
|
|
394
462
|
**How do I merge states from two peers?**
|
|
395
463
|
Use `forkState(origin, actor)` to create each peer from the same origin, then `mergeState(local, remote, { actor: localActorId })`. Each peer should keep a stable unique actor ID across merges. See the [Multi-Peer Sync](#multi-peer-sync) example above.
|
|
396
464
|
|
|
465
|
+
**Why did `forkState` throw about actor uniqueness?**
|
|
466
|
+
By default, `forkState` blocks reusing `origin.clock.actor` because same-actor forks can mint duplicate dots and produce order-dependent merges. If you intentionally need same-actor cloning, pass `forkState(origin, actor, { allowActorReuse: true })`.
|
|
467
|
+
|
|
397
468
|
**Why can my local counter jump after a merge?**
|
|
398
469
|
Array inserts that target an existing predecessor may need to outrank sibling insert dots for deterministic ordering. The library can fast-forward the local counter in constant time to avoid expensive loops, but the resulting counter value may still jump upward when merging with peers that already have high counters.
|
|
399
470
|
|
|
471
|
+
**How should I run tombstone compaction in production?**
|
|
472
|
+
Treat compaction as a maintenance step after a causal-stability checkpoint (for example, after all replicas acknowledge processing through a specific version vector), then compact and persist the compacted snapshot.
|
|
473
|
+
|
|
400
474
|
## Limitations
|
|
401
475
|
|
|
402
476
|
- The array materialization and insert mapping depend on a base snapshot; concurrent inserts resolve by dot order.
|