@ny4n/chord 0.0.0-stage → 1.0.1

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.
Files changed (124) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +261 -2
  3. package/dist/api.d.ts +20 -0
  4. package/dist/api.d.ts.map +1 -0
  5. package/dist/api.js +77 -0
  6. package/dist/api.js.map +1 -0
  7. package/dist/bundler.d.ts +6 -0
  8. package/dist/bundler.d.ts.map +1 -0
  9. package/dist/bundler.js +3 -0
  10. package/dist/bundler.js.map +1 -0
  11. package/dist/context/index.d.ts +24 -0
  12. package/dist/context/index.d.ts.map +1 -0
  13. package/dist/context/index.js +97 -0
  14. package/dist/context/index.js.map +1 -0
  15. package/dist/delta/apply-immutable-trusted.d.ts +4 -0
  16. package/dist/delta/apply-immutable-trusted.d.ts.map +1 -0
  17. package/dist/delta/apply-immutable-trusted.js +127 -0
  18. package/dist/delta/apply-immutable-trusted.js.map +1 -0
  19. package/dist/delta/core.d.ts +108 -0
  20. package/dist/delta/core.d.ts.map +1 -0
  21. package/dist/delta/core.js +645 -0
  22. package/dist/delta/core.js.map +1 -0
  23. package/dist/delta/diff.d.ts +5 -0
  24. package/dist/delta/diff.d.ts.map +1 -0
  25. package/dist/delta/diff.js +433 -0
  26. package/dist/delta/diff.js.map +1 -0
  27. package/dist/delta/draft.d.ts +5 -0
  28. package/dist/delta/draft.d.ts.map +1 -0
  29. package/dist/delta/draft.js +2 -0
  30. package/dist/delta/draft.js.map +1 -0
  31. package/dist/delta/index.d.ts +5 -0
  32. package/dist/delta/index.d.ts.map +1 -0
  33. package/dist/delta/index.js +6 -0
  34. package/dist/delta/index.js.map +1 -0
  35. package/dist/delta/revision-validator.d.ts +7 -0
  36. package/dist/delta/revision-validator.d.ts.map +1 -0
  37. package/dist/delta/revision-validator.js +71 -0
  38. package/dist/delta/revision-validator.js.map +1 -0
  39. package/dist/delta/tracker.d.ts +26 -0
  40. package/dist/delta/tracker.d.ts.map +1 -0
  41. package/dist/delta/tracker.js +2069 -0
  42. package/dist/delta/tracker.js.map +1 -0
  43. package/dist/facets/host.d.ts +12 -0
  44. package/dist/facets/host.d.ts.map +1 -0
  45. package/dist/facets/host.js +763 -0
  46. package/dist/facets/host.js.map +1 -0
  47. package/dist/facets/loader.d.ts +3 -0
  48. package/dist/facets/loader.d.ts.map +1 -0
  49. package/dist/facets/loader.js +5 -0
  50. package/dist/facets/loader.js.map +1 -0
  51. package/dist/index.d.ts +12 -0
  52. package/dist/index.d.ts.map +1 -0
  53. package/dist/index.js +10 -0
  54. package/dist/index.js.map +1 -0
  55. package/dist/json.d.ts +10 -0
  56. package/dist/json.d.ts.map +1 -0
  57. package/dist/json.js +124 -0
  58. package/dist/json.js.map +1 -0
  59. package/dist/node/bundle-loader.d.ts +30 -0
  60. package/dist/node/bundle-loader.d.ts.map +1 -0
  61. package/dist/node/bundle-loader.js +377 -0
  62. package/dist/node/bundle-loader.js.map +1 -0
  63. package/dist/node/bundle.d.ts +26 -0
  64. package/dist/node/bundle.d.ts.map +1 -0
  65. package/dist/node/bundle.js +183 -0
  66. package/dist/node/bundle.js.map +1 -0
  67. package/dist/node/manifest.d.ts +36 -0
  68. package/dist/node/manifest.d.ts.map +1 -0
  69. package/dist/node/manifest.js +6 -0
  70. package/dist/node/manifest.js.map +1 -0
  71. package/dist/node/package.d.ts +15 -0
  72. package/dist/node/package.d.ts.map +1 -0
  73. package/dist/node/package.js +199 -0
  74. package/dist/node/package.js.map +1 -0
  75. package/dist/node.d.ts +5 -0
  76. package/dist/node.d.ts.map +1 -0
  77. package/dist/node.js +3 -0
  78. package/dist/node.js.map +1 -0
  79. package/dist/services/consumer.d.ts +11 -0
  80. package/dist/services/consumer.d.ts.map +1 -0
  81. package/dist/services/consumer.js +585 -0
  82. package/dist/services/consumer.js.map +1 -0
  83. package/dist/services/errors.d.ts +8 -0
  84. package/dist/services/errors.d.ts.map +1 -0
  85. package/dist/services/errors.js +22 -0
  86. package/dist/services/errors.js.map +1 -0
  87. package/dist/services/handle.d.ts +15 -0
  88. package/dist/services/handle.d.ts.map +1 -0
  89. package/dist/services/handle.js +101 -0
  90. package/dist/services/handle.js.map +1 -0
  91. package/dist/services/instances.d.ts +26 -0
  92. package/dist/services/instances.d.ts.map +1 -0
  93. package/dist/services/instances.js +140 -0
  94. package/dist/services/instances.js.map +1 -0
  95. package/dist/services/loopback.d.ts +5 -0
  96. package/dist/services/loopback.d.ts.map +1 -0
  97. package/dist/services/loopback.js +15 -0
  98. package/dist/services/loopback.js.map +1 -0
  99. package/dist/services/provider.d.ts +37 -0
  100. package/dist/services/provider.d.ts.map +1 -0
  101. package/dist/services/provider.js +521 -0
  102. package/dist/services/provider.js.map +1 -0
  103. package/dist/services/state-codec.d.ts +15 -0
  104. package/dist/services/state-codec.d.ts.map +1 -0
  105. package/dist/services/state-codec.js +133 -0
  106. package/dist/services/state-codec.js.map +1 -0
  107. package/dist/services/state-internals.d.ts +13 -0
  108. package/dist/services/state-internals.d.ts.map +1 -0
  109. package/dist/services/state-internals.js +10 -0
  110. package/dist/services/state-internals.js.map +1 -0
  111. package/dist/services/state.d.ts +28 -0
  112. package/dist/services/state.d.ts.map +1 -0
  113. package/dist/services/state.js +397 -0
  114. package/dist/services/state.js.map +1 -0
  115. package/dist/services/wire.d.ts +63 -0
  116. package/dist/services/wire.d.ts.map +1 -0
  117. package/dist/services/wire.js +186 -0
  118. package/dist/services/wire.js.map +1 -0
  119. package/dist/types.d.ts +277 -0
  120. package/dist/types.d.ts.map +1 -0
  121. package/dist/types.js +2 -0
  122. package/dist/types.js.map +1 -0
  123. package/package.json +71 -5
  124. package/src/delta/README.md +191 -0
