json-patch-to-crdt 0.1.1 → 0.1.3
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 +103 -3
- package/dist/{merge-CKcP1ZPt.mjs → compact-BdTuOQK-.mjs} +1191 -268
- package/dist/{merge-BAfuC6bf.js → compact-DoM9CJNR.js} +1244 -267
- package/dist/{merge-B8nmGV-o.d.ts → depth-Dl_yOAKU.d.ts} +153 -7
- package/dist/{merge-DQ_KDtnE.d.mts → depth-IvWvLAkt.d.mts} +153 -7
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +22 -16
- package/dist/index.mjs +2 -2
- package/dist/internals.d.mts +26 -25
- package/dist/internals.d.ts +26 -25
- package/dist/internals.js +67 -58
- package/dist/internals.mjs +2 -2
- package/package.json +12 -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
|
|
|
@@ -165,6 +183,35 @@ If you prefer a non-throwing low-level compile+apply path, use `jsonPatchToCrdtS
|
|
|
165
183
|
- `"-"` is treated as append for array inserts.
|
|
166
184
|
- `test` operations can be evaluated against `head` or `base` using the `testAgainst` option.
|
|
167
185
|
|
|
186
|
+
## Runtime JSON Guardrails
|
|
187
|
+
|
|
188
|
+
By default, runtime inputs are accepted as-is (`jsonValidation: "none"`) for backward compatibility.
|
|
189
|
+
|
|
190
|
+
You can opt into stricter runtime behavior on `createState`, `applyPatch`/`tryApplyPatch`/`validateJsonPatch`, and `diffJsonPatch`:
|
|
191
|
+
|
|
192
|
+
- `jsonValidation: "strict"`: reject non-JSON runtime values (for example `NaN`, `Infinity`, and `undefined`).
|
|
193
|
+
- `jsonValidation: "normalize"`: coerce non-JSON values into JSON-safe values.
|
|
194
|
+
- non-finite numbers become `null`
|
|
195
|
+
- invalid array elements become `null`
|
|
196
|
+
- invalid object-property values are omitted
|
|
197
|
+
|
|
198
|
+
Examples:
|
|
199
|
+
|
|
200
|
+
```ts
|
|
201
|
+
const strictState = createState(payload as any, {
|
|
202
|
+
actor: "A",
|
|
203
|
+
jsonValidation: "strict",
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
const next = applyPatch(state, patch as any, {
|
|
207
|
+
jsonValidation: "normalize",
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
const delta = diffJsonPatch(base as any, target as any, {
|
|
211
|
+
jsonValidation: "strict",
|
|
212
|
+
});
|
|
213
|
+
```
|
|
214
|
+
|
|
168
215
|
### Semantics Modes
|
|
169
216
|
|
|
170
217
|
- `semantics: "sequential"` (default): applies operations one-by-one against the evolving head (RFC-like execution).
|
|
@@ -220,6 +267,7 @@ const fullPatch = crdtToFullReplace(doc);
|
|
|
220
267
|
### Array Delta Strategy
|
|
221
268
|
|
|
222
269
|
By default, arrays are diffed with deterministic LCS edits.
|
|
270
|
+
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.
|
|
223
271
|
|
|
224
272
|
If you want atomic array replacement, pass `{ arrayStrategy: "atomic" }`:
|
|
225
273
|
|
|
@@ -227,10 +275,22 @@ If you want atomic array replacement, pass `{ arrayStrategy: "atomic" }`:
|
|
|
227
275
|
const delta = diffJsonPatch(baseJson, nextJson, { arrayStrategy: "atomic" });
|
|
228
276
|
```
|
|
229
277
|
|
|
278
|
+
If you want to tune the LCS fallback threshold, pass `lcsMaxCells`:
|
|
279
|
+
|
|
280
|
+
```ts
|
|
281
|
+
const delta = diffJsonPatch(baseJson, nextJson, {
|
|
282
|
+
arrayStrategy: "lcs",
|
|
283
|
+
lcsMaxCells: 500_000,
|
|
284
|
+
});
|
|
285
|
+
```
|
|
286
|
+
|
|
230
287
|
Notes:
|
|
231
288
|
|
|
232
289
|
- LCS diffs are deterministic but not necessarily minimal.
|
|
233
290
|
- Reorders are expressed as remove/add pairs.
|
|
291
|
+
- LCS complexity is `O(n*m)` in time and memory.
|
|
292
|
+
- `lcsMaxCells` sets the matrix cap: `(base.length + 1) * (next.length + 1)`.
|
|
293
|
+
- Set `lcsMaxCells: Number.POSITIVE_INFINITY` to always allow LCS.
|
|
234
294
|
|
|
235
295
|
## Merging
|
|
236
296
|
|
|
@@ -264,6 +324,39 @@ Resolution rules:
|
|
|
264
324
|
`mergeDoc` is commutative (`merge(a, b)` equals `merge(b, a)`) and idempotent.
|
|
265
325
|
For `mergeState`, pass the local actor explicitly (or as the first argument) so each peer keeps a stable actor ID.
|
|
266
326
|
|
|
327
|
+
## Tombstone Compaction
|
|
328
|
+
|
|
329
|
+
Long-lived documents can accumulate object/array tombstones.
|
|
330
|
+
You can compact causally-stable tombstones with:
|
|
331
|
+
|
|
332
|
+
```ts
|
|
333
|
+
import { compactStateTombstones } from "json-patch-to-crdt";
|
|
334
|
+
|
|
335
|
+
const { state: compacted, stats } = compactStateTombstones(state, {
|
|
336
|
+
stable: { A: 120, B: 98, C: 77 },
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
console.log(stats);
|
|
340
|
+
// { objectTombstonesRemoved: number, sequenceTombstonesRemoved: number }
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
For server-side workflows operating on raw docs, use internals:
|
|
344
|
+
|
|
345
|
+
```ts
|
|
346
|
+
import { compactDocTombstones } from "json-patch-to-crdt/internals";
|
|
347
|
+
|
|
348
|
+
compactDocTombstones(doc, {
|
|
349
|
+
stable: checkpointVv,
|
|
350
|
+
mutate: true, // optional in-place compaction
|
|
351
|
+
});
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
Safety conditions:
|
|
355
|
+
|
|
356
|
+
- Only compact at checkpoints that are causally stable across all peers you still merge with.
|
|
357
|
+
- Do not merge compacted replicas with peers that may be behind that checkpoint.
|
|
358
|
+
- Compaction preserves materialized JSON output for the compacted document/state.
|
|
359
|
+
|
|
267
360
|
## Serialization
|
|
268
361
|
|
|
269
362
|
```ts
|
|
@@ -323,8 +416,9 @@ Internals helpers like `jsonPatchToCrdtSafe` and `tryMergeDoc` return the same s
|
|
|
323
416
|
- `tryApplyPatchInPlace(state, patch, options?)` - Non-throwing in-place apply result.
|
|
324
417
|
- `validateJsonPatch(baseJson, patch, options?)` - Preflight patch validation (non-mutating).
|
|
325
418
|
- `toJson(docOrState)` - Materialize a JSON value from a doc or state.
|
|
326
|
-
- `applyPatch`/`tryApplyPatch` options: `base` expects a prior `CrdtState` snapshot (not a raw doc), plus `semantics` and `
|
|
419
|
+
- `applyPatch`/`tryApplyPatch` options: `base` expects a prior `CrdtState` snapshot (not a raw doc), plus `semantics`, `testAgainst`, and optional `jsonValidation` runtime guardrails.
|
|
327
420
|
- `PatchError` - Error class thrown for failed patches (`code`, `reason`, `message`, optional `path`/`opIndex`).
|
|
421
|
+
- `JsonValueValidationError` - Error class thrown by strict runtime validation in APIs that accept raw JSON values (for example `createState` and `diffJsonPatch`).
|
|
328
422
|
|
|
329
423
|
### Merge helpers
|
|
330
424
|
|
|
@@ -334,7 +428,7 @@ Internals helpers like `jsonPatchToCrdtSafe` and `tryMergeDoc` return the same s
|
|
|
334
428
|
|
|
335
429
|
### Patch helpers
|
|
336
430
|
|
|
337
|
-
- `diffJsonPatch(baseJson, nextJson, options?)` - Compute a JSON Patch delta between two JSON values.
|
|
431
|
+
- `diffJsonPatch(baseJson, nextJson, options?)` - Compute a JSON Patch delta between two JSON values (`arrayStrategy`, `lcsMaxCells`, and optional `jsonValidation` guardrails).
|
|
338
432
|
|
|
339
433
|
### Serialization
|
|
340
434
|
|
|
@@ -389,6 +483,9 @@ Use `crdtToFullReplace(doc)` from `json-patch-to-crdt/internals`, which emits a
|
|
|
389
483
|
**Why do array deltas look bigger than expected?**
|
|
390
484
|
LCS diffs are deterministic, not minimal. If you prefer one-op array replacement, use `{ arrayStrategy: "atomic" }`.
|
|
391
485
|
|
|
486
|
+
**Why did my array delta become a full `replace` even with LCS?**
|
|
487
|
+
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.
|
|
488
|
+
|
|
392
489
|
**Does LCS guarantee the smallest patch?**
|
|
393
490
|
No. It is deterministic and usually compact, but not guaranteed to be minimal.
|
|
394
491
|
|
|
@@ -401,6 +498,9 @@ By default, `forkState` blocks reusing `origin.clock.actor` because same-actor f
|
|
|
401
498
|
**Why can my local counter jump after a merge?**
|
|
402
499
|
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.
|
|
403
500
|
|
|
501
|
+
**How should I run tombstone compaction in production?**
|
|
502
|
+
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.
|
|
503
|
+
|
|
404
504
|
## Limitations
|
|
405
505
|
|
|
406
506
|
- The array materialization and insert mapping depend on a base snapshot; concurrent inserts resolve by dot order.
|