json-patch-to-crdt 0.1.3 → 0.3.0
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 +81 -410
- package/dist/{compact-BdTuOQK-.mjs → compact-BS7F604m.mjs} +1310 -192
- package/dist/{compact-DoM9CJNR.js → compact-BToZE6Q6.js} +1345 -191
- package/dist/{depth-Dl_yOAKU.d.ts → depth-BTHjgY18.d.mts} +67 -4
- package/dist/{depth-IvWvLAkt.d.mts → depth-DSl2ghKu.d.ts} +67 -4
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -1
- package/dist/index.mjs +2 -2
- package/dist/internals.d.mts +31 -4
- package/dist/internals.d.ts +31 -4
- package/dist/internals.js +7 -1
- package/dist/internals.mjs +2 -2
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -1,92 +1,59 @@
|
|
|
1
1
|
# json-patch-to-crdt
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/json-patch-to-crdt)
|
|
4
|
+
[](LICENSE.md)
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
Convert JSON Patch (RFC 6902) operations into a CRDT-backed state that can be merged across peers, then materialize JSON again.
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
- Maintain a CRDT-compatible document model for sync.
|
|
9
|
-
- Merge divergent document states from multiple peers.
|
|
10
|
-
- Serialize and restore CRDT state safely.
|
|
11
|
-
- Generate JSON Patch deltas using explicit base snapshots.
|
|
8
|
+
Useful when you want:
|
|
12
9
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
-
|
|
16
|
-
- An RGA sequence for arrays.
|
|
17
|
-
- A map with delete-wins semantics for objects.
|
|
10
|
+
- JSON Patch in/out at your app boundary
|
|
11
|
+
- CRDT merges internally for offline/collaborative edits
|
|
12
|
+
- deterministic JSON Patch diffs between snapshots
|
|
18
13
|
|
|
19
14
|
## Install
|
|
20
15
|
|
|
21
|
-
```bash
|
|
22
|
-
bun add json-patch-to-crdt
|
|
23
|
-
```
|
|
24
|
-
|
|
25
16
|
```bash
|
|
26
17
|
npm install json-patch-to-crdt
|
|
27
18
|
```
|
|
28
19
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
- Node.js `>= 18` (for package consumers).
|
|
32
|
-
- TypeScript `^5` when type-checking in your project.
|
|
33
|
-
- Bun `1.3.7` is used for this repo's own build/test scripts.
|
|
34
|
-
|
|
35
|
-
## Testing (Repo)
|
|
36
|
-
|
|
37
|
-
Run all tests:
|
|
20
|
+
Also works with Bun / pnpm:
|
|
38
21
|
|
|
39
22
|
```bash
|
|
40
|
-
bun
|
|
23
|
+
bun add json-patch-to-crdt
|
|
24
|
+
pnpm add json-patch-to-crdt
|
|
41
25
|
```
|
|
42
26
|
|
|
43
|
-
|
|
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
|
-
```
|
|
27
|
+
Node.js `>=18`.
|
|
52
28
|
|
|
53
|
-
## Quick Start
|
|
29
|
+
## Quick Start
|
|
54
30
|
|
|
55
31
|
```ts
|
|
56
32
|
import { applyPatch, createState, toJson, type JsonPatchOp } from "json-patch-to-crdt";
|
|
57
33
|
|
|
58
|
-
const state = createState({
|
|
34
|
+
const state = createState({ todos: ["write docs"], done: false }, { actor: "client-A" });
|
|
59
35
|
|
|
60
36
|
const patch: JsonPatchOp[] = [
|
|
61
|
-
{ op: "add", path: "/
|
|
62
|
-
{ op: "replace", path: "/
|
|
37
|
+
{ op: "add", path: "/todos/-", value: "ship package" },
|
|
38
|
+
{ op: "replace", path: "/done", value: true },
|
|
63
39
|
];
|
|
64
40
|
|
|
65
|
-
|
|
66
|
-
const next = applyPatch(state, patch);
|
|
67
|
-
console.log(toJson(next));
|
|
68
|
-
} catch (err) {
|
|
69
|
-
// PatchError has a `.code` you can inspect if needed.
|
|
70
|
-
throw err;
|
|
71
|
-
}
|
|
72
|
-
```
|
|
41
|
+
const next = applyPatch(state, patch);
|
|
73
42
|
|
|
74
|
-
|
|
43
|
+
console.log(toJson(next));
|
|
44
|
+
// { todos: ["write docs", "ship package"], done: true }
|
|
45
|
+
```
|
|
75
46
|
|
|
76
|
-
|
|
47
|
+
## Merge Two Peers
|
|
77
48
|
|
|
78
49
|
```ts
|
|
79
50
|
import { applyPatch, createState, forkState, mergeState, toJson } from "json-patch-to-crdt";
|
|
80
51
|
|
|
81
|
-
// Both peers start from the same origin state.
|
|
82
52
|
const origin = createState({ count: 0, items: ["a"] }, { actor: "origin" });
|
|
83
53
|
|
|
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).
|
|
86
54
|
const peerA = forkState(origin, "A");
|
|
87
55
|
const peerB = forkState(origin, "B");
|
|
88
56
|
|
|
89
|
-
// Peers diverge with independent edits.
|
|
90
57
|
const a1 = applyPatch(peerA, [
|
|
91
58
|
{ op: "replace", path: "/count", value: 1 },
|
|
92
59
|
{ op: "add", path: "/items/-", value: "b" },
|
|
@@ -97,412 +64,116 @@ const b1 = applyPatch(peerB, [
|
|
|
97
64
|
{ op: "add", path: "/items/-", value: "c" },
|
|
98
65
|
]);
|
|
99
66
|
|
|
100
|
-
|
|
101
|
-
const mergedAtA = mergeState(a1, b1, { actor: "A" });
|
|
102
|
-
const mergedAtB = mergeState(b1, a1, { actor: "B" });
|
|
67
|
+
const merged = mergeState(a1, b1, { actor: "A" });
|
|
103
68
|
|
|
104
|
-
console.log(toJson(
|
|
69
|
+
console.log(toJson(merged));
|
|
105
70
|
// { count: 2, items: ["a", "c", "b"] }
|
|
106
|
-
// (both appends preserved; sibling order follows dot ordering)
|
|
107
|
-
|
|
108
|
-
// Both peers can continue editing safely.
|
|
109
|
-
const a2 = applyPatch(mergedAtA, [{ op: "replace", path: "/count", value: 3 }]);
|
|
110
|
-
const b2 = applyPatch(mergedAtB, [{ op: "add", path: "/items/-", value: "d" }]);
|
|
111
|
-
|
|
112
|
-
// Merge again to converge.
|
|
113
|
-
const converged = mergeState(a2, b2, { actor: "A" });
|
|
114
|
-
console.log(toJson(converged));
|
|
115
|
-
// { count: 3, items: ["a", "c", "b", "d"] }
|
|
116
71
|
```
|
|
117
72
|
|
|
118
|
-
##
|
|
119
|
-
|
|
120
|
-
- **Doc**: CRDT document node graph (primarily an internals concept).
|
|
121
|
-
- **State**: `{ doc, clock }`, used by the main API.
|
|
122
|
-
- **Base snapshot**: for `applyPatch`, pass a prior `CrdtState`; internals APIs may use raw `Doc` snapshots.
|
|
123
|
-
|
|
124
|
-
## Ordered Event Log Server Pattern
|
|
125
|
-
|
|
126
|
-
If your service contract is "JSON Patch in / JSON Patch out", and your backend keeps CRDT metadata internally:
|
|
127
|
-
|
|
128
|
-
- Keep one authoritative CRDT head per document.
|
|
129
|
-
- Keep a version vector keyed by actor ID.
|
|
130
|
-
- On each incoming JSON Patch, call `applyPatchAsActor(headDoc, vv, actor, patch, { base })`.
|
|
131
|
-
- Append the accepted event to your ordered log.
|
|
132
|
-
- For downstream clients, emit `crdtToJsonPatch(clientBaseDoc, currentHeadDoc)`.
|
|
133
|
-
|
|
134
|
-
Minimal shape (advanced API via `json-patch-to-crdt/internals`):
|
|
135
|
-
|
|
136
|
-
```ts
|
|
137
|
-
import {
|
|
138
|
-
applyPatchAsActor,
|
|
139
|
-
PatchError,
|
|
140
|
-
crdtToJsonPatch,
|
|
141
|
-
createState,
|
|
142
|
-
type Doc,
|
|
143
|
-
type JsonPatchOp,
|
|
144
|
-
type VersionVector,
|
|
145
|
-
} from "json-patch-to-crdt/internals";
|
|
146
|
-
|
|
147
|
-
let head: Doc = createState({ list: [] }, { actor: "server" }).doc;
|
|
148
|
-
let vv: VersionVector = {};
|
|
149
|
-
|
|
150
|
-
function applyIncomingPatch(
|
|
151
|
-
actor: string,
|
|
152
|
-
base: Doc,
|
|
153
|
-
patch: JsonPatchOp[],
|
|
154
|
-
): { ok: true; outPatch: JsonPatchOp[] } | { ok: false; code: number; message: string } {
|
|
155
|
-
try {
|
|
156
|
-
const applied = applyPatchAsActor(head, vv, actor, patch, { base });
|
|
157
|
-
head = applied.state.doc;
|
|
158
|
-
vv = applied.vv;
|
|
159
|
-
|
|
160
|
-
// Persist incoming event and/or outPatch in your append-only ordered log.
|
|
161
|
-
const outPatch = crdtToJsonPatch(base, head);
|
|
162
|
-
return { ok: true, outPatch };
|
|
163
|
-
} catch (error) {
|
|
164
|
-
if (error instanceof PatchError) {
|
|
165
|
-
return { ok: false, code: error.code, message: error.message };
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
throw error;
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
```
|
|
172
|
-
|
|
173
|
-
If you prefer a non-throwing low-level compile+apply path, use `jsonPatchToCrdtSafe` from `json-patch-to-crdt/internals`.
|
|
174
|
-
|
|
175
|
-
## Patch Semantics
|
|
176
|
-
|
|
177
|
-
- Patches are interpreted relative to a base snapshot.
|
|
178
|
-
- `applyPatch` defaults to RFC-style sequential patch execution.
|
|
179
|
-
- You can pass an explicit base state via `applyPatch(state, patch, { base })`.
|
|
180
|
-
- Patch semantics are configurable: `semantics: "sequential"` (default) or `"base"`.
|
|
181
|
-
- In `sequential` mode with an explicit `base`, operations are interpreted against a rolling base snapshot while being applied step-by-step to the evolving head.
|
|
182
|
-
- Array indexes are mapped to element IDs based on the base snapshot.
|
|
183
|
-
- `"-"` is treated as append for array inserts.
|
|
184
|
-
- `test` operations can be evaluated against `head` or `base` using the `testAgainst` option.
|
|
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
|
-
|
|
215
|
-
### Semantics Modes
|
|
216
|
-
|
|
217
|
-
- `semantics: "sequential"` (default): applies operations one-by-one against the evolving head (RFC-like execution).
|
|
218
|
-
- `semantics: "base"`: interprets the full patch relative to one fixed snapshot.
|
|
219
|
-
|
|
220
|
-
#### Which Mode Should You Use?
|
|
221
|
-
|
|
222
|
-
| If you need... | Use |
|
|
223
|
-
| ------------------------------------------------------------------------- | ------------------------- |
|
|
224
|
-
| Deterministic CRDT-style replay against a known snapshot | `semantics: "base"` |
|
|
225
|
-
| JSON Patch behavior that feels closest to RFC 6902 step-by-step execution | `semantics: "sequential"` |
|
|
226
|
-
| Step-by-step replay from an explicit historical base | `semantics: "sequential"` |
|
|
227
|
-
|
|
228
|
-
Example:
|
|
229
|
-
|
|
230
|
-
```ts
|
|
231
|
-
const baseMode = applyPatch(state, [{ op: "add", path: "/list/0", value: "x" }], {
|
|
232
|
-
semantics: "base",
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
const sequentialMode = applyPatch(state, [{ op: "add", path: "/list/0", value: "x" }], {
|
|
236
|
-
semantics: "sequential",
|
|
237
|
-
});
|
|
238
|
-
```
|
|
239
|
-
|
|
240
|
-
## Delta Patches (First-Class)
|
|
241
|
-
|
|
242
|
-
For most applications, diff JSON values directly:
|
|
73
|
+
## Generate JSON Patch Deltas
|
|
243
74
|
|
|
244
75
|
```ts
|
|
245
76
|
import { diffJsonPatch } from "json-patch-to-crdt";
|
|
246
77
|
|
|
247
|
-
const
|
|
248
|
-
|
|
78
|
+
const base = { profile: { name: "Sam" }, tags: ["a"] };
|
|
79
|
+
const next = { profile: { name: "Sam", active: true }, tags: ["a", "b"] };
|
|
249
80
|
|
|
250
|
-
|
|
81
|
+
const delta = diffJsonPatch(base, next);
|
|
251
82
|
|
|
252
|
-
|
|
253
|
-
|
|
83
|
+
console.log(delta);
|
|
84
|
+
// [
|
|
85
|
+
// { op: "add", path: "/profile/active", value: true },
|
|
86
|
+
// { op: "add", path: "/tags/1", value: "b" }
|
|
87
|
+
// ]
|
|
254
88
|
|
|
255
|
-
const
|
|
256
|
-
|
|
89
|
+
const reordered = diffJsonPatch(
|
|
90
|
+
{ tags: ["a", "b", "c"] },
|
|
91
|
+
{ tags: ["b", "a", "c"] },
|
|
92
|
+
{ arrayStrategy: "lcs", emitMoves: true },
|
|
93
|
+
);
|
|
257
94
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
```ts
|
|
261
|
-
import { crdtToFullReplace } from "json-patch-to-crdt/internals";
|
|
262
|
-
|
|
263
|
-
const fullPatch = crdtToFullReplace(doc);
|
|
264
|
-
// [{ op: "replace", path: "", value: { ... } }]
|
|
95
|
+
console.log(reordered);
|
|
96
|
+
// [{ op: "move", from: "/tags/0", path: "/tags/1" }]
|
|
265
97
|
```
|
|
266
98
|
|
|
267
|
-
|
|
99
|
+
For array-heavy snapshots, `diffJsonPatch` and `crdtToJsonPatch` support:
|
|
268
100
|
|
|
269
|
-
|
|
270
|
-
|
|
101
|
+
- `arrayStrategy: "lcs"`: deterministic index-level array edits using the classic LCS matrix. This is the default.
|
|
102
|
+
- `arrayStrategy: "lcs-linear"`: deterministic index-level array edits using a lower-memory linear-space LCS traversal.
|
|
103
|
+
- `arrayStrategy: "atomic"`: replace the whole array with a single patch operation.
|
|
271
104
|
|
|
272
|
-
If
|
|
105
|
+
`lcsMaxCells` only applies to `arrayStrategy: "lcs"`. If the classic LCS matrix would exceed the configured cap, the diff falls back to an atomic array `replace`. Use `lcs-linear` when you want index-level patches for larger arrays without allocating the full matrix, but note that it still has `O(n * m)` time complexity and does not automatically fall back for very large unmatched windows.
|
|
273
106
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
107
|
+
`diffJsonPatch` keeps the existing `add`/`remove`/`replace` output by default. Set
|
|
108
|
+
`emitMoves` and/or `emitCopies` to opt into deterministic RFC 6902 `move`/`copy`
|
|
109
|
+
rewrites.
|
|
277
110
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
```ts
|
|
281
|
-
const delta = diffJsonPatch(baseJson, nextJson, {
|
|
282
|
-
arrayStrategy: "lcs",
|
|
283
|
-
lcsMaxCells: 500_000,
|
|
284
|
-
});
|
|
285
|
-
```
|
|
286
|
-
|
|
287
|
-
Notes:
|
|
288
|
-
|
|
289
|
-
- LCS diffs are deterministic but not necessarily minimal.
|
|
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.
|
|
294
|
-
|
|
295
|
-
## Merging
|
|
296
|
-
|
|
297
|
-
Merge full states:
|
|
298
|
-
|
|
299
|
-
```ts
|
|
300
|
-
import { mergeState } from "json-patch-to-crdt";
|
|
301
|
-
|
|
302
|
-
// Merge full states (preserve local actor identity):
|
|
303
|
-
const mergedState = mergeState(stateA, stateB, { actor: "A" });
|
|
304
|
-
```
|
|
305
|
-
|
|
306
|
-
If you need low-level document-only merging, use `mergeDoc` from `json-patch-to-crdt/internals`.
|
|
307
|
-
|
|
308
|
-
By default, merge checks that non-empty arrays share lineage (common element IDs).
|
|
309
|
-
If you intentionally need best-effort merging of unrelated array histories, disable this guard:
|
|
310
|
-
|
|
311
|
-
```ts
|
|
312
|
-
import { mergeDoc } from "json-patch-to-crdt/internals";
|
|
313
|
-
|
|
314
|
-
const mergedDoc = mergeDoc(docA, docB, { requireSharedOrigin: false });
|
|
315
|
-
```
|
|
316
|
-
|
|
317
|
-
Resolution rules:
|
|
318
|
-
|
|
319
|
-
- **LWW registers**: the register with the higher dot wins.
|
|
320
|
-
- **Objects**: entries merge key-by-key; delete-wins semantics apply.
|
|
321
|
-
- **RGA arrays**: elements union by ID; tombstones propagate (delete wins).
|
|
322
|
-
- **Kind mismatch**: the node with the higher representative dot wins.
|
|
323
|
-
|
|
324
|
-
`mergeDoc` is commutative (`merge(a, b)` equals `merge(b, a)`) and idempotent.
|
|
325
|
-
For `mergeState`, pass the local actor explicitly (or as the first argument) so each peer keeps a stable actor ID.
|
|
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
|
-
|
|
360
|
-
## Serialization
|
|
111
|
+
## Serialize / Restore State
|
|
361
112
|
|
|
362
113
|
```ts
|
|
363
114
|
import {
|
|
115
|
+
applyPatch,
|
|
364
116
|
createState,
|
|
365
|
-
serializeState,
|
|
366
117
|
deserializeState,
|
|
367
|
-
|
|
118
|
+
serializeState,
|
|
368
119
|
toJson,
|
|
369
120
|
} from "json-patch-to-crdt";
|
|
370
121
|
|
|
371
|
-
const state = createState({
|
|
372
|
-
const
|
|
122
|
+
const state = createState({ counter: 1 }, { actor: "A" });
|
|
123
|
+
const saved = serializeState(state);
|
|
373
124
|
|
|
374
|
-
const restored = deserializeState(
|
|
375
|
-
const next = applyPatch(restored, [{ op: "replace", path: "/
|
|
125
|
+
const restored = deserializeState(saved);
|
|
126
|
+
const next = applyPatch(restored, [{ op: "replace", path: "/counter", value: 2 }]);
|
|
376
127
|
|
|
377
128
|
console.log(toJson(next));
|
|
129
|
+
// { counter: 2 }
|
|
378
130
|
```
|
|
379
131
|
|
|
380
|
-
## Supported JSON Patch Ops
|
|
381
|
-
|
|
382
|
-
- `add`, `remove`, `replace`, `move`, `copy`, `test`.
|
|
383
|
-
- `move` and `copy` are compiled to `add` + optional `remove` using the base snapshot.
|
|
384
|
-
- Object operations follow strict parent/target checks (no implicit object path creation).
|
|
385
|
-
|
|
386
132
|
## Error Handling
|
|
387
133
|
|
|
388
|
-
|
|
134
|
+
`applyPatch` throws `PatchError` when a patch cannot be applied.
|
|
389
135
|
|
|
390
136
|
```ts
|
|
391
|
-
import {
|
|
137
|
+
import { PatchError, applyPatch } from "json-patch-to-crdt";
|
|
392
138
|
|
|
393
139
|
try {
|
|
394
|
-
|
|
395
|
-
} catch (
|
|
396
|
-
if (
|
|
397
|
-
console.error(
|
|
140
|
+
applyPatch(state, patch);
|
|
141
|
+
} catch (error) {
|
|
142
|
+
if (error instanceof PatchError) {
|
|
143
|
+
console.error(error.code, error.reason, error.message);
|
|
398
144
|
}
|
|
399
145
|
}
|
|
400
146
|
```
|
|
401
147
|
|
|
402
|
-
|
|
403
|
-
Internals helpers like `jsonPatchToCrdtSafe` and `tryMergeDoc` return the same shape:
|
|
404
|
-
|
|
405
|
-
- `{ ok: false, code: 409, reason, message, path?, opIndex? }`
|
|
406
|
-
|
|
407
|
-
## API Summary
|
|
408
|
-
|
|
409
|
-
### State helpers
|
|
410
|
-
|
|
411
|
-
- `createState(initial, { actor, start? })` - Create a new CRDT state from JSON.
|
|
412
|
-
- `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).
|
|
413
|
-
- `applyPatch(state, patch, options?)` - Apply a patch immutably, returning a new state (`semantics: "sequential"` by default).
|
|
414
|
-
- `applyPatchInPlace(state, patch, options?)` - Apply a patch by mutating state in place (`atomic: true` by default).
|
|
415
|
-
- `tryApplyPatch(state, patch, options?)` - Non-throwing immutable apply (`{ ok: true, state }` or `{ ok: false, error }`).
|
|
416
|
-
- `tryApplyPatchInPlace(state, patch, options?)` - Non-throwing in-place apply result.
|
|
417
|
-
- `validateJsonPatch(baseJson, patch, options?)` - Preflight patch validation (non-mutating).
|
|
418
|
-
- `toJson(docOrState)` - Materialize a JSON value from a doc or state.
|
|
419
|
-
- `applyPatch`/`tryApplyPatch` options: `base` expects a prior `CrdtState` snapshot (not a raw doc), plus `semantics`, `testAgainst`, and optional `jsonValidation` runtime guardrails.
|
|
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`).
|
|
422
|
-
|
|
423
|
-
### Merge helpers
|
|
424
|
-
|
|
425
|
-
- `mergeState(a, b, options?)` - Merge two CRDT states (doc + clock), preserving actor identity (`options.actor`) and optional shared-origin checks.
|
|
426
|
-
- `tryMergeState(a, b, options?)` - Non-throwing merge-state result.
|
|
427
|
-
- `MergeError` - Error class thrown by throwing merge helpers.
|
|
428
|
-
|
|
429
|
-
### Patch helpers
|
|
430
|
-
|
|
431
|
-
- `diffJsonPatch(baseJson, nextJson, options?)` - Compute a JSON Patch delta between two JSON values (`arrayStrategy`, `lcsMaxCells`, and optional `jsonValidation` guardrails).
|
|
148
|
+
If you prefer non-throwing results, use `tryApplyPatch(...)` / `tryMergeState(...)`.
|
|
432
149
|
|
|
433
|
-
|
|
150
|
+
## API Overview
|
|
434
151
|
|
|
435
|
-
|
|
152
|
+
Main exports most apps need:
|
|
436
153
|
|
|
437
|
-
|
|
154
|
+
- `createState(initial, { actor })`
|
|
155
|
+
- `forkState(origin, actor)`
|
|
156
|
+
- `applyPatch(state, patch, options?)`
|
|
157
|
+
- `tryApplyPatch(state, patch, options?)`
|
|
158
|
+
- `mergeState(local, remote, { actor })`
|
|
159
|
+
- `tryMergeState(local, remote, options?)`
|
|
160
|
+
- `toJson(stateOrDoc)`
|
|
161
|
+
- `diffJsonPatch(baseJson, nextJson, options?)`
|
|
162
|
+
- `serializeState(state)` / `deserializeState(payload)`
|
|
163
|
+
- `validateJsonPatch(baseJson, patch, options?)`
|
|
438
164
|
|
|
439
|
-
Advanced helpers are available
|
|
165
|
+
Advanced/internal helpers are available from:
|
|
440
166
|
|
|
441
167
|
```ts
|
|
442
|
-
import {
|
|
443
|
-
applyPatchAsActor,
|
|
444
|
-
createClock,
|
|
445
|
-
docFromJson,
|
|
446
|
-
mergeDoc,
|
|
447
|
-
jsonPatchToCrdtSafe,
|
|
448
|
-
compareDot,
|
|
449
|
-
rgaInsertAfter,
|
|
450
|
-
HEAD,
|
|
451
|
-
} from "json-patch-to-crdt/internals";
|
|
168
|
+
import { crdtToJsonPatch, applyPatchAsActor } from "json-patch-to-crdt/internals";
|
|
452
169
|
```
|
|
453
170
|
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
- Actor/version-vector helpers: `applyPatchAsActor`, `createClock`, `cloneClock`, `nextDotForActor`, `observeDot`.
|
|
457
|
-
- Doc-level APIs: `docFromJson`, `docFromJsonWithDot`, `cloneDoc`, `materialize`, `mergeDoc`, `tryMergeDoc`.
|
|
458
|
-
- Intent compiler/apply pipeline: `compileJsonPatchToIntent`, `applyIntentsToCrdt`, `jsonPatchToCrdt`, `jsonPatchToCrdtSafe`, `tryJsonPatchToCrdt`.
|
|
459
|
-
- Doc delta/serialization helpers: `crdtToJsonPatch`, `crdtToFullReplace`, `serializeDoc`, `deserializeDoc`.
|
|
460
|
-
- CRDT primitives/utilities: `compareDot`, `vvHasDot`, `vvMerge`, `dotToElemId`, `newObj`, `newSeq`, `newReg`, `lwwSet`, `objSet`, `objRemove`, `HEAD`, `rgaInsertAfter`, `rgaDelete`, `rgaLinearizeIds`, `rgaPrevForInsertAtIndex`, `rgaIdAtIndex`.
|
|
461
|
-
|
|
462
|
-
## Determinism
|
|
463
|
-
|
|
464
|
-
- Object key ordering in deltas is stable (sorted keys).
|
|
465
|
-
- LCS array diffs are deterministic.
|
|
466
|
-
- Repeated runs for identical inputs yield identical patches.
|
|
467
|
-
|
|
468
|
-
## FAQ / Troubleshooting
|
|
469
|
-
|
|
470
|
-
**Why did I get `PatchError` with code `409`?**
|
|
471
|
-
This typically means the patch could not be applied against the base snapshot. Common causes:
|
|
472
|
-
|
|
473
|
-
- Array index out of bounds relative to the base snapshot.
|
|
474
|
-
- `test` op failed (value mismatch).
|
|
475
|
-
- Base array missing for a non-append insert.
|
|
476
|
-
|
|
477
|
-
**How do I avoid `409` for arrays?**
|
|
478
|
-
Always pass a base state snapshot that matches the array you are patching. If the array may be missing, create the parent path explicitly before inserting into it.
|
|
479
|
-
|
|
480
|
-
**How do I get a full-state patch instead of a delta?**
|
|
481
|
-
Use `crdtToFullReplace(doc)` from `json-patch-to-crdt/internals`, which emits a single root `replace` patch.
|
|
482
|
-
|
|
483
|
-
**Why do array deltas look bigger than expected?**
|
|
484
|
-
LCS diffs are deterministic, not minimal. If you prefer one-op array replacement, use `{ arrayStrategy: "atomic" }`.
|
|
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
|
-
|
|
489
|
-
**Does LCS guarantee the smallest patch?**
|
|
490
|
-
No. It is deterministic and usually compact, but not guaranteed to be minimal.
|
|
491
|
-
|
|
492
|
-
**How do I merge states from two peers?**
|
|
493
|
-
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.
|
|
494
|
-
|
|
495
|
-
**Why did `forkState` throw about actor uniqueness?**
|
|
496
|
-
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 })`.
|
|
497
|
-
|
|
498
|
-
**Why can my local counter jump after a merge?**
|
|
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.
|
|
171
|
+
## Notes
|
|
500
172
|
|
|
501
|
-
|
|
502
|
-
|
|
173
|
+
- Arrays use a CRDT sequence internally; concurrent inserts are preserved.
|
|
174
|
+
- Patches are interpreted relative to a snapshot (RFC-style sequential execution by default).
|
|
175
|
+
- Merge assumes replicas come from the same origin state (use `forkState`).
|
|
503
176
|
|
|
504
|
-
##
|
|
177
|
+
## License
|
|
505
178
|
|
|
506
|
-
|
|
507
|
-
- Under highly skewed peer counters, local counters may jump upward after merges to preserve deterministic insert ordering.
|
|
508
|
-
- Merge requires both peers to have started from the same origin document so that shared elements have matching IDs.
|
|
179
|
+
MIT
|