json-patch-to-crdt 0.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.
@@ -0,0 +1,419 @@
1
+ //#region src/types.d.ts
2
+ /** Unique identifier for a CRDT replica (peer). */
3
+ type ActorId = string;
4
+ /** A Lamport-style causality marker: `(actor, counter)`. */
5
+ type Dot = {
6
+ actor: ActorId; /** Strictly increasing per actor. */
7
+ ctr: number;
8
+ };
9
+ /** Maps each known actor to the highest counter seen from that actor. */
10
+ type VersionVector = Record<ActorId, number>;
11
+ /** A JSON leaf value: `null`, `boolean`, `number`, or `string`. */
12
+ type JsonPrimitive = null | boolean | number | string;
13
+ /** Any JSON-compatible value (primitives, arrays, and plain objects). */
14
+ type JsonValue = JsonPrimitive | JsonValue[] | {
15
+ [k: string]: JsonValue;
16
+ };
17
+ /** Mutable clock that tracks an actor's identity and monotonic counter. */
18
+ type Clock = {
19
+ actor: ActorId;
20
+ ctr: number; /** Generate the next unique dot and advance the counter. */
21
+ next: () => Dot;
22
+ };
23
+ /** Last-Writer-Wins register: stores a single JSON value tagged with a dot. */
24
+ type LwwReg = {
25
+ kind: "lww";
26
+ value: JsonValue;
27
+ dot: Dot;
28
+ };
29
+ /** Serialized element ID in the form `"actor:counter"`. */
30
+ type ElemId = string;
31
+ /** A single element in an RGA sequence, with tombstone support. */
32
+ type RgaElem = {
33
+ id: ElemId; /** Predecessor element ID (`"HEAD"` for the first element). */
34
+ prev: ElemId; /** Whether this element has been logically deleted. */
35
+ tombstone: boolean; /** The child CRDT node stored at this position. */
36
+ value: Node; /** Dot used for deterministic ordering among concurrent inserts with the same predecessor. */
37
+ insDot: Dot;
38
+ };
39
+ /** Replicated Growable Array: an ordered sequence CRDT with tombstones. */
40
+ type RgaSeq = {
41
+ kind: "seq";
42
+ elems: Map<ElemId, RgaElem>;
43
+ };
44
+ /** A single entry in an object node: a child node tagged with a dot. */
45
+ type ObjEntry = {
46
+ node: Node;
47
+ dot: Dot;
48
+ };
49
+ /** Delete-wins object CRDT: a map of string keys to child nodes. */
50
+ type ObjNode = {
51
+ kind: "obj";
52
+ entries: Map<string, ObjEntry>; /** Latest delete dot per key (delete-wins semantics). */
53
+ tombstone: Map<string, Dot>;
54
+ };
55
+ /** A CRDT node: either an object, an RGA sequence, or a LWW register. */
56
+ type Node = ObjNode | RgaSeq | LwwReg;
57
+ /** JSON-serializable form of an RGA element. */
58
+ type SerializedRgaElem = {
59
+ id: ElemId;
60
+ prev: ElemId;
61
+ tombstone: boolean;
62
+ value: SerializedNode;
63
+ insDot: Dot;
64
+ };
65
+ /** JSON-serializable form of any CRDT node. */
66
+ type SerializedNode = {
67
+ kind: "lww";
68
+ value: JsonValue;
69
+ dot: Dot;
70
+ } | {
71
+ kind: "obj";
72
+ entries: Record<string, {
73
+ node: SerializedNode;
74
+ dot: Dot;
75
+ }>;
76
+ tombstone: Record<string, Dot>;
77
+ } | {
78
+ kind: "seq";
79
+ elems: Record<string, SerializedRgaElem>;
80
+ };
81
+ /** JSON-serializable form of a CRDT document. */
82
+ type SerializedDoc = {
83
+ root: SerializedNode;
84
+ };
85
+ /** JSON-serializable form of a clock. */
86
+ type SerializedClock = {
87
+ actor: ActorId;
88
+ ctr: number;
89
+ };
90
+ /** JSON-serializable form of a full CRDT state (document + clock). */
91
+ type SerializedState = {
92
+ doc: SerializedDoc;
93
+ clock: SerializedClock;
94
+ };
95
+ /**
96
+ * Internal intent operations produced by compiling RFC 6902 JSON Patch ops.
97
+ * Each variant maps to a specific CRDT mutation.
98
+ */
99
+ type IntentOp = {
100
+ t: "ObjSet";
101
+ path: string[];
102
+ key: string;
103
+ value: JsonValue;
104
+ mode?: "add" | "replace";
105
+ } | {
106
+ t: "ObjRemove";
107
+ path: string[];
108
+ key: string;
109
+ } | {
110
+ t: "ArrInsert";
111
+ path: string[];
112
+ index: number;
113
+ value: JsonValue;
114
+ } | {
115
+ t: "ArrDelete";
116
+ path: string[];
117
+ index: number;
118
+ } | {
119
+ t: "ArrReplace";
120
+ path: string[];
121
+ index: number;
122
+ value: JsonValue;
123
+ } | {
124
+ t: "Test";
125
+ path: string[];
126
+ value: JsonValue;
127
+ };
128
+ /** A single RFC 6902 JSON Patch operation. */
129
+ type JsonPatchOp = {
130
+ op: "add";
131
+ path: string;
132
+ value: JsonValue;
133
+ } | {
134
+ op: "remove";
135
+ path: string;
136
+ } | {
137
+ op: "replace";
138
+ path: string;
139
+ value: JsonValue;
140
+ } | {
141
+ op: "move";
142
+ from: string;
143
+ path: string;
144
+ } | {
145
+ op: "copy";
146
+ from: string;
147
+ path: string;
148
+ } | {
149
+ op: "test";
150
+ path: string;
151
+ value: JsonValue;
152
+ };
153
+ /** An array of JSON Patch operations (RFC 6902). */
154
+ type JsonPatch = JsonPatchOp[];
155
+ /** Top-level CRDT document wrapper holding the root node. */
156
+ type Doc = {
157
+ root: Node;
158
+ };
159
+ /** Combined CRDT state: a document and its associated clock. */
160
+ type CrdtState = {
161
+ doc: Doc;
162
+ clock: Clock;
163
+ };
164
+ /** Result from applying a patch for a specific actor using a shared version vector. */
165
+ type ApplyPatchAsActorResult = {
166
+ /** Updated CRDT state for the actor that produced this patch. */state: CrdtState; /** Updated version vector after applying the patch. */
167
+ vv: VersionVector;
168
+ };
169
+ /** Result of applying a patch: success or a 409 conflict with a message. */
170
+ type ApplyResult = {
171
+ ok: true;
172
+ } | {
173
+ ok: false;
174
+ code: 409;
175
+ message: string;
176
+ };
177
+ /** How JSON Patch operations are interpreted during application. */
178
+ type PatchSemantics = "base" | "sequential";
179
+ /**
180
+ * Options for `applyPatch` / `applyPatchInPlace`.
181
+ * - `semantics: "base"` maps array indices against a fixed snapshot (current default).
182
+ * - `semantics: "sequential"` applies operations one-by-one against the evolving head.
183
+ * - `atomic` applies only to `applyPatchInPlace` (ignored by immutable `applyPatch`).
184
+ */
185
+ type ApplyPatchOptions = {
186
+ base?: Doc;
187
+ testAgainst?: "head" | "base";
188
+ semantics?: PatchSemantics;
189
+ atomic?: boolean;
190
+ };
191
+ /** Options for `mergeState`. */
192
+ type MergeStateOptions = {
193
+ /**
194
+ * Actor to use for the merged clock.
195
+ * Defaults to the actor from the first state argument.
196
+ */
197
+ actor?: ActorId;
198
+ /**
199
+ * Require array sequences to share element lineage before merging.
200
+ * Defaults to `true`.
201
+ */
202
+ requireSharedOrigin?: boolean;
203
+ };
204
+ /** Options for `mergeDoc`. */
205
+ type MergeDocOptions = {
206
+ /**
207
+ * Require array sequences to share element lineage before merging.
208
+ * Defaults to `true`.
209
+ */
210
+ requireSharedOrigin?: boolean;
211
+ };
212
+ /** Options for `crdtToJsonPatch` and `diffJsonPatch`. */
213
+ type DiffOptions = {
214
+ arrayStrategy?: "atomic" | "lcs";
215
+ };
216
+ /**
217
+ * Internal sentinel key used in `IntentOp` to represent root-level operations.
218
+ * Namespaced to avoid collision with user data keys.
219
+ */
220
+ declare const ROOT_KEY = "@@crdt/root";
221
+ //#endregion
222
+ //#region src/state.d.ts
223
+ /** Error thrown when a JSON Patch cannot be applied. Includes a numeric `.code` (409 for conflicts). */
224
+ declare class PatchError extends Error {
225
+ readonly code: number;
226
+ constructor(message: string, code?: number);
227
+ }
228
+ /**
229
+ * Create a new CRDT state from an initial JSON value.
230
+ * @param initial - The initial JSON document.
231
+ * @param options - Actor ID and optional starting counter.
232
+ * @returns A new `CrdtState` containing the document and clock.
233
+ */
234
+ declare function createState(initial: JsonValue, options: {
235
+ actor: ActorId;
236
+ start?: number;
237
+ }): CrdtState;
238
+ /**
239
+ * Materialize a CRDT document or state back to a plain JSON value.
240
+ * @param target - A `Doc` or `CrdtState` to materialize.
241
+ * @returns The JSON representation of the current document.
242
+ */
243
+ declare function toJson(target: Doc | CrdtState): JsonValue;
244
+ /**
245
+ * Apply a JSON Patch to the state, returning a new immutable state.
246
+ * Throws `PatchError` on conflict (e.g. out-of-bounds index, failed test op).
247
+ * @param state - The current CRDT state.
248
+ * @param patch - Array of RFC 6902 JSON Patch operations.
249
+ * @param options - Optional base document and test evaluation mode.
250
+ * @returns A new `CrdtState` with the patch applied.
251
+ */
252
+ declare function applyPatch(state: CrdtState, patch: JsonPatchOp[], options?: ApplyPatchOptions): CrdtState;
253
+ /**
254
+ * Apply a JSON Patch to the state in place, mutating the existing state.
255
+ * Throws `PatchError` on conflict.
256
+ * @param state - The CRDT state to mutate.
257
+ * @param patch - Array of RFC 6902 JSON Patch operations.
258
+ * @param options - Optional base document and test evaluation mode.
259
+ */
260
+ declare function applyPatchInPlace(state: CrdtState, patch: JsonPatchOp[], options?: ApplyPatchOptions): void;
261
+ /**
262
+ * Apply a JSON Patch as a specific actor while maintaining an external version vector.
263
+ * Returns the updated state and a new version vector snapshot.
264
+ */
265
+ declare function applyPatchAsActor(doc: Doc, vv: VersionVector, actor: ActorId, patch: JsonPatchOp[], options?: ApplyPatchOptions): ApplyPatchAsActorResult;
266
+ //#endregion
267
+ //#region src/clock.d.ts
268
+ /**
269
+ * Create a new clock for the given actor. Each call to `clock.next()` yields a fresh `Dot`.
270
+ * @param actor - Unique identifier for this peer.
271
+ * @param start - Initial counter value (defaults to 0).
272
+ */
273
+ declare function createClock(actor: ActorId, start?: number): Clock;
274
+ /** Create an independent copy of a clock at the same counter position. */
275
+ declare function cloneClock(clock: Clock): Clock;
276
+ /**
277
+ * Generate the next per-actor dot from a mutable version vector.
278
+ * Useful when a server needs to mint dots for many actors.
279
+ */
280
+ declare function nextDotForActor(vv: VersionVector, actor: ActorId): Dot;
281
+ /** Record an observed dot in a version vector. */
282
+ declare function observeDot(vv: VersionVector, dot: Dot): void;
283
+ //#endregion
284
+ //#region src/doc.d.ts
285
+ /**
286
+ * Create a CRDT document from a JSON value, using fresh dots for each node.
287
+ * @param value - The JSON value to convert.
288
+ * @param nextDot - A function that generates a unique `Dot` on each call.
289
+ * @returns A new CRDT `Doc`.
290
+ */
291
+ declare function docFromJson(value: JsonValue, nextDot: () => Dot): Doc;
292
+ /**
293
+ * Legacy: create a doc using a single dot with counter offsets for array children.
294
+ * Prefer `docFromJson(value, nextDot)` to ensure unique dots per node.
295
+ */
296
+ declare function docFromJsonWithDot(value: JsonValue, dot: Dot): Doc;
297
+ /** Deep-clone a CRDT document. The clone is fully independent of the original. */
298
+ declare function cloneDoc(doc: Doc): Doc;
299
+ /**
300
+ * Apply compiled intent operations to a CRDT document.
301
+ * Array indices are resolved against the base document.
302
+ * @param base - The base document snapshot used for index mapping and test evaluation.
303
+ * @param head - The target document to mutate.
304
+ * @param intents - Compiled intent operations from `compileJsonPatchToIntent`.
305
+ * @param newDot - A function that generates a unique `Dot` per mutation.
306
+ * @param evalTestAgainst - Whether `test` ops are evaluated against `"head"` or `"base"`.
307
+ * @param bumpCounterAbove - Optional hook that can fast-forward the underlying counter before inserts.
308
+ * @returns `{ ok: true }` on success, or `{ ok: false, code: 409, message }` on conflict.
309
+ */
310
+ declare function applyIntentsToCrdt(base: Doc, head: Doc, intents: IntentOp[], newDot: () => Dot, evalTestAgainst?: "head" | "base", bumpCounterAbove?: (ctr: number) => void): ApplyResult;
311
+ /**
312
+ * Convenience wrapper: compile a JSON Patch and apply it to a CRDT document.
313
+ * @param base - The base document for index resolution.
314
+ * @param head - The target document to mutate.
315
+ * @param patch - Array of RFC 6902 JSON Patch operations.
316
+ * @param newDot - A function that generates a unique `Dot` per mutation.
317
+ * @param evalTestAgainst - Whether `test` ops evaluate against `"head"` or `"base"`.
318
+ * @param bumpCounterAbove - Optional hook that can fast-forward the underlying counter before inserts.
319
+ * @returns `{ ok: true }` on success, or `{ ok: false, code: 409, message }` on conflict.
320
+ */
321
+ declare function jsonPatchToCrdt(base: Doc, head: Doc, patch: JsonPatchOp[], newDot: () => Dot, evalTestAgainst?: "head" | "base", bumpCounterAbove?: (ctr: number) => void): ApplyResult;
322
+ /**
323
+ * Safe wrapper around `jsonPatchToCrdt` that converts compile-time errors into `409` results.
324
+ * This function never throws for malformed/invalid patch paths.
325
+ */
326
+ declare function jsonPatchToCrdtSafe(base: Doc, head: Doc, patch: JsonPatchOp[], newDot: () => Dot, evalTestAgainst?: "head" | "base", bumpCounterAbove?: (ctr: number) => void): ApplyResult;
327
+ /**
328
+ * Generate a JSON Patch delta between two CRDT documents.
329
+ * @param base - The base document snapshot.
330
+ * @param head - The current document state.
331
+ * @param options - Diff options (e.g. `{ arrayStrategy: "lcs" }`).
332
+ * @returns An array of JSON Patch operations that transform base into head.
333
+ */
334
+ declare function crdtToJsonPatch(base: Doc, head: Doc, options?: DiffOptions): JsonPatchOp[];
335
+ /**
336
+ * Emit a single root `replace` patch representing the full document state.
337
+ * Use `crdtToJsonPatch(base, head)` for delta patches instead.
338
+ */
339
+ declare function crdtToFullReplace(doc: Doc): JsonPatchOp[];
340
+ //#endregion
341
+ //#region src/patch.d.ts
342
+ /**
343
+ * Parse an RFC 6901 JSON Pointer into a path array, unescaping `~1` and `~0`.
344
+ * @param ptr - A JSON Pointer string (e.g. `"/a/b"` or `""`).
345
+ * @returns An array of path segments.
346
+ */
347
+ declare function parseJsonPointer(ptr: string): string[];
348
+ /** Convert a path array back to an RFC 6901 JSON Pointer string. */
349
+ declare function stringifyJsonPointer(path: string[]): string;
350
+ /**
351
+ * Navigate a JSON value by path and return the value at that location.
352
+ * Throws if the path is invalid, out of bounds, or traverses a non-container.
353
+ */
354
+ declare function getAtJson(base: JsonValue, path: string[]): JsonValue;
355
+ /**
356
+ * Compile RFC 6902 JSON Patch operations into CRDT intent operations.
357
+ * `move`/`copy` are expanded to `add` + optional `remove`. Array indices
358
+ * and the `"-"` append token are resolved against the base JSON.
359
+ * @param baseJson - The base JSON value for resolving paths.
360
+ * @param patch - Array of JSON Patch operations.
361
+ * @returns An array of `IntentOp` ready for `applyIntentsToCrdt`.
362
+ */
363
+ declare function compileJsonPatchToIntent(baseJson: JsonValue, patch: JsonPatchOp[]): IntentOp[];
364
+ /**
365
+ * Compute a JSON Patch delta between two JSON values.
366
+ * By default arrays use a deterministic LCS strategy.
367
+ * Pass `{ arrayStrategy: "atomic" }` for single-op array replacement.
368
+ * @param base - The original JSON value.
369
+ * @param next - The target JSON value.
370
+ * @param options - Diff options.
371
+ * @returns An array of JSON Patch operations that transform `base` into `next`.
372
+ */
373
+ declare function diffJsonPatch(base: JsonValue, next: JsonValue, options?: DiffOptions): JsonPatchOp[];
374
+ /** Deep equality check for JSON values (null-safe, handles arrays and objects). */
375
+ declare function jsonEquals(a: JsonValue, b: JsonValue): boolean;
376
+ //#endregion
377
+ //#region src/serialize.d.ts
378
+ /** Serialize a CRDT document to a JSON-safe representation (Maps become plain objects). */
379
+ declare function serializeDoc(doc: Doc): SerializedDoc;
380
+ /** Reconstruct a CRDT document from its serialized form. */
381
+ declare function deserializeDoc(data: SerializedDoc): Doc;
382
+ /** Serialize a full CRDT state (document + clock) to a JSON-safe representation. */
383
+ declare function serializeState(state: CrdtState): SerializedState;
384
+ /** Reconstruct a full CRDT state from its serialized form, restoring the clock. */
385
+ declare function deserializeState(data: SerializedState): CrdtState;
386
+ //#endregion
387
+ //#region src/materialize.d.ts
388
+ /** Recursively convert a CRDT node graph into a plain JSON value. */
389
+ declare function materialize(node: Node): JsonValue;
390
+ //#endregion
391
+ //#region src/merge.d.ts
392
+ /**
393
+ * Merge two CRDT documents from different peers into one.
394
+ * By default this requires shared array lineage for non-empty sequences.
395
+ *
396
+ * Resolution rules:
397
+ * - **LwwReg**: the register with the higher dot wins (total order by counter then actor).
398
+ * - **ObjNode**: entries are merged key-by-key; tombstones use max-dot-per-key.
399
+ * If both sides have a live entry for the same key, the entry nodes are merged recursively.
400
+ * Delete-wins: if a tombstone dot >= an entry dot, the entry is removed.
401
+ * - **RgaSeq**: elements from both sides are unioned by element ID.
402
+ * If both sides have the same element, tombstone wins (delete bias) and values are merged recursively.
403
+ * - **Kind mismatch**: the node with the higher "representative dot" wins and replaces the other entirely.
404
+ */
405
+ declare function mergeDoc(a: Doc, b: Doc, options?: MergeDocOptions): Doc;
406
+ /**
407
+ * Merge two CRDT states.
408
+ *
409
+ * The merged clock keeps a stable actor identity:
410
+ * - defaults to the actor from the first argument (`a`)
411
+ * - can be overridden via `options.actor`
412
+ * - optional `options.requireSharedOrigin` controls merge lineage checks
413
+ *
414
+ * The merged counter is lifted to the highest counter already observed for
415
+ * that actor across both input clocks and the merged document dots.
416
+ */
417
+ declare function mergeState(a: CrdtState, b: CrdtState, options?: MergeStateOptions): CrdtState;
418
+ //#endregion
419
+ export { PatchSemantics as $, createState as A, Dot as B, createClock as C, applyPatch as D, PatchError as E, ApplyResult as F, JsonPrimitive as G, IntentOp as H, Clock as I, MergeDocOptions as J, JsonValue as K, CrdtState as L, ActorId as M, ApplyPatchAsActorResult as N, applyPatchAsActor as O, ApplyPatchOptions as P, ObjNode as Q, DiffOptions as R, cloneClock as S, observeDot as T, JsonPatch as U, ElemId as V, JsonPatchOp as W, Node as X, MergeStateOptions as Y, ObjEntry as Z, crdtToJsonPatch as _, deserializeState as a, SerializedState as at, jsonPatchToCrdt as b, compileJsonPatchToIntent as c, jsonEquals as d, ROOT_KEY as et, parseJsonPointer as f, crdtToFullReplace as g, cloneDoc as h, deserializeDoc as i, SerializedNode as it, toJson as j, applyPatchInPlace as k, diffJsonPatch as l, applyIntentsToCrdt as m, mergeState as n, RgaSeq as nt, serializeDoc as o, VersionVector as ot, stringifyJsonPointer as p, LwwReg as q, materialize as r, SerializedDoc as rt, serializeState as s, mergeDoc as t, RgaElem as tt, getAtJson as u, docFromJson as v, nextDotForActor as w, jsonPatchToCrdtSafe as x, docFromJsonWithDot as y, Doc as z };