@@ -0,0 +1,645 @@
1
+ const isObj = (value) => value !== null && typeof value === "object";
2
+ // ─── Classification ──────────────────────────────────────────────────────────
3
+ export const isReplace = (op) => op[0] === "r";
4
+ /**
5
+ * A batch begins with a replacement. Flush guarantees `r` is at index 0 or absent,
6
+ * so this is exact rather than a heuristic.
7
+ */
8
+ export const isBase = (ops) => ops.length > 0 && ops[0][0] === "r";
9
+ // ─── Overlap ─────────────────────────────────────────────────────────────────
10
+ /**
11
+ * Longest suffix of `a` that is a prefix of `b`. Probes with indexOf and verifies
12
+ * exact substring equality, so the hot loops are native. A hand-written KMP is
13
+ * asymptotically equivalent and much slower in practice.
14
+ *
15
+ * Always correct: the returned n satisfies a.slice(a.length - n) === b.slice(0, n).
16
+ */
17
+ export function overlap(a, b, scan, probe = 64, maxCandidates = 8) {
18
+ if (a.length === 0 || b.length === 0 || scan === 0)
19
+ return 0;
20
+ const tail = a.length > scan ? a.slice(a.length - scan) : a;
21
+ // A probe of length h can only find overlaps of at least h — the head must
22
+ // actually occur in `a`. So try a long head first (few candidates, and it
23
+ // catches the large overlaps a rolling window produces), then fall back to one
24
+ // character, which finds any overlap at the cost of more candidates.
25
+ //
26
+ // Candidates are bounded because repetitive output — a build log, or any run of
27
+ // one character — makes a long head match at thousands of positions. Giving up
28
+ // returns 0, which emits a set: larger, never wrong.
29
+ for (const h of [Math.min(probe, b.length), 1]) {
30
+ const head = b.slice(0, h);
31
+ let tried = 0;
32
+ for (let k = tail.indexOf(head); k !== -1; k = tail.indexOf(head, k + 1)) {
33
+ if (++tried > maxCandidates)
34
+ break;
35
+ const n = tail.length - k;
36
+ if (n <= b.length && tail.slice(k) === b.slice(0, n))
37
+ return n;
38
+ }
39
+ if (h === 1)
40
+ break;
41
+ }
42
+ return 0;
43
+ }
44
+ // ─── Path safety ─────────────────────────────────────────────────────────────
45
+ /**
46
+ * Segments that reach the prototype chain.
47
+ *
48
+ * `JSON.parse` is safe on its own — it makes `__proto__` an own property. What is
49
+ * not safe is `parent[key] = value`, which is exactly what an applier does, and
50
+ * paths are data: `["s", ["__proto__", "isAdmin"], true]` pollutes
51
+ * `Object.prototype` for the whole process.
52
+ *
53
+ * Ops arrive from a facet, a plugin compartment, or a tool whose details may echo
54
+ * model output, so none of it is trusted input.
55
+ */
56
+ export const RESERVED_SEGMENTS = new Set(["__proto__", "constructor", "prototype"]);
57
+ export class UnsafePathError extends Error {
58
+ // Not a parameter property: Node's --experimental-strip-types rejects those,
59
+ // and these files are meant to run under it directly.
60
+ segment;
61
+ constructor(segment) {
62
+ super(`unsafe path segment: ${String(segment)}`);
63
+ this.segment = segment;
64
+ this.name = "UnsafePathError";
65
+ }
66
+ }
67
+ /**
68
+ * Verb, arity and payload shape for a **decoded** op: paths inline, no `#`, no
69
+ * short forms. `apply` uses this.
70
+ *
71
+ * Validating `Op` against the wire grammar would be laxer than the type: a
72
+ * two-element `["s", value]` would pass, and `apply` would then read the value as
73
+ * a path. Each vocabulary gets the validator that matches it.
74
+ */
75
+ export function assertValidOp(op) {
76
+ if (!Array.isArray(op) || op.length === 0)
77
+ throw new TypeError("op is not a tuple");
78
+ switch (op[0]) {
79
+ case "r":
80
+ if (op.length !== 2)
81
+ throw new TypeError("r arity");
82
+ return;
83
+ case "s":
84
+ if (op.length !== 3)
85
+ throw new TypeError("s arity");
86
+ assertPathArg(op[1], true);
87
+ return;
88
+ case "d":
89
+ if (op.length !== 2)
90
+ throw new TypeError("d arity");
91
+ assertPathArg(op[1], true);
92
+ return;
93
+ case "a":
94
+ if (op.length !== 3 || typeof op[2] !== "string")
95
+ throw new TypeError("a shape");
96
+ assertPathArg(op[1], true);
97
+ return;
98
+ case "t":
99
+ if (op.length !== 3 || !Number.isInteger(op[2]) || op[2] < 0)
100
+ throw new TypeError("t shape");
101
+ assertPathArg(op[1], true);
102
+ return;
103
+ case "p": {
104
+ if (op.length !== 5)
105
+ throw new TypeError("p arity");
106
+ assertPathArg(op[1]);
107
+ if (!Number.isInteger(op[2]) || op[2] < 0)
108
+ throw new TypeError("p index");
109
+ if (!Number.isInteger(op[3]) || op[3] < 0)
110
+ throw new TypeError("p remove");
111
+ if (!Array.isArray(op[4]))
112
+ throw new TypeError("p items");
113
+ return;
114
+ }
115
+ case "m":
116
+ if (op.length !== 3)
117
+ throw new TypeError("m arity");
118
+ assertPathArg(op[1]);
119
+ assertPermutation(op[2]);
120
+ return;
121
+ // Silently skipping an unknown verb is how a newer producer's op vanishes.
122
+ default:
123
+ throw new TypeError(`unknown op verb: ${String(op[0])}`);
124
+ }
125
+ }
126
+ function assertPathArg(p, nonEmpty = false) {
127
+ if (!Array.isArray(p))
128
+ throw new TypeError("path is not an array");
129
+ if (nonEmpty && p.length === 0)
130
+ throw new TypeError("path is empty");
131
+ assertSafePath(p);
132
+ }
133
+ function assertPermutation(value) {
134
+ if (!Array.isArray(value))
135
+ throw new TypeError("m permutation is not an array");
136
+ const seen = new Uint8Array(value.length);
137
+ for (const index of value) {
138
+ if (!Number.isInteger(index) || index < 0 || index >= value.length || seen[index] !== 0) {
139
+ throw new TypeError("m permutation is not a bijection");
140
+ }
141
+ seen[index] = 1;
142
+ }
143
+ }
144
+ /** The same, for the wire grammar: ids and short forms are legal here. */
145
+ export function assertValidWireOp(op) {
146
+ if (!Array.isArray(op) || op.length === 0)
147
+ throw new TypeError("op is not a tuple");
148
+ const [verb] = op;
149
+ const okRef = (r) => {
150
+ if (typeof r === "number") {
151
+ if (!Number.isInteger(r) || r < 0)
152
+ throw new TypeError("bad path id");
153
+ return;
154
+ }
155
+ // A string is not a path. Unchecked, `"a".slice(0, -1)` is `""`, so it
156
+ // resolves to the ROOT and writes there — a path that is not a path, accepted.
157
+ if (!Array.isArray(r))
158
+ throw new TypeError("path is not an array");
159
+ assertSafePath(r);
160
+ };
161
+ switch (verb) {
162
+ case "r":
163
+ if (op.length !== 2)
164
+ throw new TypeError("r arity");
165
+ return;
166
+ case "s":
167
+ if (op.length === 3)
168
+ okRef(op[1]);
169
+ else if (op.length !== 2)
170
+ throw new TypeError("s arity");
171
+ return;
172
+ case "d":
173
+ if (op.length === 2)
174
+ okRef(op[1]);
175
+ else if (op.length !== 1)
176
+ throw new TypeError("d arity");
177
+ return;
178
+ case "a":
179
+ if (op.length === 3) {
180
+ okRef(op[1]);
181
+ if (typeof op[2] !== "string")
182
+ throw new TypeError("a value");
183
+ }
184
+ else if (op.length === 2) {
185
+ if (typeof op[1] !== "string")
186
+ throw new TypeError("a value");
187
+ }
188
+ else
189
+ throw new TypeError("a arity");
190
+ return;
191
+ case "t":
192
+ if (op.length === 3) {
193
+ okRef(op[1]);
194
+ if (!Number.isInteger(op[2]) || op[2] < 0)
195
+ throw new TypeError("t count");
196
+ }
197
+ else if (op.length === 2) {
198
+ if (!Number.isInteger(op[1]) || op[1] < 0)
199
+ throw new TypeError("t count");
200
+ }
201
+ else
202
+ throw new TypeError("t arity");
203
+ return;
204
+ case "p": {
205
+ const [i, r, items] = op.length === 5 ? [op[2], op[3], op[4]] : op.length === 4 ? [op[1], op[2], op[3]] : [];
206
+ if (items === undefined)
207
+ throw new TypeError("p arity");
208
+ if (op.length === 5)
209
+ okRef(op[1]);
210
+ if (!Number.isInteger(i) || i < 0)
211
+ throw new TypeError("p index");
212
+ if (!Number.isInteger(r) || r < 0)
213
+ throw new TypeError("p remove");
214
+ if (!Array.isArray(items))
215
+ throw new TypeError("p items");
216
+ return;
217
+ }
218
+ case "m":
219
+ if (op.length === 3)
220
+ okRef(op[1]);
221
+ else if (op.length !== 2)
222
+ throw new TypeError("m arity");
223
+ assertPermutation(op[op.length - 1]);
224
+ return;
225
+ case "#": {
226
+ if (op.length !== 3 || !Number.isInteger(op[1]) || op[1] < 0 || !Array.isArray(op[2])) {
227
+ throw new TypeError("# shape");
228
+ }
229
+ assertSafePath(op[2]);
230
+ return;
231
+ }
232
+ // Silently skipping an unknown verb is how a newer producer's op vanishes.
233
+ default:
234
+ throw new TypeError(`unknown op verb: ${String(verb)}`);
235
+ }
236
+ }
237
+ export function assertSafePath(path) {
238
+ for (const seg of path) {
239
+ if (typeof seg === "string") {
240
+ if (RESERVED_SEGMENTS.has(seg))
241
+ throw new UnsafePathError(seg);
242
+ }
243
+ else if (!Number.isInteger(seg) || seg < 0) {
244
+ throw new UnsafePathError(seg);
245
+ }
246
+ }
247
+ }
248
+ /**
249
+ * An index may address an existing element or append exactly one past the end.
250
+ *
251
+ * This is not an arbitrary cap — it is what keeps the value a `JsonValue`. A
252
+ * sparse array does not survive a JSON round trip: holes serialise to `null` and
253
+ * return as real properties, so `arr[7] = x` on a length-3 array already produces
254
+ * state a replica cannot match. Rejecting the write is more honest than silently
255
+ * diverging.
256
+ *
257
+ * It also removes the denial of service it would otherwise permit:
258
+ * `["s", ["xs", 4294967290], 1]` allocates a 4.29-billion-entry array from one op.
259
+ * Growth stays possible and stays proportional — the tracker already emits
260
+ * `arr.length = n` as a splice of explicit nulls, whose op size grows with the
261
+ * gap, so a large growth costs a large op rather than a small one.
262
+ */
263
+ function assertIndexInRange(parent, index) {
264
+ if (index > parent.length)
265
+ throw new UnsafePathError(index);
266
+ }
267
+ // ─── Applier ─────────────────────────────────────────────────────────────────
268
+ export class PathError extends Error {
269
+ path;
270
+ constructor(path) {
271
+ super(`unresolvable path: ${JSON.stringify(path)}`);
272
+ this.path = path;
273
+ this.name = "PathError";
274
+ }
275
+ }
276
+ /**
277
+ * Apply ops to a plain mutable value. Returns the value, because `r` replaces it
278
+ * outright and cannot be done in place.
279
+ *
280
+ * Takes decoded ops. Path ids and omitted paths are a wire concern — run
281
+ * `decode` first if the ops came from a boundary.
282
+ */
283
+ export function apply(target, ops) {
284
+ return applyOps(target, ops);
285
+ }
286
+ function applyOps(target, ops) {
287
+ let root = target;
288
+ for (const op of ops) {
289
+ assertValidOp(op);
290
+ if (op[0] === "r") {
291
+ // Adopted, not copied. The consumer owns the batch it was handed.
292
+ //
293
+ // Fanning one batch out to several consumers in-process therefore makes
294
+ // their replicas alias each other. That is an ownership rule, not a
295
+ // defect: copy the batch at the fan-out point, or let each consumer
296
+ // decode its own. A batch that crosses a real boundary is already
297
+ // distinct, because serialisation produces fresh objects.
298
+ root = op[1];
299
+ continue;
300
+ }
301
+ const path = op[1];
302
+ assertSafePath(path);
303
+ if (op[0] === "p") {
304
+ const target_ = path.length === 0 ? root : resolve(root, path);
305
+ if (!Array.isArray(target_))
306
+ throw new PathError(path);
307
+ target_.splice(op[2], op[3]);
308
+ const chunkSize = 10_000;
309
+ for (let offset = 0; offset < op[4].length; offset += chunkSize) {
310
+ target_.splice(op[2] + offset, 0, ...op[4].slice(offset, offset + chunkSize));
311
+ }
312
+ continue;
313
+ }
314
+ if (op[0] === "m") {
315
+ const target_ = path.length === 0 ? root : resolve(root, path);
316
+ if (!Array.isArray(target_) || target_.length !== op[2].length)
317
+ throw new PathError(path);
318
+ const previous = target_.slice();
319
+ for (let index = 0; index < op[2].length; index++)
320
+ target_[index] = previous[op[2][index]];
321
+ continue;
322
+ }
323
+ // s/d/a/t can never target the root — the type forbids it.
324
+ const parent = resolve(root, path.slice(0, -1));
325
+ const key = path[path.length - 1];
326
+ if (Array.isArray(parent)) {
327
+ if (typeof key !== "number")
328
+ throw new UnsafePathError(key);
329
+ assertIndexInRange(parent, key);
330
+ }
331
+ // defineProperty rather than assignment: a setter inherited from the prototype
332
+ // chain would otherwise run on write.
333
+ const write = (value) => {
334
+ Object.defineProperty(parent, key, { value, writable: true, enumerable: true, configurable: true });
335
+ };
336
+ const read = () => (Object.hasOwn(parent, key) ? parent[key] : undefined);
337
+ switch (op[0]) {
338
+ case "s":
339
+ write(op[2]);
340
+ break;
341
+ case "d":
342
+ if (Array.isArray(parent)) {
343
+ if (typeof key !== "number" || key >= parent.length)
344
+ throw new PathError(path);
345
+ parent.splice(key, 1);
346
+ }
347
+ else
348
+ delete parent[key];
349
+ break;
350
+ case "a": {
351
+ const current = read();
352
+ if (typeof current !== "string")
353
+ throw new PathError(path);
354
+ write(`${current}${op[2]}`);
355
+ break;
356
+ }
357
+ case "t": {
358
+ const current = read();
359
+ if (typeof current !== "string")
360
+ throw new PathError(path);
361
+ write(current.slice(op[2]));
362
+ break;
363
+ }
364
+ }
365
+ }
366
+ return root;
367
+ }
368
+ /** Apply one decoded operation batch without mutating the previous immutable value. */
369
+ export function applyImmutable(target, ops) {
370
+ return applyImmutableBatches(target, [ops]);
371
+ }
372
+ /**
373
+ * Apply decoded operation batches as one final-result-only replay.
374
+ *
375
+ * Containers copied for an earlier batch may be mutated while applying a later
376
+ * batch, so this deliberately exposes no intermediate revisions.
377
+ */
378
+ export function applyImmutableBatches(target, batches) {
379
+ let root = target;
380
+ const owned = new WeakSet();
381
+ for (const ops of batches) {
382
+ for (const op of ops) {
383
+ assertValidOp(op);
384
+ if (op[0] === "r") {
385
+ root = op[1];
386
+ continue;
387
+ }
388
+ root = copyContainers(root, op[0] === "p" || op[0] === "m" ? op[1] : op[1].slice(0, -1), owned);
389
+ root = applyOps(root, [op]);
390
+ }
391
+ }
392
+ return root;
393
+ }
394
+ function copyContainers(root, path, owned) {
395
+ const copy = (value) => {
396
+ if (Array.isArray(value))
397
+ return value.slice();
398
+ if (!isObj(value))
399
+ throw new PathError(path);
400
+ const result = Object.create(Object.getPrototypeOf(value) === null ? null : Object.prototype);
401
+ for (const key of Object.keys(value)) {
402
+ Object.defineProperty(result, key, {
403
+ value: value[key],
404
+ writable: true,
405
+ enumerable: true,
406
+ configurable: true,
407
+ });
408
+ }
409
+ return result;
410
+ };
411
+ if (!isObj(root))
412
+ throw new PathError(path);
413
+ let copiedRoot;
414
+ if (owned.has(root))
415
+ copiedRoot = root;
416
+ else {
417
+ copiedRoot = copy(root);
418
+ owned.add(copiedRoot);
419
+ }
420
+ let destination = copiedRoot;
421
+ for (const segment of path) {
422
+ if (!Object.hasOwn(destination, segment))
423
+ throw new PathError(path);
424
+ if (Array.isArray(destination) && typeof segment !== "number")
425
+ throw new UnsafePathError(segment);
426
+ const child = destination[segment];
427
+ if (!isObj(child))
428
+ throw new PathError(path);
429
+ if (owned.has(child)) {
430
+ destination = child;
431
+ continue;
432
+ }
433
+ const copiedChild = copy(child);
434
+ Object.defineProperty(destination, segment, {
435
+ value: copiedChild,
436
+ writable: true,
437
+ enumerable: true,
438
+ configurable: true,
439
+ });
440
+ owned.add(copiedChild);
441
+ destination = copiedChild;
442
+ }
443
+ return copiedRoot;
444
+ }
445
+ function resolveValue(root, path) {
446
+ let node = root;
447
+ for (const seg of path) {
448
+ if (!isObj(node))
449
+ throw new PathError(path);
450
+ if (Array.isArray(node) && typeof seg !== "number")
451
+ throw new UnsafePathError(seg);
452
+ // Own properties only: an inherited getter must not run, and a walk must not
453
+ // escape the value into the prototype chain.
454
+ if (!Object.hasOwn(node, seg))
455
+ throw new PathError(path);
456
+ node = node[seg];
457
+ }
458
+ return node;
459
+ }
460
+ function resolve(root, path) {
461
+ const node = resolveValue(root, path);
462
+ if (!isObj(node))
463
+ throw new PathError(path);
464
+ return node;
465
+ }
466
+ // ─── Codec ───────────────────────────────────────────────────────────────────
467
+ //
468
+ // Path interning and arity omission live between the tracker and a boundary;
469
+ // `Op` and `apply` know nothing about them.
470
+ //
471
+ // ONE PAIR PER INDEPENDENT STATE STREAM. Every decoder must observe exactly the
472
+ // batches encoded by its matching encoder, beginning with that state's base.
473
+ // Sharing a transport connection does not make separately hydrated states one
474
+ // stream.
475
+ const pathKey = (path) => JSON.stringify(path);
476
+ /**
477
+ * Intern on SECOND use. A definition costs more than the path it replaces, so
478
+ * interning on first use loses on the many paths written exactly once.
479
+ */
480
+ export function encoder() {
481
+ const seen = new Set();
482
+ const ids = new Map();
483
+ let nextId = 0;
484
+ let previous; // last path in THIS batch
485
+ return {
486
+ encode(ops) {
487
+ // Arity omission is scoped to a batch. Letting it span batches would make
488
+ // a batch's first op depend on the previous batch's last one, so a reader
489
+ // that skips or reorders a batch decodes into the wrong path. Ids are the
490
+ // only cross-batch state, and the dictionary makes those explicit.
491
+ previous = undefined;
492
+ const out = [];
493
+ for (const op of ops) {
494
+ if (op[0] === "r") {
495
+ out.push(op);
496
+ // A base batch is a RECOVERY POINT: a reader replays from the last one
497
+ // with a fresh decoder. So everything after it must be self-contained.
498
+ // Keeping ids across a replacement emits references to definitions the
499
+ // reader never saw — recovery fails with an unresolvable path id.
500
+ seen.clear();
501
+ ids.clear();
502
+ nextId = 0;
503
+ previous = undefined;
504
+ continue;
505
+ }
506
+ const path = op[1];
507
+ const key = pathKey(path);
508
+ // Same path as the previous op: drop the ref entirely.
509
+ if (key === previous) {
510
+ switch (op[0]) {
511
+ case "s":
512
+ out.push(["s", op[2]]);
513
+ break;
514
+ case "d":
515
+ out.push(["d"]);
516
+ break;
517
+ case "a":
518
+ out.push(["a", op[2]]);
519
+ break;
520
+ case "t":
521
+ out.push(["t", op[2]]);
522
+ break;
523
+ case "p":
524
+ out.push(["p", op[2], op[3], op[4]]);
525
+ break;
526
+ case "m":
527
+ out.push(["m", op[2]]);
528
+ break;
529
+ }
530
+ continue;
531
+ }
532
+ let ref = path;
533
+ const existing = ids.get(key);
534
+ if (existing !== undefined) {
535
+ ref = existing;
536
+ }
537
+ else if (seen.has(key)) {
538
+ const id = nextId++;
539
+ ids.set(key, id);
540
+ out.push(["#", id, path]); // second use: define, then reference
541
+ ref = id;
542
+ }
543
+ else {
544
+ seen.add(key); // first use: inline
545
+ }
546
+ switch (op[0]) {
547
+ case "s":
548
+ out.push(["s", ref, op[2]]);
549
+ break;
550
+ case "d":
551
+ out.push(["d", ref]);
552
+ break;
553
+ case "a":
554
+ out.push(["a", ref, op[2]]);
555
+ break;
556
+ case "t":
557
+ out.push(["t", ref, op[2]]);
558
+ break;
559
+ case "p":
560
+ out.push(["p", ref, op[2], op[3], op[4]]);
561
+ break;
562
+ case "m":
563
+ out.push(["m", ref, op[2]]);
564
+ break;
565
+ }
566
+ previous = key;
567
+ }
568
+ return out;
569
+ },
570
+ };
571
+ }
572
+ export function decoder() {
573
+ const paths = new Map();
574
+ return {
575
+ decode(wire) {
576
+ let previous; // scoped to the batch, as in encode
577
+ const out = [];
578
+ for (const op of wire) {
579
+ assertValidWireOp(op);
580
+ if (op[0] === "#") {
581
+ assertSafePath(op[2]);
582
+ paths.set(op[1], op[2]);
583
+ continue;
584
+ }
585
+ if (op[0] === "r") {
586
+ out.push(op);
587
+ paths.clear();
588
+ previous = undefined;
589
+ continue;
590
+ }
591
+ // Arity tells us whether a ref is present: the short forms omit it.
592
+ const short = (op[0] === "d" && op.length === 1) ||
593
+ (op[0] !== "d" && op[0] !== "p" && op.length === 2) ||
594
+ (op[0] === "p" && op.length === 4);
595
+ let path;
596
+ if (short) {
597
+ if (previous === undefined)
598
+ throw new PathError([]);
599
+ path = previous;
600
+ }
601
+ else {
602
+ const ref = op[1];
603
+ if (typeof ref === "number") {
604
+ const resolved = paths.get(ref);
605
+ if (resolved === undefined)
606
+ throw new PathError(ref);
607
+ path = resolved;
608
+ }
609
+ else {
610
+ path = ref;
611
+ }
612
+ previous = path;
613
+ }
614
+ if (op[0] !== "p" && op[0] !== "m" && path.length === 0)
615
+ throw new PathError(path);
616
+ switch (op[0]) {
617
+ case "s":
618
+ out.push(["s", path, (short ? op[1] : op[2])]);
619
+ break;
620
+ case "d":
621
+ out.push(["d", path]);
622
+ break;
623
+ case "a":
624
+ out.push(["a", path, (short ? op[1] : op[2])]);
625
+ break;
626
+ case "t":
627
+ out.push(["t", path, (short ? op[1] : op[2])]);
628
+ break;
629
+ case "p": {
630
+ const [i, r, items] = short
631
+ ? [op[1], op[2], op[3]]
632
+ : [op[2], op[3], op[4]];
633
+ out.push(["p", path, i, r, items]);
634
+ break;
635
+ }
636
+ case "m":
637
+ out.push(["m", path, (short ? op[1] : op[2])]);
638
+ break;
639
+ }
640
+ }
641
+ return out;
642
+ },
643
+ };
644
+ }
645
+ //# sourceMappingURL=core.js.map