json-patch-to-crdt 0.5.0 → 1.0.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 +127 -2
- package/dist/{compact-CDvajUfn.js → compact-BUXv4MXQ.js} +749 -129
- package/dist/{compact-Dj0BYeY5.mjs → compact-CncfNnDy.mjs} +672 -130
- package/dist/{depth-CM1kCxhm.d.mts → depth-C5m9qI-V.d.mts} +155 -17
- package/dist/{depth-NbZ6Giq9.d.ts → depth-vwQdqCBN.d.ts} +155 -17
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +18 -2
- package/dist/index.mjs +2 -2
- package/dist/internals.d.mts +4 -3
- package/dist/internals.d.ts +4 -3
- package/dist/internals.js +15 -2
- package/dist/internals.mjs +2 -2
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -104,12 +104,45 @@ For array-heavy snapshots, `diffJsonPatch` and `crdtToJsonPatch` support:
|
|
|
104
104
|
|
|
105
105
|
`lcsMaxCells` only applies to `arrayStrategy: "lcs"`. If the classic LCS matrix for the trimmed unmatched window would exceed the configured cap, the diff falls back to an atomic array `replace`.
|
|
106
106
|
|
|
107
|
-
`lcsLinearMaxCells` is the matching
|
|
107
|
+
`lcsLinearMaxCells` is the matching guardrail for `arrayStrategy: "lcs-linear"` and defaults to `250_000`. It uses the same trimmed unmatched-window estimate, but caps worst-case runtime instead of matrix allocation. When the cap is exceeded, `lcs-linear` also falls back to an atomic array `replace`. Set `lcsLinearMaxCells: Number.POSITIVE_INFINITY` only when you explicitly want the previous unbounded behavior and accept the CPU cost for large unmatched arrays.
|
|
108
108
|
|
|
109
109
|
`diffJsonPatch` keeps the existing `add`/`remove`/`replace` output by default. Set
|
|
110
110
|
`emitMoves` and/or `emitCopies` to opt into deterministic RFC 6902 `move`/`copy`
|
|
111
111
|
rewrites.
|
|
112
112
|
|
|
113
|
+
## Cancellation
|
|
114
|
+
|
|
115
|
+
Expensive high-level APIs accept an optional `signal` compatible with `AbortSignal`:
|
|
116
|
+
`diffJsonPatch`, `applyPatch`, `applyPatchInPlace`, `tryApplyPatch`, `tryApplyPatchInPlace`,
|
|
117
|
+
`mergeState`, `tryMergeState`, `deserializeState`, and `tryDeserializeState`.
|
|
118
|
+
|
|
119
|
+
Cancellation is checked at safe points between traversal steps and array-diff loop iterations.
|
|
120
|
+
Immutable APIs either return no result or leave the input state unchanged when cancelled. In-place
|
|
121
|
+
patch application with `atomic: true` keeps the same all-or-nothing behavior; with `atomic: false`,
|
|
122
|
+
operations already applied before cancellation remain applied. Non-throwing APIs return
|
|
123
|
+
`reason: "OPERATION_CANCELLED"` and throwing APIs throw their usual domain error wrappers where
|
|
124
|
+
applicable.
|
|
125
|
+
|
|
126
|
+
## Performance Gates
|
|
127
|
+
|
|
128
|
+
CI runs `bun run test:perf-gate` as a fixed-size performance smoke test for
|
|
129
|
+
the hottest paths: array diffing, merge traversal, and sequential patch
|
|
130
|
+
application. These gates are intentionally smaller and more stable than the
|
|
131
|
+
microbenchmarks in `bench/`; they fail with the measured median, sample list,
|
|
132
|
+
threshold, and tuning variable so regressions are actionable.
|
|
133
|
+
|
|
134
|
+
Run the gate locally before changing hot-path code:
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
bun run test:perf-gate
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
The default budgets are deliberately generous for shared CI. If a runner needs
|
|
141
|
+
environment-specific tuning, set `PERF_GATE_ARRAY_DIFF_MS`,
|
|
142
|
+
`PERF_GATE_MERGE_TRAVERSAL_MS`, `PERF_GATE_SEQUENTIAL_APPLY_MS`, or
|
|
143
|
+
`PERF_GATE_RUNS`. Use the `bench:*` scripts for deeper investigation and only
|
|
144
|
+
raise a gate after confirming the slowdown is intentional.
|
|
145
|
+
|
|
113
146
|
## Serialize / Restore State
|
|
114
147
|
|
|
115
148
|
```ts
|
|
@@ -118,12 +151,18 @@ import {
|
|
|
118
151
|
createState,
|
|
119
152
|
deserializeState,
|
|
120
153
|
serializeState,
|
|
154
|
+
validateSerializedState,
|
|
121
155
|
toJson,
|
|
122
156
|
} from "json-patch-to-crdt";
|
|
123
157
|
|
|
124
158
|
const state = createState({ counter: 1 }, { actor: "A" });
|
|
125
159
|
const saved = serializeState(state);
|
|
126
160
|
|
|
161
|
+
const validation = validateSerializedState(saved);
|
|
162
|
+
if (!validation.ok) {
|
|
163
|
+
throw new Error(`Invalid snapshot at ${validation.error.path}: ${validation.error.message}`);
|
|
164
|
+
}
|
|
165
|
+
|
|
127
166
|
const restored = deserializeState(saved);
|
|
128
167
|
const next = applyPatch(restored, [{ op: "replace", path: "/counter", value: 2 }]);
|
|
129
168
|
|
|
@@ -135,10 +174,40 @@ Persisted snapshot compatibility:
|
|
|
135
174
|
|
|
136
175
|
- `serializeState(...)` emits a versioned envelope.
|
|
137
176
|
- `deserializeState(...)` accepts both the current versioned format and legacy unversioned snapshots.
|
|
177
|
+
- `validateSerializedState(...)` and `validateSerializedDoc(...)` preflight serialized payloads and return typed failures without requiring callers to catch exceptions.
|
|
138
178
|
- Future envelope versions are rejected until an explicit migration path is added.
|
|
139
179
|
|
|
140
180
|
The same compatibility contract applies to the lower-level `serializeDoc(...)` and
|
|
141
|
-
`deserializeDoc(...)` helpers
|
|
181
|
+
`deserializeDoc(...)` helpers. Use the validation-only helpers at trust boundaries
|
|
182
|
+
when you need to reject malformed snapshots before restoring runtime state; they
|
|
183
|
+
walk the serialized payload and CRDT invariants without allocating the runtime
|
|
184
|
+
document maps returned by the deserializers. Use `deserializeState(...)` or
|
|
185
|
+
`deserializeDoc(...)` when you need the CRDT data structures.
|
|
186
|
+
|
|
187
|
+
### Resource Budgets for Serialized Payloads
|
|
188
|
+
|
|
189
|
+
When accepting serialized CRDT payloads from network clients, pass
|
|
190
|
+
`resourceBudget` limits to `deserializeState(...)`, `deserializeDoc(...)`, or the
|
|
191
|
+
non-throwing `tryDeserialize*` variants. These limits reject hostile shallow
|
|
192
|
+
payloads before validation allocates large maps or walks excessive element sets.
|
|
193
|
+
|
|
194
|
+
```ts
|
|
195
|
+
const result = tryDeserializeState(payload, {
|
|
196
|
+
resourceBudget: {
|
|
197
|
+
objectEntries: 10_000,
|
|
198
|
+
sequenceElements: 50_000,
|
|
199
|
+
serializedElements: 100_000,
|
|
200
|
+
visitedNodes: 100_000,
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Tune these caps to your product limits. For network-exposed services, start with
|
|
206
|
+
caps near the largest document you intend to accept and lower them where request
|
|
207
|
+
size, latency, or memory limits are tighter. `objectEntries` covers object keys
|
|
208
|
+
and tombstone maps, `sequenceElements` covers RGA sequence elements and JSON
|
|
209
|
+
arrays, `serializedElements` caps the combined serialized breadth, and
|
|
210
|
+
`visitedNodes` caps total decoded node/value traversal.
|
|
142
211
|
|
|
143
212
|
## Error Handling
|
|
144
213
|
|
|
@@ -158,6 +227,33 @@ try {
|
|
|
158
227
|
|
|
159
228
|
If you prefer non-throwing results, use `tryApplyPatch(...)` / `tryMergeState(...)`.
|
|
160
229
|
|
|
230
|
+
## Strict Parent Semantics
|
|
231
|
+
|
|
232
|
+
RFC 6902 requires the parent of an `add` target to already exist. For compatibility
|
|
233
|
+
with older CRDT intent flows, missing array parents can still be materialized for
|
|
234
|
+
`/path/0` and `/path/-` inserts when `strictParents` is explicitly disabled.
|
|
235
|
+
|
|
236
|
+
Use the named strict profile for RFC 6902 boundaries:
|
|
237
|
+
|
|
238
|
+
```ts
|
|
239
|
+
import { applyPatch, createState, withStrictRfc6902Parents } from "json-patch-to-crdt";
|
|
240
|
+
|
|
241
|
+
const base = createState({}, { actor: "A" });
|
|
242
|
+
const head = applyPatch(base, [{ op: "add", path: "/items", value: [] }]);
|
|
243
|
+
|
|
244
|
+
applyPatch(head, [{ op: "add", path: "/items/0", value: "x" }], withStrictRfc6902Parents({ base }));
|
|
245
|
+
// throws PatchError: base array missing at /items
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
The error is based on the explicit `base` snapshot used for CRDT array index
|
|
249
|
+
resolution. In this example, `base` predates `/items`; without `{ base }`, the
|
|
250
|
+
current `head` state would be used as the base and the insert would succeed.
|
|
251
|
+
|
|
252
|
+
Callers that intentionally depend on the legacy auto-create behavior should opt
|
|
253
|
+
in explicitly with `withLegacyMissingArrayParents(...)`. That compatibility
|
|
254
|
+
profile is deprecated because accepting missing parents can hide invalid upstream
|
|
255
|
+
patch generation.
|
|
256
|
+
|
|
161
257
|
## Version Vector Helpers
|
|
162
258
|
|
|
163
259
|
`observedVersionVector(...)` lets you inspect the highest observed counter per
|
|
@@ -195,6 +291,7 @@ vector, it returns that vector unchanged.
|
|
|
195
291
|
|
|
196
292
|
`createState`, `applyPatch`, `diffJsonPatch`, and `crdtToJsonPatch` accept a
|
|
197
293
|
`jsonValidation` option for callers that may pass runtime values through `any`.
|
|
294
|
+
The default remains `"none"` for backward compatibility with earlier releases.
|
|
198
295
|
|
|
199
296
|
- `"none"` keeps the current behavior with no extra runtime validation.
|
|
200
297
|
- `"strict"` rejects values that are not valid JSON, including non-finite
|
|
@@ -219,13 +316,39 @@ console.log(toJson(state));
|
|
|
219
316
|
// { keep: true, nested: {}, arr: [null] }
|
|
220
317
|
```
|
|
221
318
|
|
|
319
|
+
For new untrusted-input boundaries, prefer the safe convenience helpers instead
|
|
320
|
+
of relying on every call site to remember `jsonValidation`.
|
|
321
|
+
|
|
322
|
+
```ts
|
|
323
|
+
import {
|
|
324
|
+
applySafePatch,
|
|
325
|
+
createNormalizedState,
|
|
326
|
+
createSafeState,
|
|
327
|
+
diffSafeJsonPatch,
|
|
328
|
+
} from "json-patch-to-crdt";
|
|
329
|
+
|
|
330
|
+
const strictState = createSafeState(inputFromApi, { actor: "A" });
|
|
331
|
+
const strictNext = applySafePatch(strictState, patchFromApi);
|
|
332
|
+
const strictDelta = diffSafeJsonPatch(previousSnapshot, nextSnapshot);
|
|
333
|
+
|
|
334
|
+
const normalizedState = createNormalizedState(inputFromApi, { actor: "A" });
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
The `Safe` helpers use strict validation and reject invalid runtime values. The
|
|
338
|
+
`Normalized` helpers use normalization and coerce invalid runtime values into
|
|
339
|
+
JSON-safe output. Existing `createState`, `applyPatch`, and `diffJsonPatch`
|
|
340
|
+
calls keep their current compatibility defaults unless you opt into a validation
|
|
341
|
+
mode directly.
|
|
342
|
+
|
|
222
343
|
## API Overview
|
|
223
344
|
|
|
224
345
|
Main exports most apps need:
|
|
225
346
|
|
|
226
347
|
- `createState(initial, { actor })`
|
|
348
|
+
- `createSafeState(initial, { actor })` / `createNormalizedState(initial, { actor })`
|
|
227
349
|
- `forkState(origin, actor)`
|
|
228
350
|
- `applyPatch(state, patch, options?)`
|
|
351
|
+
- `applySafePatch(state, patch, options?)` / `applyNormalizedPatch(state, patch, options?)`
|
|
229
352
|
- `tryApplyPatch(state, patch, options?)`
|
|
230
353
|
- `mergeState(local, remote, { actor })`
|
|
231
354
|
- `tryMergeState(local, remote, options?)`
|
|
@@ -236,7 +359,9 @@ Main exports most apps need:
|
|
|
236
359
|
- `compactStateTombstones(state, { stable })`
|
|
237
360
|
- `toJson(stateOrDoc)`
|
|
238
361
|
- `diffJsonPatch(baseJson, nextJson, options?)`
|
|
362
|
+
- `diffSafeJsonPatch(baseJson, nextJson, options?)` / `diffNormalizedJsonPatch(baseJson, nextJson, options?)`
|
|
239
363
|
- `serializeState(state)` / `deserializeState(payload)`
|
|
364
|
+
- `validateSerializedState(payload)` / `validateSerializedDoc(payload)`
|
|
240
365
|
- `validateJsonPatch(baseJson, patch, options?)`
|
|
241
366
|
|
|
242
367
|
Advanced/internal helpers are available from:
|