doxum 0.1.7 → 0.1.9

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 (70) hide show
  1. package/README.md +217 -296
  2. package/dist/contract-BNStLbSE.d.ts +441 -0
  3. package/dist/contract-CIU5FCC1.d.cts +441 -0
  4. package/dist/driver-BlR81Dqg.js +200 -0
  5. package/dist/driver-BlR81Dqg.js.map +1 -0
  6. package/dist/driver-xOIkwrB8.cjs +241 -0
  7. package/dist/driver-xOIkwrB8.cjs.map +1 -0
  8. package/dist/index.cjs +666 -2222
  9. package/dist/index.cjs.map +1 -1
  10. package/dist/index.d.cts +10 -20
  11. package/dist/index.d.ts +10 -18
  12. package/dist/index.js +634 -2189
  13. package/dist/index.js.map +1 -1
  14. package/dist/{integration-CZwCwFBS.cjs → integration-C87tjRop.cjs} +4 -9
  15. package/dist/integration-C87tjRop.cjs.map +1 -0
  16. package/dist/{integration-B56u1l9V.js → integration-D5XCBLJ8.js} +2 -7
  17. package/dist/integration-D5XCBLJ8.js.map +1 -0
  18. package/dist/integration.cjs +11 -11
  19. package/dist/integration.d.cts +10 -4
  20. package/dist/integration.d.ts +10 -4
  21. package/dist/integration.js +4 -3
  22. package/dist/issue-DVaGQGeP.js +576 -0
  23. package/dist/issue-DVaGQGeP.js.map +1 -0
  24. package/dist/issue-DhrNdQNg.cjs +797 -0
  25. package/dist/issue-DhrNdQNg.cjs.map +1 -0
  26. package/dist/local-sync.cjs +67 -39
  27. package/dist/local-sync.cjs.map +1 -1
  28. package/dist/local-sync.d.cts +10 -10
  29. package/dist/local-sync.d.ts +10 -10
  30. package/dist/local-sync.js +66 -38
  31. package/dist/local-sync.js.map +1 -1
  32. package/dist/react.cjs +23 -4
  33. package/dist/react.cjs.map +1 -1
  34. package/dist/react.d.cts +8 -3
  35. package/dist/react.d.ts +8 -3
  36. package/dist/react.js +21 -6
  37. package/dist/react.js.map +1 -1
  38. package/dist/store-1Uob0Ghk.cjs +2750 -0
  39. package/dist/store-1Uob0Ghk.cjs.map +1 -0
  40. package/dist/store-CD0KdGsq.d.cts +262 -0
  41. package/dist/store-D7QH6Rzw.js +2511 -0
  42. package/dist/store-D7QH6Rzw.js.map +1 -0
  43. package/dist/store-cp5CpfCy.d.ts +262 -0
  44. package/package.json +1 -1
  45. package/skills/doxum-runtime/SKILL.md +26 -44
  46. package/skills/doxum-runtime/references/guide.en.md +95 -359
  47. package/skills/doxum-runtime/references/guide.zh-CN.md +80 -285
  48. package/skills/doxum-runtime/references/invariants.en.md +42 -165
  49. package/skills/doxum-runtime/references/invariants.zh-CN.md +34 -97
  50. package/skills/doxum-runtime/references/patterns.en.md +73 -225
  51. package/skills/doxum-runtime/references/patterns.zh-CN.md +73 -179
  52. package/dist/chunk-pbuEa-1d.js +0 -13
  53. package/dist/contract-DNZ4D53r.d.ts +0 -563
  54. package/dist/contract-j3SLGAwh.d.cts +0 -563
  55. package/dist/driver-CNxqMVFH.cjs +0 -69
  56. package/dist/driver-CNxqMVFH.cjs.map +0 -1
  57. package/dist/driver-CbzfW5MR.js +0 -46
  58. package/dist/driver-CbzfW5MR.js.map +0 -1
  59. package/dist/integration-B56u1l9V.js.map +0 -1
  60. package/dist/integration-CZwCwFBS.cjs.map +0 -1
  61. package/dist/ownership-CY0nPXGF.cjs +0 -304
  62. package/dist/ownership-CY0nPXGF.cjs.map +0 -1
  63. package/dist/ownership-CduRygE7.js +0 -245
  64. package/dist/ownership-CduRygE7.js.map +0 -1
  65. package/dist/runtime-0mOFbe_H.cjs +0 -2439
  66. package/dist/runtime-0mOFbe_H.cjs.map +0 -1
  67. package/dist/runtime-B22tuj9A.d.cts +0 -150
  68. package/dist/runtime-BFmhzPpZ.d.ts +0 -150
  69. package/dist/runtime-DF1q9Gje.js +0 -2129
  70. package/dist/runtime-DF1q9Gje.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,91 +1,6 @@
1
- import { A as id, B as set$1, C as restore, D as DocumentReentrancyError, E as DocumentDisposedError, F as locate, G as object, H as field, I as overlaps, J as table, K as optional, L as read, N as contains$1, O as address$1, P as debugKey, R as resolveAddress, S as keys, T as validPositions, U as list, V as dict, W as map, X as variant, Y as tree, a as bindDocumentReadable, b as at$1, c as notify, d as subscribeTargets, f as accessOf, h as commandFootprint, i as ProjectionError, j as impact_target_exports, k as belongs, l as shareNotification, m as readWith, o as createNotification, p as bindRuntimeAccess, q as schema, r as ProjectionDisposedError, s as disposeNotification, t as createProjectionRuntime, u as subscribeRoot, v as documentReader, w as valid, x as index, y as afterRemove, z as resolveLocated } from "./runtime-DF1q9Gje.js";
2
- import { a as isStablePayload, c as snapshotPayload, i as isRecord, l as transferPayload, n as deepEqual, o as ownPayload, r as isPlainObject, s as sameStructuralValue, t as cloneValue, u as profile } from "./ownership-CduRygE7.js";
3
- import { n as bindRuntimeDriver, r as disposeRuntimeDriver, t as assertRuntimeWritable } from "./driver-CbzfW5MR.js";
4
- //#region core/src/impact.ts
5
- const emptyCollectionImpact = Object.freeze({
6
- kind: "incremental",
7
- added: /* @__PURE__ */ new Set(),
8
- removed: /* @__PURE__ */ new Set(),
9
- updated: /* @__PURE__ */ new Set(),
10
- orderChanged: false
11
- });
12
- const resetCollectionImpact = Object.freeze({ kind: "reset" });
13
- const createImpact = (input) => {
14
- const kind = input.reset ? "reset" : "incremental";
15
- const paths = input.paths ?? input.operations.map((operation) => operation.at);
16
- let pathBuckets;
17
- const somePath = (address, test) => {
18
- if (address.length === 0 || paths.length <= 8) return paths.some(test);
19
- if (!pathBuckets) {
20
- pathBuckets = /* @__PURE__ */ new Map();
21
- for (const changed of paths) {
22
- const first = changed[0];
23
- const bucket = pathBuckets.get(first) ?? [];
24
- bucket.push(changed);
25
- pathBuckets.set(first, bucket);
26
- }
27
- }
28
- if (pathBuckets.get(void 0)?.some(test)) return true;
29
- return pathBuckets.get(address[0])?.some(test) ?? false;
30
- };
31
- const pathsAffect = (address) => address.length === 0 ? paths.length > 0 : somePath(address, (changed) => overlaps(changed, address));
32
- let collectionCache;
33
- const findCollection = (address) => input.collections?.find((entry) => entry.address.length === address.length && entry.address.every((segment, index) => segment === address[index]));
34
- return {
35
- kind,
36
- operations: input.operations,
37
- affects: (value) => {
38
- profile.impact.affects();
39
- if (kind === "reset") return true;
40
- if (!belongs(value, input.schema)) return false;
41
- if (pathsAffect(address$1(value))) return true;
42
- if (value.kind === "collection") {
43
- const collection = findCollection(address$1(value));
44
- if (!collection) return false;
45
- const id$1 = id(value);
46
- if (id$1 !== void 0) return collection.added.has(id$1) || collection.removed.has(id$1) || collection.updated.has(id$1);
47
- return collection.added.size > 0 || collection.removed.size > 0 || collection.updated.size > 0 || collection.orderChanged;
48
- }
49
- return false;
50
- },
51
- collection: (selector) => {
52
- if (selector.schema !== input.schema) throw new Error("Collection selector belongs to another schema.");
53
- if (kind === "reset") return resetCollectionImpact;
54
- const cached = collectionCache?.find((entry) => entry.address.length === selector.address.length && entry.address.every((segment, index) => segment === selector.address[index]))?.value;
55
- if (cached) return cached;
56
- const exact = findCollection(selector.address);
57
- if (somePath(selector.address, (changed) => contains$1(changed, selector.address) && changed.length < selector.address.length)) {
58
- const reset = { kind: "reset" };
59
- (collectionCache ??= []).push({
60
- address: selector.address,
61
- value: reset
62
- });
63
- return reset;
64
- }
65
- if (!exact) {
66
- const empty = emptyCollectionImpact;
67
- (collectionCache ??= []).push({
68
- address: selector.address,
69
- value: empty
70
- });
71
- return empty;
72
- }
73
- const incremental = {
74
- kind: "incremental",
75
- added: exact.added,
76
- removed: exact.removed,
77
- updated: exact.updated,
78
- orderChanged: exact.orderChanged
79
- };
80
- (collectionCache ??= []).push({
81
- address: selector.address,
82
- value: incremental
83
- });
84
- return incremental;
85
- }
86
- };
87
- };
88
- //#endregion
1
+ import { A as insert, D as equal, F as remove, L as profile, M as keys, N as matches, _ as resolveTreeContainer, a as compiledShape, c as debugKey, d as overlaps, f as read, g as resolveLocated, h as resolveContainer, i as AddressIndex, j as install, l as memberKey, n as fail, o as contains, p as resolveAddress, r as invalidValue, s as createAddressResolver, t as MutationRejected, v as resolveValue } from "./issue-DVaGQGeP.js";
2
+ import { A as object, B as parse, C as assign, D as field, E as compilePath, F as ParseError, I as checkKey, L as checkValue, M as table, N as tree, O as list, P as variant, R as copyValue, T as snapshot, _ as readWith, a as input, b as TransactionRejected, c as createNotification, d as shareNotification, g as bindRuntimeAccess, h as accessOf, i as ProjectionError, j as optional, k as map, l as disposeNotification, m as subscribeTargets, o as project, p as subscribeRoot, r as ProjectionDisposedError, s as bindDocumentReadable, t as createProjectionStore, u as notify, v as DocumentDisposedError, w as createAccess, x as createImpact, y as DocumentReentrancyError, z as equalValue } from "./store-D7QH6Rzw.js";
3
+ import { n as bindRuntimeDriver, o as decodeChanges, r as disposeRuntimeDriver, s as sealChanges, t as assertRuntimeWritable } from "./driver-BlR81Dqg.js";
89
4
  //#region core/src/history.ts
90
5
  const createHistory = (input) => {
91
6
  let undos = [];
@@ -149,8 +64,8 @@ const createHistory = (input) => {
149
64
  } else to.push(entry);
150
65
  let committed = false;
151
66
  try {
152
- const operations = direction === "redo" ? entry.batches.flatMap((batch) => batch.operations) : entry.batches.slice().reverse().flatMap((batch) => batch.inverse);
153
- const result = input.apply(operations);
67
+ const changes = direction === "redo" ? entry.batches : entry.batches.slice().reverse();
68
+ const result = input.apply(changes, direction === "redo" ? "forward" : "backward");
154
69
  committed = result.status !== "rejected";
155
70
  if (committed && direction === "cancel") group = void 0;
156
71
  if (result.status === "unchanged") {
@@ -224,12 +139,8 @@ const createHistory = (input) => {
224
139
  });
225
140
  }
226
141
  },
227
- record: (operations, inverse) => {
142
+ record: (batch) => {
228
143
  if (input.capacity <= 0) return;
229
- const batch = {
230
- operations,
231
- inverse
232
- };
233
144
  if (group?.entry) group.entry.batches.push(batch);
234
145
  else {
235
146
  const entry = { batches: [batch] };
@@ -255,1990 +166,609 @@ const createHistory = (input) => {
255
166
  };
256
167
  };
257
168
  //#endregion
258
- //#region core/src/access/writer.ts
259
- const schemaRoot = (schema) => object(schema.shape);
260
- const emit = (sink, operation) => sink(operation);
261
- const writerFor = (node, inputAddress, sink) => {
262
- const address = Object.freeze(inputAddress);
263
- if (node.kind === "field") return {
264
- set: (value) => emit(sink, {
265
- type: "field.set",
266
- at: address,
267
- value
268
- }),
269
- clear: node.optional ? () => emit(sink, {
270
- type: "field.clear",
271
- at: address
272
- }) : void 0
273
- };
274
- if (node.kind === "dict") return {
275
- set: (key, value) => emit(sink, {
276
- type: "dict.set",
277
- at: address,
278
- key,
279
- value
280
- }),
281
- delete: (key) => emit(sink, {
282
- type: "dict.delete",
283
- at: address,
284
- key
285
- }),
286
- replace: (value) => emit(sink, {
287
- type: "dict.replace",
288
- at: address,
289
- value
290
- }),
291
- ...node.optional ? { clear: () => emit(sink, {
292
- type: "value.clear",
293
- at: address
294
- }) } : {}
295
- };
296
- if (node.kind === "list") return {
297
- insert: (value, anchor) => emit(sink, {
298
- type: "list.insert",
299
- at: address,
300
- key: node.keyOf(value),
301
- value,
302
- anchor
303
- }),
304
- move: (key, anchor) => emit(sink, {
305
- type: "list.move",
306
- at: address,
307
- key,
308
- anchor
309
- }),
310
- remove: (key) => emit(sink, {
311
- type: "list.remove",
312
- at: address,
313
- key
314
- }),
315
- replace: (value) => emit(sink, {
316
- type: "list.replace",
317
- at: address,
318
- value,
319
- keys: value.map(node.keyOf)
320
- }),
321
- ...node.optional ? { clear: () => emit(sink, {
322
- type: "value.clear",
323
- at: address
324
- }) } : {}
325
- };
326
- if (node.kind === "tree") return {
327
- insert: (treeNodeId, value, position) => emit(sink, {
328
- type: "tree.insert",
329
- at: address,
330
- treeNodeId,
331
- value,
332
- parentId: position?.parentId,
333
- index: position?.index
334
- }),
335
- move: (treeNodeId, position) => emit(sink, {
336
- type: "tree.move",
337
- at: address,
338
- treeNodeId,
339
- parentId: position?.parentId,
340
- index: position?.index
341
- }),
342
- remove: (treeNodeId) => emit(sink, {
343
- type: "tree.remove",
344
- at: address,
345
- treeNodeId
346
- }),
347
- set: (treeNodeId, value) => emit(sink, {
348
- type: "tree.set",
349
- at: address,
350
- treeNodeId,
351
- value
352
- }),
353
- replace: (value) => emit(sink, {
354
- type: "tree.replace",
355
- at: address,
356
- value
357
- }),
358
- ...node.optional ? { clear: () => emit(sink, {
359
- type: "value.clear",
360
- at: address
361
- }) } : {}
362
- };
363
- if (node.kind === "table") {
364
- let items;
365
- return {
366
- create: (input, anchor) => {
367
- emit(sink, {
368
- type: "entity.create",
369
- at: address,
370
- entries: (Array.isArray(input) ? input : [input]).map(({ id, value }) => ({
371
- id,
372
- value
373
- })),
374
- anchor
375
- });
376
- },
377
- item: (id) => {
378
- const cached = items?.get(id);
379
- if (cached) return cached;
380
- const writer = writerFor(node.value, [...address, id], sink);
381
- (items ??= /* @__PURE__ */ new Map()).set(id, writer);
382
- return writer;
383
- },
384
- remove: (input) => emit(sink, {
385
- type: "entity.remove",
386
- at: address,
387
- ids: Array.isArray(input) ? [...input] : [input]
388
- }),
389
- move: (id, anchor) => emit(sink, {
390
- type: "entity.move",
391
- at: address,
392
- id,
393
- anchor
394
- })
395
- };
396
- }
397
- if (node.kind === "map") {
398
- let items;
399
- return {
400
- create: (input) => {
401
- emit(sink, {
402
- type: "entity.create",
403
- at: address,
404
- entries: (Array.isArray(input) ? input : [input]).map(({ id, value }) => ({
405
- id,
406
- value
407
- }))
408
- });
409
- },
410
- item: (id) => {
411
- const cached = items?.get(id);
412
- if (cached) return cached;
413
- const writer = writerFor(node.value, [...address, id], sink);
414
- (items ??= /* @__PURE__ */ new Map()).set(id, writer);
415
- return writer;
416
- },
417
- remove: (input) => emit(sink, {
418
- type: "entity.remove",
419
- at: address,
420
- ids: Array.isArray(input) ? [...input] : [input]
421
- })
422
- };
423
- }
424
- if (node.kind === "variant") return {
425
- replace: (value) => emit(sink, {
426
- type: "variant.replace",
427
- at: address,
428
- value
429
- }),
430
- ...node.optional ? { clear: () => emit(sink, {
431
- type: "value.clear",
432
- at: address
433
- }) } : {}
434
- };
435
- let children;
436
- return new Proxy({}, { get: (_target, property) => {
437
- if (typeof property !== "string" || !node.shape[property]) return void 0;
438
- const cached = children?.get(property);
439
- if (cached) return cached;
440
- const writer = writerFor(node.shape[property], [...address, property], sink);
441
- (children ??= /* @__PURE__ */ new Map()).set(property, writer);
442
- return writer;
443
- } });
444
- };
445
- const documentWriter = (schema, sink) => writerFor(schemaRoot(schema), [], sink);
446
- //#endregion
447
- //#region core/src/mutation/issue.ts
448
- const ownAddress$1 = (address) => Object.freeze(address.slice());
449
- const at = (address, code, message, operation) => Object.freeze({
450
- source: "mutation",
451
- code,
452
- address: ownAddress$1(address),
453
- message,
454
- ...operation === void 0 ? {} : { operation }
455
- });
456
- const from = (operation, code, message) => at(operation.at, code, message, operation.type);
457
- const input = (code, message) => at([], code, message);
458
- //#endregion
459
- //#region core/src/mutation/execute-entity.ts
460
- const resolveEntityCollection = (node, value) => {
461
- if (node.kind === "table") {
462
- if (!isRecord(value) || !Array.isArray(value.ids) || !isRecord(value.byId)) return { status: "invalid" };
463
- return {
464
- status: "resolved",
465
- collection: {
466
- kind: "table",
467
- ids: value.ids,
468
- byId: value.byId
469
- }
470
- };
471
- }
472
- return isRecord(value) ? {
473
- status: "resolved",
474
- collection: {
475
- kind: "map",
476
- byId: value
477
- }
478
- } : { status: "invalid" };
169
+ //#region core/src/mutation/state.ts
170
+ /** Install validated values, or captured before values during rollback. */
171
+ const installMember = (parent, key, present, value) => {
172
+ if (Array.isArray(parent)) {
173
+ const index = Number(key);
174
+ if (present) if (index < 0) insert(parent, parent.length, value);
175
+ else parent[index] = value;
176
+ else if (index >= 0) remove(parent, index);
177
+ } else if (!present) delete parent[key];
178
+ else if (Object.hasOwn(parent, key)) parent[key] = value;
179
+ else Object.defineProperty(parent, key, {
180
+ value,
181
+ writable: true,
182
+ configurable: true,
183
+ enumerable: true
184
+ });
479
185
  };
480
- const rejected$4 = (operation, code, message) => ({
481
- status: "rejected",
482
- issue: from(operation, code, message)
483
- });
484
- const executeEntity = (target, operation, copyPayload) => {
485
- if (target.node.kind !== "table" && target.node.kind !== "map") return rejected$4(operation, "invalid-address", "Collection target is invalid.");
486
- const resolution = resolveEntityCollection(target.node, target.value);
487
- if (resolution.status === "invalid") return rejected$4(operation, "invalid-address", "Collection target is invalid.");
488
- const collection = resolution.collection;
489
- const ids = collection.kind === "table" ? collection.ids : void 0;
490
- if (operation.type === "entity.move" && collection.kind === "map") return rejected$4(operation, "invalid-operation", "Map entries are unordered.");
491
- const byId = collection.byId;
492
- if (operation.type === "entity.create") {
493
- if (operation.entries.length === 0) return { status: "unchanged" };
494
- const seen = /* @__PURE__ */ new Set();
495
- for (const entry of operation.entries) {
496
- if (seen.has(entry.id)) return rejected$4(operation, "duplicate-entity", "Entity ids must be unique within a batch.");
497
- seen.add(entry.id);
498
- if (Object.prototype.hasOwnProperty.call(byId, entry.id)) return rejected$4(operation, "duplicate-entity", `Entity '${entry.id}' already exists.`);
499
- }
500
- if (collection.kind === "map" && (operation.anchor || operation.positions)) return rejected$4(operation, "invalid-anchor", "Map entries do not support ordering metadata.");
501
- if (ids && operation.positions) {
502
- if (operation.positions.length !== operation.entries.length || !validPositions(ids.length, operation.positions)) return rejected$4(operation, "invalid-operation", "Entity restore positions are invalid.");
503
- } else if (ids && !valid(ids, operation.anchor)) return rejected$4(operation, "invalid-anchor", "Collection anchor is invalid.");
504
- for (const entry of operation.entries) byId[entry.id] = copyPayload ? cloneValue(entry.value, "canonical") : transferPayload(entry.value);
505
- if (ids) if (operation.positions) restore(ids, operation.entries.map((entry) => entry.id), operation.positions);
506
- else if (!operation.anchor || "at" in operation.anchor && operation.anchor.at === "end") for (const entry of operation.entries) ids.push(entry.id);
507
- else {
508
- const insertion = index(ids, operation.anchor);
509
- const currentLength = ids.length;
510
- ids.length = currentLength + operation.entries.length;
511
- for (let index = currentLength - 1; index >= insertion; index -= 1) ids[index + operation.entries.length] = ids[index];
512
- for (let index = 0; index < operation.entries.length; index += 1) ids[insertion + index] = operation.entries[index].id;
513
- }
514
- return {
515
- status: "changed",
516
- inverse: [{
517
- type: "entity.remove",
518
- at: operation.at,
519
- ids: operation.entries.map((entry) => entry.id)
520
- }]
521
- };
522
- }
523
- if (operation.type === "entity.remove") {
524
- if (operation.ids.length === 0) return { status: "unchanged" };
525
- const seen = /* @__PURE__ */ new Set();
526
- for (const id of operation.ids) {
527
- if (seen.has(id)) return rejected$4(operation, "duplicate-entity", "Entity ids must be unique within a batch.");
528
- seen.add(id);
529
- if (!Object.prototype.hasOwnProperty.call(byId, id)) return rejected$4(operation, "missing-entity", `Entity '${id}' does not exist.`);
530
- }
531
- const entries = [];
532
- const positions = [];
533
- if (ids) {
534
- const positionsById = /* @__PURE__ */ new Map();
535
- for (let index = 0; index < ids.length; index += 1) {
536
- const id = ids[index];
537
- if (seen.has(id)) positionsById.set(id, index);
538
- }
539
- for (const id of operation.ids) if (!positionsById.has(id)) return rejected$4(operation, "invalid-collection", `Collection index is missing '${id}'.`);
540
- let writeIndex = 0;
541
- for (let index = 0; index < ids.length; index += 1) {
542
- const id = ids[index];
543
- if (seen.has(id)) {
544
- entries.push({
545
- id,
546
- value: cloneValue(byId[id], "inverse")
547
- });
548
- positions.push(index);
549
- delete byId[id];
550
- } else ids[writeIndex++] = id;
551
- }
552
- ids.length = writeIndex;
553
- } else for (const id of operation.ids) {
554
- entries.push({
555
- id,
556
- value: cloneValue(byId[id], "inverse")
557
- });
558
- delete byId[id];
559
- }
560
- return {
561
- status: "changed",
562
- inverse: [{
563
- type: "entity.create",
564
- at: operation.at,
565
- entries,
566
- ...ids ? { positions } : {}
567
- }]
568
- };
569
- }
570
- const exists = Object.prototype.hasOwnProperty.call(byId, operation.id);
571
- const position = ids?.indexOf(operation.id) ?? -1;
572
- if (!exists) return rejected$4(operation, "missing-entity", `Entity '${operation.id}' does not exist.`);
573
- if (!ids || position < 0) return rejected$4(operation, "invalid-collection", `Collection index is missing '${operation.id}'.`);
574
- if (!valid(ids, operation.anchor)) return rejected$4(operation, "invalid-anchor", "Collection anchor is invalid.");
575
- if (operation.anchor && ("before" in operation.anchor && operation.anchor.before === operation.id || "after" in operation.anchor && operation.anchor.after === operation.id)) return rejected$4(operation, "invalid-anchor", "An entity cannot be moved relative to itself.");
576
- const nextIndex = afterRemove(ids, position, operation.anchor);
577
- if (nextIndex === position) return { status: "unchanged" };
578
- const inverse = {
579
- type: "entity.move",
580
- at: operation.at,
581
- id: operation.id,
582
- anchor: at$1(ids, position)
583
- };
584
- ids.splice(position, 1);
585
- ids.splice(nextIndex, 0, operation.id);
586
- return {
587
- status: "changed",
588
- inverse: [inverse]
589
- };
186
+ const orderOf = (node, value) => {
187
+ if (node.kind === "list") return value.map(node.keyOf);
188
+ if (node.kind === "table") return [...value.ids];
189
+ throw new Error("Order requires a table or list.");
590
190
  };
591
- //#endregion
592
- //#region core/src/mutation/execute-list.ts
593
- const rejected$3 = (operation, code, message) => ({
594
- status: "rejected",
595
- issue: from(operation, code, message)
596
- });
597
- const executeList = (target, operation, copyPayload) => {
598
- if (target.node.kind !== "list") return rejected$3(operation, "invalid-address", "List target is invalid.");
599
- const absent = target.value === void 0 && "optional" in target.node && target.node.optional === true;
600
- if (!Array.isArray(target.value) && !absent) return rejected$3(operation, "invalid-address", "List target is invalid.");
601
- if (absent && operation.type !== "list.replace") return rejected$3(operation, "invalid-address", "Only list replacement can initialize an optional list.");
602
- const list = Array.isArray(target.value) ? target.value : [];
603
- const orderedKeys = keys(list, target.node.keyOf);
604
- const own = (value) => copyPayload ? ownPayload(value) : value;
605
- if (operation.type === "list.replace") {
606
- if (operation.keys.length !== operation.value.length) return rejected$3(operation, "invalid-list-keys", "List replacement keys are invalid.");
607
- const seen = /* @__PURE__ */ new Set();
608
- for (let index = 0; index < operation.value.length; index += 1) {
609
- const key = operation.keys[index];
610
- if (seen.has(key) || target.node.keyOf(operation.value[index]) !== key) return rejected$3(operation, "invalid-list-keys", "List replacement keys are invalid.");
611
- seen.add(key);
612
- }
613
- if (!absent && sameStructuralValue(list, operation.value)) return { status: "unchanged" };
614
- const keys = list.map(target.node.keyOf);
615
- const inverse = absent ? {
616
- type: "value.clear",
617
- at: operation.at
618
- } : {
619
- type: "list.replace",
620
- at: operation.at,
621
- value: cloneValue(list, "inverse"),
622
- keys
623
- };
624
- const owned = own(operation.value);
625
- if (absent) target.parent[target.key] = owned;
626
- else {
627
- list.length = owned.length;
628
- for (let index = 0; index < owned.length; index += 1) list[index] = owned[index];
629
- }
630
- return {
631
- status: "changed",
632
- inverse: [inverse]
633
- };
634
- }
635
- if (operation.type === "list.insert") {
636
- if (target.node.keyOf(operation.value) !== operation.key) return rejected$3(operation, "invalid-list-key", "List item key does not match schema.");
637
- if (orderedKeys.index(operation.key) >= 0) return rejected$3(operation, "duplicate-list-item", `List item '${operation.key}' already exists.`);
638
- if (!valid(orderedKeys, operation.anchor)) return rejected$3(operation, "invalid-anchor", "List anchor is invalid.");
639
- list.splice(index(orderedKeys, operation.anchor), 0, own(operation.value));
640
- return {
641
- status: "changed",
642
- inverse: [{
643
- type: "list.remove",
644
- at: operation.at,
645
- key: operation.key
646
- }]
647
- };
648
- }
649
- const position = orderedKeys.index(operation.key);
650
- if (position < 0) return rejected$3(operation, "missing-list-item", `List item '${operation.key}' does not exist.`);
651
- if (operation.type === "list.remove") {
652
- const inverse = {
653
- type: "list.insert",
654
- at: operation.at,
655
- key: operation.key,
656
- value: cloneValue(list[position], "inverse"),
657
- anchor: at$1(orderedKeys, position)
658
- };
659
- list.splice(position, 1);
660
- return {
661
- status: "changed",
662
- inverse: [inverse]
663
- };
664
- }
665
- if (!valid(orderedKeys, operation.anchor)) return rejected$3(operation, "invalid-anchor", "List anchor is invalid.");
666
- if (operation.anchor && ("before" in operation.anchor && operation.anchor.before === operation.key || "after" in operation.anchor && operation.anchor.after === operation.key)) return rejected$3(operation, "invalid-anchor", "A list item cannot be moved relative to itself.");
667
- const nextIndex = afterRemove(orderedKeys, position, operation.anchor);
668
- if (nextIndex === position) return { status: "unchanged" };
669
- const inverse = {
670
- type: "list.move",
671
- at: operation.at,
672
- key: operation.key,
673
- anchor: at$1(orderedKeys, position)
674
- };
675
- const [entry] = list.splice(position, 1);
676
- list.splice(nextIndex, 0, entry);
677
- return {
678
- status: "changed",
679
- inverse: [inverse]
680
- };
191
+ const installOrder = (node, value, order) => {
192
+ if (node.kind === "list") install(value, node.keyOf, order);
193
+ else if (node.kind === "table") value.ids = [...order];
194
+ else throw new Error("Order requires a table or list.");
681
195
  };
682
196
  //#endregion
683
- //#region core/src/mutation/tree.ts
684
- const has$1 = (value, key) => Object.prototype.hasOwnProperty.call(value, key);
685
- const validIndex = (index) => index === void 0 || Number.isInteger(index) && index >= 0;
686
- const rejected$2 = (code, message) => ({
687
- status: "rejected",
688
- code,
689
- message
197
+ //#region core/src/mutation/recorder.ts
198
+ const firstMember = (group, key) => {
199
+ if (group.layout === "dynamic") return group.members?.get(key);
200
+ const member = group.definitions.get(key);
201
+ return member && group.members[member.slot];
202
+ };
203
+ const forgetMember = (group, key) => {
204
+ if (group.layout === "dynamic") group.members?.delete(key);
205
+ else {
206
+ const definition = group.definitions.get(key);
207
+ if (definition) group.members[definition.slot] = void 0;
208
+ }
209
+ };
210
+ const emptyGroup = (group) => !group.order && (group.layout === "fixed" ? !group.members.some(Boolean) : !group.members?.size);
211
+ const copyNode = (node) => ({
212
+ ...node,
213
+ children: [...node.children]
690
214
  });
691
- const node = (tree, id) => tree.nodes[id];
692
- const childIndex = (parent, id) => parent.children.indexOf(id);
693
- const sameOrder$1 = (left, right) => {
694
- if (left.length !== right.length) return false;
695
- for (let index = 0; index < left.length; index += 1) if (left[index] !== right[index]) return false;
696
- return true;
697
- };
698
- const rootPresence = (id) => id === void 0 ? { status: "absent" } : {
699
- status: "present",
700
- id
701
- };
702
- const capture = (change, id, presence) => {
703
- if (!change.nodes.has(id)) change.nodes.set(id, presence);
704
- };
705
- const without = (values, id) => {
706
- const result = [];
707
- for (const value of values) if (value !== id) result.push(value);
708
- return result;
709
- };
710
- const inserted = (values, id, index) => {
711
- const result = [...values];
712
- result.splice(Math.min(index, result.length), 0, id);
713
- return result;
714
- };
715
- const moved = (values, id, index) => {
716
- const result = without(values, id);
717
- result.splice(Math.min(index, result.length), 0, id);
718
- return result;
719
- };
720
- const parentPresence = (tree, id, children) => {
721
- const entry = node(tree, id);
722
- return entry ? {
723
- status: "present",
724
- ...entry.parentId === void 0 ? {} : { parentId: entry.parentId },
725
- children,
726
- value: entry.value
727
- } : { status: "absent" };
215
+ const transition = (node, key, beforePresent, before, afterPresent, after) => {
216
+ if (beforePresent === afterPresent && (!beforePresent || Object.is(before, after))) return;
217
+ if (node.kind !== "field" && beforePresent === afterPresent && equalValue(node, before, after)) return;
218
+ const value = afterPresent ? node.kind === "field" ? after : copyValue(node, after) : void 0;
219
+ profile.recorder("transitions");
220
+ return !beforePresent ? {
221
+ key,
222
+ kind: "added",
223
+ after: value
224
+ } : !afterPresent ? {
225
+ key,
226
+ kind: "removed",
227
+ before
228
+ } : {
229
+ key,
230
+ kind: "updated",
231
+ before,
232
+ after: value
233
+ };
234
+ };
235
+ const publishMembers = (changes, at, members, order) => {
236
+ if (!members.length && !order) return;
237
+ for (let i = 1; i < members.length; i++) if (members[i - 1].key > members[i].key) {
238
+ members.sort((a, b) => lexical(a.key, b.key));
239
+ break;
240
+ }
241
+ changes.push(order ? {
242
+ kind: "members",
243
+ at,
244
+ members,
245
+ order
246
+ } : {
247
+ kind: "members",
248
+ at,
249
+ members
250
+ });
728
251
  };
729
- const restoreIncremental = (value, change) => {
730
- if (!is(value)) return value;
731
- if (change.root.status === "absent") delete value.rootId;
732
- else value.rootId = change.root.id;
733
- for (const [id, before] of change.nodes) {
734
- if (before.status === "absent") {
735
- delete value.nodes[id];
736
- continue;
252
+ /** Diff only the captured subtree; this algorithm has no recorder or session state. */
253
+ const diffMember = (changes, members, node, at, key, beforePresent, before, afterPresent, after) => {
254
+ if (beforePresent === afterPresent && (!beforePresent || Object.is(before, after))) return;
255
+ if (beforePresent && afterPresent && before !== void 0 && after !== void 0) {
256
+ if (node.kind === "object" || node.kind === "variant" && before[node.tag] === after[node.tag]) {
257
+ const left = before, right = after;
258
+ const childAt = at.concat(key);
259
+ const children = [];
260
+ for (const [child, definition] of compiledShape(node, before).members) diffMember(changes, children, definition.node, childAt, child, Object.hasOwn(left, child), left[child], Object.hasOwn(right, child), right[child]);
261
+ publishMembers(changes, childAt, children);
262
+ return;
737
263
  }
738
- value.nodes[id] = {
739
- ...before.parentId === void 0 ? {} : { parentId: before.parentId },
740
- children: [...before.children],
741
- value: before.value
742
- };
743
- }
744
- return value;
745
- };
746
- const restoreChange = (value, change) => change.mode === "whole" ? ownPayload(change.value, "journal") : restoreIncremental(value, change);
747
- const wholeChange = (inverse, previous) => {
748
- if (!previous) return {
749
- mode: "whole",
750
- value: inverse.value
751
- };
752
- if (previous.mode === "whole") return previous;
753
- return {
754
- mode: "whole",
755
- value: restoreIncremental(ownPayload(inverse.value, "journal"), previous)
756
- };
757
- };
758
- const removedNodePresences = (inverses) => {
759
- const children = /* @__PURE__ */ new Map();
760
- for (const inverse of inverses) {
761
- if (inverse.parentId === void 0) continue;
762
- const entries = children.get(inverse.parentId);
763
- const child = {
764
- id: inverse.treeNodeId,
765
- index: inverse.index ?? Number.MAX_SAFE_INTEGER
766
- };
767
- if (entries) entries.push(child);
768
- else children.set(inverse.parentId, [child]);
769
- }
770
- const result = /* @__PURE__ */ new Map();
771
- for (const inverse of inverses) {
772
- const ordered = children.get(inverse.treeNodeId) ?? [];
773
- ordered.sort((left, right) => left.index - right.index);
774
- result.set(inverse.treeNodeId, {
775
- status: "present",
776
- ...inverse.parentId === void 0 ? {} : { parentId: inverse.parentId },
777
- children: ordered.map((entry) => entry.id),
778
- value: inverse.value
779
- });
780
- }
781
- return result;
782
- };
783
- const recordChange = (previous, current, forward, inverses) => {
784
- const replacement = inverses[0];
785
- if (replacement?.type === "tree.replace") return wholeChange(replacement, previous);
786
- if (previous?.mode === "whole") return previous;
787
- const change = previous ?? {
788
- mode: "incremental",
789
- root: forward.type === "tree.insert" && forward.parentId === void 0 ? { status: "absent" } : forward.type === "tree.remove" && inverses[0]?.type === "tree.insert" ? rootPresence(inverses[0].parentId === void 0 ? inverses[0].treeNodeId : current.rootId) : rootPresence(current.rootId),
790
- nodes: /* @__PURE__ */ new Map()
791
- };
792
- if (forward.type === "tree.insert") {
793
- capture(change, forward.treeNodeId, { status: "absent" });
794
- if (forward.parentId !== void 0) {
795
- const parentNode = node(current, forward.parentId);
796
- if (parentNode) capture(change, forward.parentId, parentPresence(current, forward.parentId, without(parentNode.children, forward.treeNodeId)));
264
+ if (node.kind === "map" || node.kind === "table") {
265
+ const a = before, b = after;
266
+ const left = node.kind === "table" ? a.byId : a;
267
+ const right = node.kind === "table" ? b.byId : b;
268
+ const childAt = at.concat(key);
269
+ const children = [];
270
+ for (const id of Object.keys(left)) diffMember(changes, children, node.value, childAt, id, true, left[id], Object.hasOwn(right, id), right[id]);
271
+ for (const id of Object.keys(right)) if (!Object.hasOwn(left, id)) diffMember(changes, children, node.value, childAt, id, false, void 0, true, right[id]);
272
+ let order;
273
+ if (node.kind === "table" && !equal(a.ids, b.ids)) {
274
+ profile.recorder("publishedOrderItems", b.ids.length);
275
+ order = {
276
+ before: a.ids,
277
+ after: [...b.ids]
278
+ };
279
+ }
280
+ publishMembers(changes, childAt, children, order);
281
+ return;
797
282
  }
798
- return change;
799
283
  }
800
- if (forward.type === "tree.remove") {
801
- const inserts = inverses.filter((inverse) => inverse.type === "tree.insert");
802
- const removed = removedNodePresences(inserts);
803
- for (const [id, presence] of removed) capture(change, id, presence);
804
- const first = inserts[0];
805
- if (first?.parentId !== void 0) {
806
- const parentNode = node(current, first.parentId);
807
- if (parentNode) capture(change, first.parentId, parentPresence(current, first.parentId, inserted(parentNode.children, first.treeNodeId, first.index ?? parentNode.children.length)));
284
+ const member = transition(node, key, beforePresent, before, afterPresent, after);
285
+ if (member) members.push(member);
286
+ };
287
+ const restore = (fact, schema, root, skip = 0) => {
288
+ const at = skip ? fact.at.slice(skip) : fact.at;
289
+ if (fact.kind === "member") {
290
+ const member = fact.member;
291
+ const value = member.present ? copyValue(member.node, member.value) : void 0;
292
+ if (!at.length) return value;
293
+ const location = resolveLocated(schema, root, at);
294
+ if (!location) throw new Error("Cannot restore an unresolved member.");
295
+ installMember(location.parent, location.key, member.present, value);
296
+ return root;
297
+ }
298
+ const location = resolveValue(schema, root, at);
299
+ if (!location) throw new Error("Cannot restore an unresolved subtree.");
300
+ if (fact.kind === "order") installOrder(location.node, location.value, fact.group.order);
301
+ else {
302
+ const tree = location.value;
303
+ if (fact.before === null) delete tree.rootId;
304
+ else tree.rootId = fact.before;
305
+ for (const [id, node] of fact.nodes) installMember(tree.nodes, id, node !== void 0, node && copyNode(node));
306
+ }
307
+ return root;
308
+ };
309
+ const reconstructBefore = (node, current, records, offset) => {
310
+ let before = copyValue(node, current);
311
+ for (let i = records.length - 1; i >= 0; i--) if (records[i].kind !== "order") before = restore(records[i], node, before, offset);
312
+ for (const record of records) if (record.kind === "order") before = restore(record, node, before, offset);
313
+ return before;
314
+ };
315
+ const sealMembers = (fact, changes) => {
316
+ const members = [];
317
+ for (const member of fact.members?.values() ?? []) {
318
+ if (!member) continue;
319
+ const key = memberKey(fact, member.key);
320
+ const present = Object.hasOwn(fact.parent, key);
321
+ const value = fact.parent[key];
322
+ if (member.node.kind === "field") {
323
+ const change = transition(member.node, member.key, member.present, member.value, present, value);
324
+ if (change) members.push(change);
325
+ } else diffMember(changes, members, member.node, fact.at, member.key, member.present, member.value, present, value);
326
+ }
327
+ let order;
328
+ if (fact.order) {
329
+ const current = fact.node.kind === "list" ? keys(fact.value, fact.node.keyOf) : fact.value.ids;
330
+ if (!equal(fact.order, current)) {
331
+ const after = orderOf(fact.node, fact.value);
332
+ profile.recorder("publishedOrderItems", after.length);
333
+ order = {
334
+ before: fact.order,
335
+ after
336
+ };
808
337
  }
809
- return change;
810
- }
811
- if (forward.type === "tree.set") {
812
- const inverse = inverses[0];
813
- const entry = node(current, forward.treeNodeId);
814
- if (entry && inverse?.type === "tree.set") capture(change, forward.treeNodeId, {
815
- status: "present",
816
- ...entry.parentId === void 0 ? {} : { parentId: entry.parentId },
817
- children: [...entry.children],
818
- value: inverse.value
819
- });
820
- return change;
821
338
  }
822
- if (forward.type === "tree.move") {
823
- const inverse = inverses[0];
824
- const entry = node(current, forward.treeNodeId);
825
- if (!entry || inverse?.type !== "tree.move" || inverse.parentId === void 0) return change;
826
- capture(change, forward.treeNodeId, {
827
- status: "present",
828
- parentId: inverse.parentId,
829
- children: [...entry.children],
830
- value: entry.value
339
+ publishMembers(changes, fact.at, members, order);
340
+ };
341
+ const sealTree = (fact, state, changes) => {
342
+ const location = resolveValue(state.schema, state.document, fact.at);
343
+ const tree = location?.value;
344
+ const node = location?.node;
345
+ if (node?.kind !== "tree") throw new Error("Tree schema disappeared before sealing.");
346
+ const nodes = [];
347
+ for (const [id, before] of fact.nodes) {
348
+ const after = Object.hasOwn(tree.nodes, id) ? tree.nodes[id] : void 0;
349
+ if (!before && !after) continue;
350
+ if (before && after && before.parentId === after.parentId && equal(before.children, after.children) && Object.hasOwn(before, "value") === Object.hasOwn(after, "value") && equalValue(node.value, before.value, after.value)) continue;
351
+ nodes.push(!before ? {
352
+ id,
353
+ kind: "added",
354
+ after: copyNode(after)
355
+ } : !after ? {
356
+ id,
357
+ kind: "removed",
358
+ before
359
+ } : {
360
+ id,
361
+ kind: "updated",
362
+ before,
363
+ after: copyNode(after)
831
364
  });
832
- const oldParent = node(current, inverse.parentId);
833
- if (oldParent) {
834
- const children = inverse.parentId === forward.parentId ? moved(oldParent.children, forward.treeNodeId, inverse.index ?? oldParent.children.length) : inserted(oldParent.children, forward.treeNodeId, inverse.index ?? oldParent.children.length);
835
- capture(change, inverse.parentId, parentPresence(current, inverse.parentId, children));
836
- }
837
- if (forward.parentId !== void 0 && forward.parentId !== inverse.parentId) {
838
- const nextParent = node(current, forward.parentId);
839
- if (nextParent) capture(change, forward.parentId, parentPresence(current, forward.parentId, without(nextParent.children, forward.treeNodeId)));
840
- }
841
- }
842
- return change;
843
- };
844
- const sameNodePresence = (before, value) => {
845
- if (before.status === "absent") return value === void 0;
846
- return value !== void 0 && before.parentId === value.parentId && sameOrder$1(before.children, value.children) && sameStructuralValue(before.value, value.value);
847
- };
848
- const changeChanged = (change, value) => {
849
- if (change.mode === "whole") return !sameStructuralValue(change.value, value);
850
- if (!is(value)) return true;
851
- if ((change.root.status === "present" ? change.root.id : void 0) !== value.rootId) return true;
852
- for (const [id, before] of change.nodes) if (!sameNodePresence(before, node(value, id))) return true;
853
- return false;
854
- };
855
- const is = (value) => isRecord(value) && isRecord(value.nodes);
856
- const contains = (tree, id) => has$1(tree.nodes, id);
857
- const validate = (value) => {
858
- if (!is(value)) return false;
859
- const ids = Object.keys(value.nodes);
860
- if (ids.length === 0) return value.rootId === void 0;
861
- if (typeof value.rootId !== "string" || !has$1(value.nodes, value.rootId)) return false;
862
- for (const id of ids) {
863
- const entry = value.nodes[id];
864
- if (!isRecord(entry) || !Array.isArray(entry.children)) return false;
865
- if (entry.children.some((child) => typeof child !== "string") || new Set(entry.children).size !== entry.children.length) return false;
866
- if (entry.parentId !== void 0 && typeof entry.parentId !== "string") return false;
867
- if (id === value.rootId) {
868
- if (entry.parentId !== void 0) return false;
869
- } else if (entry.parentId === void 0 || !has$1(value.nodes, entry.parentId)) return false;
870
- for (const child of entry.children) {
871
- const childNode = value.nodes[child];
872
- if (!childNode || childNode.parentId !== id) return false;
873
- }
874
365
  }
875
- const seen = /* @__PURE__ */ new Set();
876
- const stack = [value.rootId];
877
- while (stack.length) {
878
- const id = stack.pop();
879
- if (seen.has(id)) return false;
880
- seen.add(id);
881
- const entry = value.nodes[id];
882
- if (!entry) return false;
883
- for (const child of entry.children) stack.push(child);
884
- }
885
- return seen.size === ids.length;
366
+ const after = tree.rootId ?? null;
367
+ if (nodes.length || fact.before !== after) changes.push({
368
+ kind: "tree",
369
+ at: fact.at,
370
+ before: fact.before,
371
+ after,
372
+ nodes: nodes.sort((a, b) => lexical(a.id, b.id))
373
+ });
886
374
  };
887
- const documentTreeError = (node, value, address) => {
888
- if ("optional" in node && node.optional && value === void 0) return void 0;
889
- if (node.kind === "tree") return validate(value) ? void 0 : address;
890
- if (node.kind === "object") {
891
- if (!isRecord(value)) return void 0;
892
- for (const [key, child] of Object.entries(node.shape)) {
893
- const error = documentTreeError(child, value[key], [...address, key]);
894
- if (error) return error;
895
- }
896
- return;
897
- }
898
- if (node.kind === "variant") {
899
- if (!isRecord(value) || typeof value[node.tag] !== "string") return void 0;
900
- const variant = node.variants[value[node.tag]];
901
- return variant ? documentTreeError(variant, value, address) : void 0;
902
- }
903
- if (node.kind === "table") {
904
- if (!isRecord(value) || !isRecord(value.byId)) return void 0;
905
- for (const [id, entry] of Object.entries(value.byId)) {
906
- const error = documentTreeError(node.value, entry, [...address, id]);
907
- if (error) return error;
908
- }
909
- return;
910
- }
911
- if (node.kind === "map") {
912
- if (!isRecord(value)) return void 0;
913
- for (const [id, entry] of Object.entries(value)) {
914
- const error = documentTreeError(node.value, entry, [...address, id]);
915
- if (error) return error;
916
- }
375
+ /** One first-touch record per owning container, never a per-write operation log. */
376
+ var ChangeRecorder = class {
377
+ facts = /* @__PURE__ */ new Set();
378
+ groups = /* @__PURE__ */ new WeakMap();
379
+ index;
380
+ indexedGroups = false;
381
+ resetBefore;
382
+ constructor(state) {
383
+ this.state = state;
384
+ }
385
+ indexGroups() {
386
+ const index = this.index ??= new AddressIndex();
387
+ if (!this.indexedGroups) {
388
+ this.indexedGroups = true;
389
+ for (const fact of this.facts) if (fact.kind === "members") {
390
+ index.add(fact.at, fact);
391
+ profile.recorder("indexedGroups");
392
+ }
393
+ }
394
+ return index;
395
+ }
396
+ covered(at) {
397
+ return this.resetBefore !== void 0 || this.indexedGroups && this.index.someAncestor(at, (fact) => fact.kind === "members" && fact.at.length < at.length && firstMember(fact, at[fact.at.length]) !== void 0);
398
+ }
399
+ absorb(at, node, value, present) {
400
+ const index = this.indexGroups();
401
+ const children = [];
402
+ index.query((fact) => {
403
+ if (fact.kind === "members") {
404
+ if (fact.at.length < at.length) {
405
+ if (fact.at.length + 1 === at.length) {
406
+ const member = firstMember(fact, at[fact.at.length]);
407
+ if (member) children.push({
408
+ kind: "member",
409
+ at,
410
+ group: fact,
411
+ member
412
+ });
413
+ }
414
+ return;
415
+ }
416
+ if (fact.order && contains(at, fact.at)) children.push({
417
+ kind: "order",
418
+ at: fact.at,
419
+ group: fact
420
+ });
421
+ for (const member of fact.members?.values() ?? []) {
422
+ if (!member) continue;
423
+ const memberAt = fact.at.concat(member.key);
424
+ if (contains(at, memberAt)) children.push({
425
+ kind: "member",
426
+ at: memberAt,
427
+ group: fact,
428
+ member
429
+ });
430
+ }
431
+ } else if (contains(at, fact.at)) children.push(fact);
432
+ })(at);
433
+ if (present) value = reconstructBefore(node, value, children, at.length);
434
+ this.releaseCovered(children);
435
+ return value;
917
436
  }
918
- };
919
- const invalidDocument = (schema, value) => documentTreeError({
920
- kind: "object",
921
- shape: schema.shape
922
- }, value, []);
923
- const replace = (current, operation) => {
924
- if (!validate(operation.value)) return rejected$2("invalid-tree", "Tree replacement is malformed.");
925
- if (sameStructuralValue(current, operation.value)) return { status: "unchanged" };
926
- return {
927
- status: "changed",
928
- inverse: [{
929
- type: "tree.replace",
930
- at: operation.at,
931
- value: cloneValue(current, "inverse")
932
- }]
933
- };
934
- };
935
- const insert = (tree, operation, value) => {
936
- if (contains(tree, operation.treeNodeId)) return rejected$2("duplicate-tree-node", `Tree node '${operation.treeNodeId}' already exists.`);
937
- if (!validIndex(operation.index)) return rejected$2("invalid-tree-index", "Tree index is invalid.");
938
- if (tree.rootId === void 0) {
939
- if (operation.parentId !== void 0) return rejected$2("missing-tree-parent", `Tree parent '${operation.parentId}' does not exist.`);
940
- tree.nodes[operation.treeNodeId] = {
941
- children: [],
942
- value
943
- };
944
- tree.rootId = operation.treeNodeId;
945
- } else {
946
- if (operation.parentId === void 0) return rejected$2("invalid-tree", "A non-empty tree has exactly one root.");
947
- const parentNode = node(tree, operation.parentId);
948
- if (!parentNode) return rejected$2("missing-tree-parent", `Tree parent '${operation.parentId}' does not exist.`);
949
- tree.nodes[operation.treeNodeId] = {
950
- parentId: operation.parentId,
951
- children: [],
437
+ releaseCovered(children) {
438
+ let changedGroups;
439
+ for (const child of children) {
440
+ profile.recorder("absorbed");
441
+ if (child.kind === "member") {
442
+ const { group, member } = child;
443
+ forgetMember(group, member.key);
444
+ (changedGroups ??= /* @__PURE__ */ new Set()).add(group);
445
+ } else if (child.kind === "order") {
446
+ child.group.order = void 0;
447
+ (changedGroups ??= /* @__PURE__ */ new Set()).add(child.group);
448
+ } else this.unregister(child);
449
+ }
450
+ for (const group of changedGroups ?? []) if (emptyGroup(group)) this.unregister(group);
451
+ }
452
+ register(fact) {
453
+ this.facts.add(fact);
454
+ if (fact.kind === "members") {
455
+ this.groups.set(fact.parent, fact);
456
+ profile.recorder("groups");
457
+ if (!this.indexedGroups) return;
458
+ profile.recorder("indexedGroups");
459
+ }
460
+ (this.index ??= new AddressIndex()).add(fact.at, fact);
461
+ }
462
+ unregister(fact) {
463
+ this.facts.delete(fact);
464
+ this.index?.delete(fact.at, fact);
465
+ if (fact.kind === "members") this.groups.delete(fact.parent);
466
+ }
467
+ member(container, key, definition, physicalKey) {
468
+ if (this.resetBefore) return;
469
+ const { node } = definition;
470
+ let group = this.groups.get(container.parent);
471
+ if (group?.layout === "dynamic" && group.members?.has(key)) return;
472
+ if (group?.layout === "fixed" && definition.kind === "fixed" && group.members[definition.slot]) return;
473
+ const at = this.indexedGroups || node.kind !== "field" ? container.at.concat(key) : void 0;
474
+ if (at && this.covered(at)) return;
475
+ const present = Object.hasOwn(container.parent, physicalKey);
476
+ let value = container.parent[physicalKey];
477
+ if (node.kind !== "field") value = this.absorb(at, node, value, present);
478
+ if (!group || !this.facts.has(group)) group = this.createGroup(container);
479
+ const member = {
480
+ key,
481
+ node,
482
+ present,
952
483
  value
953
484
  };
954
- parentNode.children.splice(Math.min(operation.index ?? parentNode.children.length, parentNode.children.length), 0, operation.treeNodeId);
955
- }
956
- return {
957
- status: "changed",
958
- inverse: [{
959
- type: "tree.remove",
960
- at: operation.at,
961
- treeNodeId: operation.treeNodeId
962
- }]
963
- };
964
- };
965
- const set = (tree, operation, value) => {
966
- const entry = node(tree, operation.treeNodeId);
967
- if (!entry) return rejected$2("missing-tree-node", `Tree node '${operation.treeNodeId}' does not exist.`);
968
- if (Object.is(entry.value, value)) return { status: "unchanged" };
969
- const inverse = {
970
- type: "tree.set",
971
- at: operation.at,
972
- treeNodeId: operation.treeNodeId,
973
- value: cloneValue(entry.value, "inverse")
974
- };
975
- entry.value = value;
976
- return {
977
- status: "changed",
978
- inverse: [inverse]
979
- };
980
- };
981
- const remove = (tree, operation) => {
982
- const first = node(tree, operation.treeNodeId);
983
- if (!first) return rejected$2("missing-tree-node", `Tree node '${operation.treeNodeId}' does not exist.`);
984
- const parentId = first.parentId;
985
- const parentNode = parentId === void 0 ? void 0 : node(tree, parentId);
986
- const originalIndex = parentNode ? childIndex(parentNode, operation.treeNodeId) : 0;
987
- if (parentId !== void 0 && (!parentNode || originalIndex < 0) || parentId === void 0 && tree.rootId !== operation.treeNodeId) return rejected$2("invalid-tree", "Tree parent links are inconsistent.");
988
- const entries = [];
989
- const seen = /* @__PURE__ */ new Set();
990
- const stack = [{
991
- id: operation.treeNodeId,
992
- parentId,
993
- index: originalIndex
994
- }];
995
- while (stack.length) {
996
- const frame = stack.pop();
997
- const entry = node(tree, frame.id);
998
- if (!entry || seen.has(frame.id) || entry.parentId !== frame.parentId) return rejected$2("invalid-tree", "Tree contains an invalid descendant.");
999
- seen.add(frame.id);
1000
- entries.push({
1001
- id: frame.id,
1002
- parentId: frame.parentId,
1003
- index: frame.index,
1004
- value: cloneValue(entry.value, "inverse")
1005
- });
1006
- for (let index = entry.children.length - 1; index >= 0; index -= 1) {
1007
- const childId = entry.children[index];
1008
- const child = node(tree, childId);
1009
- if (!child || child.parentId !== frame.id) return rejected$2("invalid-tree", "Tree child links are inconsistent.");
1010
- stack.push({
1011
- id: childId,
1012
- parentId: frame.id,
1013
- index
1014
- });
1015
- }
1016
- }
1017
- if (parentNode) parentNode.children.splice(originalIndex, 1);
1018
- for (let index = entries.length - 1; index >= 0; index -= 1) delete tree.nodes[entries[index].id];
1019
- if (tree.rootId === operation.treeNodeId) delete tree.rootId;
1020
- return {
1021
- status: "changed",
1022
- inverse: entries.map((entry) => ({
1023
- type: "tree.insert",
1024
- at: operation.at,
1025
- treeNodeId: entry.id,
1026
- ...entry.parentId === void 0 ? {} : { parentId: entry.parentId },
1027
- index: entry.index,
1028
- value: entry.value
1029
- }))
1030
- };
1031
- };
1032
- const move = (tree, operation) => {
1033
- const entry = node(tree, operation.treeNodeId);
1034
- if (!entry) return rejected$2("missing-tree-node", `Tree node '${operation.treeNodeId}' does not exist.`);
1035
- if (!validIndex(operation.index)) return rejected$2("invalid-tree-index", "Tree index is invalid.");
1036
- if (tree.rootId === operation.treeNodeId) return rejected$2("invalid-tree", "The root cannot be moved.");
1037
- if (operation.parentId === void 0) return rejected$2("invalid-tree", "Only the root can have no parent.");
1038
- const nextParent = node(tree, operation.parentId);
1039
- if (!nextParent) return rejected$2("missing-tree-parent", `Tree parent '${operation.parentId}' does not exist.`);
1040
- if (operation.parentId === operation.treeNodeId) return rejected$2("tree-cycle", "A tree node cannot parent itself.");
1041
- const descendants = [operation.treeNodeId];
1042
- const seen = /* @__PURE__ */ new Set();
1043
- while (descendants.length) {
1044
- const id = descendants.pop();
1045
- if (seen.has(id)) return rejected$2("invalid-tree", "Tree contains a cycle.");
1046
- seen.add(id);
1047
- const current = node(tree, id);
1048
- if (!current) return rejected$2("invalid-tree", "Tree contains a missing node.");
1049
- for (const childId of current.children) {
1050
- const child = node(tree, childId);
1051
- if (!child || child.parentId !== id) return rejected$2("invalid-tree", "Tree child links are inconsistent.");
1052
- if (childId === operation.parentId) return rejected$2("tree-cycle", "Tree move would create a cycle.");
1053
- descendants.push(childId);
1054
- }
1055
- }
1056
- const parentId = entry.parentId;
1057
- const currentParent = parentId === void 0 ? void 0 : node(tree, parentId);
1058
- if (!currentParent || parentId === void 0) return rejected$2("invalid-tree", "Tree parent links are inconsistent.");
1059
- const originalIndex = childIndex(currentParent, operation.treeNodeId);
1060
- if (originalIndex < 0) return rejected$2("invalid-tree", "Tree parent links are inconsistent.");
1061
- currentParent.children.splice(originalIndex, 1);
1062
- const nextIndex = Math.min(operation.index ?? nextParent.children.length, nextParent.children.length);
1063
- if (parentId === operation.parentId && nextIndex === originalIndex) {
1064
- currentParent.children.splice(originalIndex, 0, operation.treeNodeId);
1065
- return { status: "unchanged" };
1066
- }
1067
- nextParent.children.splice(nextIndex, 0, operation.treeNodeId);
1068
- entry.parentId = operation.parentId;
1069
- return {
1070
- status: "changed",
1071
- inverse: [{
1072
- type: "tree.move",
1073
- at: operation.at,
1074
- treeNodeId: operation.treeNodeId,
1075
- parentId,
1076
- index: originalIndex
1077
- }]
1078
- };
1079
- };
1080
- //#endregion
1081
- //#region core/src/mutation/execute-tree.ts
1082
- const outcome = (operation, value) => value.status === "rejected" ? {
1083
- status: "rejected",
1084
- issue: from(operation, value.code, value.message)
1085
- } : value;
1086
- const executeTree = (root, target, operation, copyPayload) => {
1087
- if (target.node.kind !== "tree") return {
1088
- status: "rejected",
1089
- issue: from(operation, "invalid-address", "Tree target is invalid.")
1090
- };
1091
- const own = (value) => copyPayload ? ownPayload(value) : value;
1092
- const absent = target.value === void 0 && "optional" in target.node && target.node.optional === true;
1093
- if (operation.type === "tree.replace") {
1094
- if (absent) {
1095
- if (!validate(operation.value)) return {
1096
- status: "rejected",
1097
- issue: from(operation, "invalid-tree", "Tree replacement is malformed.")
1098
- };
1099
- target.parent[target.key] = own(operation.value);
1100
- return {
1101
- status: "changed",
1102
- inverse: [{
1103
- type: "value.clear",
1104
- at: operation.at
1105
- }]
1106
- };
1107
- }
1108
- if (!is(target.value)) return {
1109
- status: "rejected",
1110
- issue: from(operation, "invalid-address", "Tree target is invalid.")
1111
- };
1112
- const result = replace(target.value, operation);
1113
- if (result.status === "changed") set$1(root, operation.at, ownPayload(operation.value));
1114
- return outcome(operation, result);
1115
- }
1116
- if (!is(target.value)) return {
1117
- status: "rejected",
1118
- issue: from(operation, "invalid-address", "Tree target is invalid.")
1119
- };
1120
- if (operation.type === "tree.insert") return outcome(operation, insert(target.value, operation, own(operation.value)));
1121
- if (operation.type === "tree.set") return outcome(operation, set(target.value, operation, own(operation.value)));
1122
- if (operation.type === "tree.remove") return outcome(operation, remove(target.value, operation));
1123
- return outcome(operation, move(target.value, operation));
1124
- };
1125
- //#endregion
1126
- //#region core/src/mutation/execute-value.ts
1127
- const rejected$1 = (operation, code, message) => ({
1128
- status: "rejected",
1129
- issue: from(operation, code, message)
1130
- });
1131
- const executeValue = (target, operation, copyPayload) => {
1132
- const own = (value) => copyPayload ? ownPayload(value) : value;
1133
- if (operation.type === "field.set" || operation.type === "field.clear") {
1134
- const existed = Object.prototype.hasOwnProperty.call(target.parent, target.key);
1135
- if (operation.type === "field.clear") {
1136
- if (target.node.kind !== "field" || !target.node.optional) return rejected$1(operation, "required-field", "Only optional fields can be cleared.");
1137
- if (!existed) return { status: "unchanged" };
1138
- const inverse = {
1139
- type: "field.set",
1140
- at: operation.at,
1141
- value: target.value
1142
- };
1143
- delete target.parent[target.key];
1144
- return {
1145
- status: "changed",
1146
- inverse: [inverse]
1147
- };
1148
- }
1149
- if (existed && Object.is(target.value, operation.value)) return { status: "unchanged" };
1150
- const inverse = existed ? {
1151
- type: "field.set",
1152
- at: operation.at,
1153
- value: target.value
485
+ if (group.layout === "dynamic") (group.members ??= /* @__PURE__ */ new Map()).set(key, member);
486
+ else if (definition.kind === "fixed") group.members[definition.slot] = member;
487
+ else throw new Error("A fixed member group requires a fixed schema member.");
488
+ profile.recorder("facts");
489
+ }
490
+ reset() {
491
+ if (this.resetBefore) return;
492
+ const value = this.absorb([], this.state.schema, this.state.document, true);
493
+ this.resetBefore = { value };
494
+ this.groups = /* @__PURE__ */ new WeakMap();
495
+ profile.recorder("facts");
496
+ }
497
+ createGroup(container) {
498
+ const group = container.layout.kind === "fixed" ? {
499
+ kind: "members",
500
+ at: container.at,
501
+ node: container.node,
502
+ parent: container.parent,
503
+ value: container.value,
504
+ layout: "fixed",
505
+ definitions: container.layout.members,
506
+ members: new Array(container.layout.members.size)
1154
507
  } : {
1155
- type: "field.clear",
1156
- at: operation.at
1157
- };
1158
- target.parent[target.key] = operation.value;
1159
- return {
1160
- status: "changed",
1161
- inverse: [inverse]
1162
- };
1163
- }
1164
- if (operation.type === "variant.replace") {
1165
- if (sameStructuralValue(target.value, operation.value)) return { status: "unchanged" };
1166
- if (target.value === void 0 && target.node.optional) {
1167
- target.parent[target.key] = own(operation.value);
1168
- return {
1169
- status: "changed",
1170
- inverse: [{
1171
- type: "value.clear",
1172
- at: operation.at
1173
- }]
1174
- };
1175
- }
1176
- const inverse = {
1177
- type: "variant.replace",
1178
- at: operation.at,
1179
- value: ownPayload(target.value, "inverse")
1180
- };
1181
- target.parent[target.key] = own(operation.value);
1182
- return {
1183
- status: "changed",
1184
- inverse: [inverse]
1185
- };
1186
- }
1187
- if (operation.type === "dict.replace" && target.value === void 0) {
1188
- if (!("optional" in target.node) || !target.node.optional) return rejected$1(operation, "invalid-address", "Dictionary target is invalid.");
1189
- const inverse = {
1190
- type: "value.clear",
1191
- at: operation.at
1192
- };
1193
- target.parent[target.key] = own(operation.value);
1194
- return {
1195
- status: "changed",
1196
- inverse: [inverse]
1197
- };
1198
- }
1199
- if (!isRecord(target.value)) return rejected$1(operation, "invalid-address", "Dictionary target is invalid.");
1200
- if (operation.type === "dict.replace") {
1201
- if (sameStructuralValue(target.value, operation.value)) return { status: "unchanged" };
1202
- const inverse = {
1203
- type: "dict.replace",
1204
- at: operation.at,
1205
- value: cloneValue(target.value, "inverse")
1206
- };
1207
- target.parent[target.key] = own(operation.value);
1208
- return {
1209
- status: "changed",
1210
- inverse: [inverse]
1211
- };
1212
- }
1213
- const existed = Object.prototype.hasOwnProperty.call(target.value, operation.key);
1214
- if (operation.type === "dict.delete") {
1215
- if (!existed) return { status: "unchanged" };
1216
- const inverse = {
1217
- type: "dict.set",
1218
- at: operation.at,
1219
- key: operation.key,
1220
- value: target.value[operation.key]
1221
- };
1222
- delete target.value[operation.key];
1223
- return {
1224
- status: "changed",
1225
- inverse: [inverse]
1226
- };
1227
- }
1228
- if (existed && Object.is(target.value[operation.key], operation.value)) return { status: "unchanged" };
1229
- const inverse = existed ? {
1230
- type: "dict.set",
1231
- at: operation.at,
1232
- key: operation.key,
1233
- value: target.value[operation.key]
1234
- } : {
1235
- type: "dict.delete",
1236
- at: operation.at,
1237
- key: operation.key
1238
- };
1239
- target.value[operation.key] = operation.value;
1240
- return {
1241
- status: "changed",
1242
- inverse: [inverse]
1243
- };
1244
- };
1245
- //#endregion
1246
- //#region core/src/mutation/execute.ts
1247
- const hasOwn$1 = (value, key) => Object.prototype.hasOwnProperty.call(value, key);
1248
- const executeClear = (target, operation) => {
1249
- if (!("optional" in target.node) || !target.node.optional) return {
1250
- status: "rejected",
1251
- issue: from(operation, "required-field", "Only optional values can be cleared.")
1252
- };
1253
- if (!hasOwn$1(target.parent, target.key)) return { status: "unchanged" };
1254
- if (target.node.kind !== "field" && target.node.kind !== "variant" && target.node.kind !== "dict" && target.node.kind !== "list" && target.node.kind !== "tree") return {
1255
- status: "rejected",
1256
- issue: from(operation, "invalid-operation", "This optional value has no clear operation.")
1257
- };
1258
- const inverse = (() => {
1259
- switch (target.node.kind) {
1260
- case "field": return {
1261
- type: "field.set",
1262
- at: operation.at,
1263
- value: target.value
1264
- };
1265
- case "variant": return {
1266
- type: "variant.replace",
1267
- at: operation.at,
1268
- value: cloneValue(target.value, "inverse")
1269
- };
1270
- case "dict": return {
1271
- type: "dict.replace",
1272
- at: operation.at,
1273
- value: cloneValue(target.value, "inverse")
1274
- };
1275
- case "list":
1276
- if (!Array.isArray(target.value)) return {
1277
- type: "list.replace",
1278
- at: operation.at,
1279
- value: [],
1280
- keys: []
1281
- };
1282
- return {
1283
- type: "list.replace",
1284
- at: operation.at,
1285
- value: cloneValue(target.value, "inverse"),
1286
- keys: target.value.map(target.node.keyOf)
1287
- };
1288
- case "tree": return {
1289
- type: "tree.replace",
1290
- at: operation.at,
1291
- value: cloneValue(target.value, "inverse")
508
+ kind: "members",
509
+ at: container.at,
510
+ node: container.node,
511
+ parent: container.parent,
512
+ value: container.value,
513
+ layout: "dynamic",
514
+ members: void 0
515
+ };
516
+ this.register(group);
517
+ return group;
518
+ }
519
+ order(container) {
520
+ profile.recorder("orderCaptures");
521
+ let group = this.groups.get(container.parent);
522
+ if (group?.order || this.covered(container.at)) return;
523
+ group ??= this.createGroup(container);
524
+ group.order = orderOf(container.node, container.value);
525
+ profile.recorder("orderSnapshots");
526
+ profile.recorder("orderItems", group.order.length);
527
+ }
528
+ tree(container, id) {
529
+ const { at, value: tree } = container;
530
+ if (this.covered(at)) return;
531
+ const index = this.index ??= new AddressIndex();
532
+ let fact;
533
+ for (const item of index.exact(at) ?? []) if (item.kind === "tree") fact = item;
534
+ if (!fact) {
535
+ fact = {
536
+ kind: "tree",
537
+ at,
538
+ before: tree.rootId ?? null,
539
+ nodes: /* @__PURE__ */ new Map()
1292
540
  };
541
+ this.register(fact);
1293
542
  }
1294
- })();
1295
- delete target.parent[target.key];
1296
- return {
1297
- status: "changed",
1298
- inverse: [inverse]
1299
- };
1300
- };
1301
- const invalidTarget = (operation) => ({
1302
- status: "rejected",
1303
- issue: from(operation, "invalid-address", `Operation '${operation.type}' does not match the schema address.`)
1304
- });
1305
- const execute = (root, operation, target, copyPayload) => {
1306
- if (operation.type === "value.clear") return !target ? invalidTarget(operation) : executeClear(target, operation);
1307
- if (operation.type === "field.set" || operation.type === "field.clear") return !target || target.node.kind !== "field" ? invalidTarget(operation) : executeValue(target, operation, copyPayload);
1308
- if (operation.type === "variant.replace") return !target || target.node.kind !== "variant" ? invalidTarget(operation) : executeValue(target, operation, copyPayload);
1309
- if (operation.type === "dict.set" || operation.type === "dict.delete" || operation.type === "dict.replace") return !target || target.node.kind !== "dict" ? invalidTarget(operation) : executeValue(target, operation, copyPayload);
1310
- if (operation.type === "entity.create" || operation.type === "entity.remove" || operation.type === "entity.move") return !target || target.node.kind !== "table" && target.node.kind !== "map" ? invalidTarget(operation) : executeEntity(target, operation, copyPayload);
1311
- if (operation.type === "list.insert" || operation.type === "list.move" || operation.type === "list.remove" || operation.type === "list.replace") return !target || target.node.kind !== "list" ? invalidTarget(operation) : executeList(target, operation, copyPayload);
1312
- if (operation.type === "tree.insert" || operation.type === "tree.move" || operation.type === "tree.remove" || operation.type === "tree.set" || operation.type === "tree.replace") return !target || target.node.kind !== "tree" ? invalidTarget(operation) : executeTree(root, target, operation, copyPayload);
1313
- return {
1314
- status: "rejected",
1315
- issue: from(operation, "unknown-operation", "Unknown document operation.")
1316
- };
1317
- };
1318
- //#endregion
1319
- //#region core/src/mutation/journal.ts
1320
- const indexNode = () => ({ children: /* @__PURE__ */ new Map() });
1321
- const hasOwn = (value, key) => Object.prototype.hasOwnProperty.call(value, key);
1322
- const sameAddress = (left, right) => {
1323
- if (left.length !== right.length) return false;
1324
- for (let index = 0; index < left.length; index += 1) if (left[index] !== right[index]) return false;
1325
- return true;
1326
- };
1327
- const sameOrder = (left, right) => {
1328
- if (left.length !== right.length) return false;
1329
- for (let index = 0; index < left.length; index += 1) if (left[index] !== right[index]) return false;
1330
- return true;
1331
- };
1332
- const replaceArray = (target, value) => {
1333
- target.length = value.length;
1334
- for (let index = 0; index < value.length; index += 1) target[index] = value[index];
1335
- };
1336
- const present = (value) => ({
1337
- status: "present",
1338
- value
1339
- });
1340
- const absent = Object.freeze({ status: "absent" });
1341
- const inverseMismatch = (operation) => {
1342
- throw new Error(`Mutation inverse does not match '${operation.type}'.`);
1343
- };
1344
- const ownsDescendant = (subject, address) => {
1345
- if (subject.address.length >= address.length) return false;
1346
- if (subject.kind === "variant") return true;
1347
- if (subject.kind !== "entity") return false;
1348
- return subject.entries.has(address[subject.address.length]);
1349
- };
1350
- const nodeAt = (root, address) => {
1351
- let node = root;
1352
- for (const segment of address) {
1353
- node = node.children.get(segment);
1354
- if (!node) return void 0;
1355
- }
1356
- return node;
1357
- };
1358
- const indexedSubjects = (root, address) => {
1359
- const start = nodeAt(root, address);
1360
- if (!start) return [];
1361
- const subjects = [];
1362
- const stack = [start];
1363
- while (stack.length) {
1364
- const node = stack.pop();
1365
- if (!node) break;
1366
- if (node.subject) subjects.push(node.subject);
1367
- for (const child of node.children.values()) stack.push(child);
1368
- }
1369
- return subjects;
1370
- };
1371
- const writePresence = (root, address, before) => {
1372
- const target = locate(root, address);
1373
- if (!target) return;
1374
- if (before.status === "present") target.parent[target.key] = before.value;
1375
- else if (Array.isArray(target.parent)) target.parent.splice(Number(target.key), 1);
1376
- else delete target.parent[String(target.key)];
1377
- };
1378
- const writeContainer = (root, address, value) => {
1379
- const target = locate(root, address);
1380
- if (!target) return;
1381
- target.parent[target.key] = value;
1382
- };
1383
- const restoreDict = (value, state) => {
1384
- if (state.mode === "whole") {
1385
- for (const key of Object.keys(value)) delete value[key];
1386
- for (const [key, entry] of Object.entries(state.value)) value[key] = entry;
1387
- return;
1388
- }
1389
- for (const [key, before] of state.entries) if (before.status === "present") value[key] = before.value;
1390
- else delete value[key];
1391
- };
1392
- const restoreList = (value, subject) => {
1393
- if (subject.state.mode === "whole") {
1394
- replaceArray(value, subject.state.value);
1395
- return;
1396
- }
1397
- const entries = /* @__PURE__ */ new Map();
1398
- for (const entry of value) entries.set(subject.node.keyOf(entry), entry);
1399
- for (const [key, before] of subject.state.items) if (before.status === "present") entries.set(key, before.value);
1400
- else entries.delete(key);
1401
- const restored = new Array(subject.state.order.length);
1402
- for (let index = 0; index < subject.state.order.length; index += 1) restored[index] = entries.get(subject.state.order[index]);
1403
- replaceArray(value, restored);
1404
- };
1405
- const entityCollection = (subject, value) => resolveEntityCollection(subject.node, value);
1406
- const restoreEntity = (value, subject) => {
1407
- const resolution = entityCollection(subject, value);
1408
- if (resolution.status === "invalid") return;
1409
- const collection = resolution.collection;
1410
- for (const [id, before] of subject.entries) if (before.status === "present") collection.byId[id] = before.value;
1411
- else delete collection.byId[id];
1412
- if (collection.kind !== "table") return;
1413
- if (subject.order.kind === "tracked") {
1414
- replaceArray(collection.ids, subject.order.before);
1415
- return;
1416
- }
1417
- if (subject.order.kind === "stable") restoreEntityOrder(collection.ids, subject.order.inverses);
1418
- };
1419
- const restoreRootSubject = (value, subject) => {
1420
- if (subject.kind === "value") return subject.before.status === "present" ? subject.before.value : void 0;
1421
- if (subject.kind === "variant") return subject.before.status === "present" ? subject.before.value : void 0;
1422
- if (subject.kind === "dict") {
1423
- if (isRecord(value)) restoreDict(value, subject.state);
1424
- return value;
1425
- }
1426
- if (subject.kind === "entity") {
1427
- restoreEntity(value, subject);
1428
- return value;
1429
- }
1430
- if (subject.kind === "list") {
1431
- if (Array.isArray(value)) restoreList(value, subject);
1432
- return value;
1433
- }
1434
- return restoreChange(value, subject.state);
1435
- };
1436
- const restoreSubject = (value, prefix, subject) => {
1437
- if (sameAddress(prefix, subject.address)) return restoreRootSubject(value, subject);
1438
- const address = subject.address.slice(prefix.length);
1439
- if (subject.kind === "value") {
1440
- writePresence(value, address, subject.before);
1441
- return value;
1442
- }
1443
- if (subject.kind === "variant") {
1444
- writePresence(value, address, subject.before);
1445
- return value;
1446
- }
1447
- const current = read(value, address);
1448
- if (subject.kind === "dict") {
1449
- if (isRecord(current)) restoreDict(current, subject.state);
1450
- return value;
1451
- }
1452
- if (subject.kind === "entity") {
1453
- restoreEntity(current, subject);
1454
- return value;
1455
- }
1456
- if (subject.kind === "list") {
1457
- if (Array.isArray(current)) restoreList(current, subject);
1458
- return value;
1459
- }
1460
- const restored = restoreChange(current, subject.state);
1461
- if (restored !== current) writeContainer(value, address, restored);
1462
- return value;
1463
- };
1464
- const valueChanged = (root, subject) => {
1465
- const target = locate(root, subject.address);
1466
- const exists = !!target && hasOwn(target.parent, target.key);
1467
- if (subject.before.status === "absent") return exists;
1468
- return !exists || !Object.is(subject.before.value, target?.value);
1469
- };
1470
- const dictChanged = (root, subject) => {
1471
- const value = read(root, subject.address);
1472
- if (!isRecord(value)) return true;
1473
- if (subject.state.mode === "whole") return !sameStructuralValue(subject.state.value, value);
1474
- for (const [key, before] of subject.state.entries) {
1475
- const exists = hasOwn(value, key);
1476
- if (before.status === "absent" ? exists : !exists || !Object.is(before.value, value[key])) return true;
1477
- }
1478
- return false;
1479
- };
1480
- const listChanged = (root, subject) => {
1481
- const value = read(root, subject.address);
1482
- if (!Array.isArray(value)) return true;
1483
- if (subject.state.mode === "whole") return !sameStructuralValue(subject.state.value, value);
1484
- const positions = /* @__PURE__ */ new Map();
1485
- const order = new Array(value.length);
1486
- for (let index = 0; index < value.length; index += 1) {
1487
- const key = subject.node.keyOf(value[index]);
1488
- order[index] = key;
1489
- positions.set(key, index);
1490
- }
1491
- if (!sameOrder(subject.state.order, order)) return true;
1492
- for (const [key, before] of subject.state.items) {
1493
- const position = positions.get(key);
1494
- if (before.status === "absent") {
1495
- if (position !== void 0) return true;
1496
- } else if (position === void 0 || !sameStructuralValue(before.value, value[position])) return true;
1497
- }
1498
- return false;
1499
- };
1500
- const commonOrderChanged = (before, after) => {
1501
- const finalPositions = /* @__PURE__ */ new Map();
1502
- for (let index = 0; index < after.length; index += 1) finalPositions.set(after[index], index);
1503
- let previous = -1;
1504
- for (const id of before) {
1505
- const position = finalPositions.get(id);
1506
- if (position === void 0) continue;
1507
- if (position < previous) return true;
1508
- previous = position;
1509
- }
1510
- return false;
1511
- };
1512
- const analyzeEntity = (root, subject) => {
1513
- const resolution = entityCollection(subject, read(root, subject.address));
1514
- const added = /* @__PURE__ */ new Set();
1515
- const removed = /* @__PURE__ */ new Set();
1516
- const updated = /* @__PURE__ */ new Set();
1517
- if (resolution.status === "invalid") return {
1518
- changed: true,
1519
- added,
1520
- removed,
1521
- updated,
1522
- orderChanged: false
1523
- };
1524
- const collection = resolution.collection;
1525
- for (const [id, before] of subject.entries) {
1526
- const exists = hasOwn(collection.byId, id);
1527
- if (before.status === "absent") {
1528
- if (exists) added.add(id);
1529
- } else if (!exists) removed.add(id);
1530
- else if (!sameStructuralValue(before.value, collection.byId[id])) updated.add(id);
1531
- }
1532
- const beforeOrder = subject.order.kind === "tracked" ? subject.order.before : void 0;
1533
- const finalOrder = collection.kind === "table" ? collection.ids : [];
1534
- const netOrderChanged = beforeOrder !== void 0 && !sameOrder(beforeOrder, finalOrder);
1535
- return {
1536
- changed: added.size > 0 || removed.size > 0 || updated.size > 0 || netOrderChanged,
1537
- added,
1538
- removed,
1539
- updated,
1540
- orderChanged: beforeOrder !== void 0 && commonOrderChanged(beforeOrder, finalOrder)
1541
- };
1542
- };
1543
- const subjectChanged = (root, subject) => {
1544
- if (subject.kind === "value") return valueChanged(root, subject);
1545
- if (subject.kind === "variant") {
1546
- const target = locate(root, subject.address);
1547
- const exists = !!target && hasOwn(target.parent, target.key);
1548
- return subject.before.status === "absent" ? exists : !exists || !sameStructuralValue(subject.before.value, target?.value);
1549
- }
1550
- if (subject.kind === "dict") return dictChanged(root, subject);
1551
- if (subject.kind === "list") return listChanged(root, subject);
1552
- return changeChanged(subject.state, read(root, subject.address));
1553
- };
1554
- const listOrderBefore = (value, node, inverse) => {
1555
- const order = value.map(node.keyOf);
1556
- if (inverse.type === "list.remove") return order.filter((key) => key !== inverse.key);
1557
- if (inverse.type === "list.insert") {
1558
- order.splice(index(order, inverse.anchor), 0, inverse.key);
1559
- return order;
1560
- }
1561
- if (inverse.type === "list.move") {
1562
- const position = order.indexOf(inverse.key);
1563
- if (position >= 0) order.splice(position, 1);
1564
- order.splice(index(order, inverse.anchor), 0, inverse.key);
1565
- }
1566
- return order;
1567
- };
1568
- const recordListPresence = (state, inverse) => {
1569
- if (inverse.type === "list.remove" && !state.items.has(inverse.key)) state.items.set(inverse.key, absent);
1570
- else if (inverse.type === "list.insert" && !state.items.has(inverse.key)) state.items.set(inverse.key, present(inverse.value));
1571
- };
1572
- const entityOrderInverse = (operation, inverse) => {
1573
- if (inverse.type === "entity.remove") return {
1574
- kind: "remove",
1575
- ids: inverse.ids
1576
- };
1577
- if (inverse.type === "entity.create") {
1578
- if (!inverse.positions || inverse.positions.length !== inverse.entries.length) return inverseMismatch(operation);
1579
- return {
1580
- kind: "restore",
1581
- ids: inverse.entries.map((entry) => entry.id),
1582
- positions: inverse.positions
1583
- };
1584
- }
1585
- if (inverse.type === "entity.move") return {
1586
- kind: "move",
1587
- id: inverse.id,
1588
- ...inverse.anchor ? { anchor: inverse.anchor } : {}
1589
- };
1590
- return inverseMismatch(operation);
1591
- };
1592
- const removeEntityIds = (order, ids) => {
1593
- const removed = new Set(ids);
1594
- let writeIndex = 0;
1595
- for (const id of order) if (!removed.has(id)) order[writeIndex++] = id;
1596
- order.length = writeIndex;
1597
- };
1598
- const restoreEntityOrder = (order, inverses) => {
1599
- for (let index = inverses.length - 1; index >= 0; index -= 1) {
1600
- const inverse = inverses[index];
1601
- if (inverse.kind === "remove") {
1602
- removeEntityIds(order, inverse.ids);
1603
- continue;
1604
- }
1605
- if (inverse.kind === "restore") {
1606
- restore(order, inverse.ids, inverse.positions);
1607
- continue;
1608
- }
1609
- const position = order.indexOf(inverse.id);
1610
- if (position < 0 || !valid(order, inverse.anchor)) throw new Error(`Entity order inverse cannot restore '${inverse.id}'.`);
1611
- const nextIndex = afterRemove(order, position, inverse.anchor);
1612
- order.splice(position, 1);
1613
- order.splice(nextIndex, 0, inverse.id);
1614
- }
1615
- };
1616
- const treeOperation = (operation) => operation.type.startsWith("tree.");
1617
- const createChangeJournal = (root) => {
1618
- const active = /* @__PURE__ */ new Set();
1619
- const index = indexNode();
1620
- const buckets = /* @__PURE__ */ new Map();
1621
- const add = (subject) => {
1622
- active.add(subject);
1623
- const candidates = buckets.get(subject.addressHash);
1624
- if (candidates) candidates.push(subject);
1625
- else buckets.set(subject.addressHash, [subject]);
1626
- let node = index;
1627
- for (const segment of subject.address) {
1628
- const existing = node.children.get(segment);
1629
- if (existing) node = existing;
1630
- else {
1631
- const created = indexNode();
1632
- node.children.set(segment, created);
1633
- node = created;
1634
- }
1635
- }
1636
- if (node.subject) throw new Error(`Change journal already owns '${subject.address.join(".")}'.`);
1637
- node.subject = subject;
1638
- profile.journal.subject();
1639
- return subject;
1640
- };
1641
- const remove = (subject) => {
1642
- if (!active.delete(subject)) return;
1643
- const candidates = buckets.get(subject.addressHash);
1644
- if (candidates) {
1645
- const position = candidates.indexOf(subject);
1646
- if (position >= 0) candidates.splice(position, 1);
1647
- if (candidates.length === 0) buckets.delete(subject.addressHash);
1648
- }
1649
- const nodes = [index];
1650
- let node = index;
1651
- for (const segment of subject.address) {
1652
- const child = node.children.get(segment);
1653
- if (!child) return;
1654
- nodes.push(child);
1655
- node = child;
1656
- }
1657
- if (node.subject === subject) delete node.subject;
1658
- for (let position = subject.address.length - 1; position >= 0; position -= 1) {
1659
- const child = nodes[position + 1];
1660
- if (child.subject || child.children.size > 0) break;
1661
- nodes[position].children.delete(subject.address[position]);
1662
- }
1663
- };
1664
- function find(kind, address, addressHash) {
1665
- const candidates = buckets.get(addressHash) ?? [];
1666
- for (const subject of candidates) {
1667
- profile.journal.comparison();
1668
- if (subject.kind === kind && sameAddress(subject.address, address)) return subject;
543
+ if (id !== void 0 && !fact.nodes.has(id)) {
544
+ profile.recorder("treeNodes");
545
+ fact.nodes.set(id, Object.hasOwn(tree.nodes, id) ? copyNode(tree.nodes[id]) : void 0);
1669
546
  }
1670
547
  }
1671
- const ownerOf = (address) => {
1672
- let node = index;
1673
- for (let position = 0; position < address.length; position += 1) {
1674
- if (node.subject && ownsDescendant(node.subject, address)) return node.subject;
1675
- const child = node.children.get(address[position]);
1676
- if (!child) return void 0;
1677
- node = child;
1678
- }
1679
- };
1680
- const absorb = (value, address) => {
1681
- const subjects = indexedSubjects(index, address);
1682
- if (subjects.length === 0) return value;
1683
- subjects.sort((left, right) => left.address.length - right.address.length);
1684
- let before = ownPayload(value, "journal");
1685
- for (const subject of subjects) before = restoreSubject(before, address, subject);
1686
- for (const subject of subjects) {
1687
- remove(subject);
1688
- profile.journal.absorbed();
1689
- }
1690
- return before;
1691
- };
1692
- const base = (resolved, operation) => ({
1693
- address: operation.at,
1694
- addressHash: resolved.addressHash,
1695
- ...resolved.collection ? { collection: resolved.collection } : {}
1696
- });
1697
- const recordValue = (resolved, operation, inverse) => {
1698
- if (find("value", operation.at, resolved.addressHash)) return;
1699
- const first = inverse[0];
1700
- if (first?.type !== "field.set" && first?.type !== "field.clear" && first?.type !== "value.clear" && !("value" in (first ?? {}))) return inverseMismatch(operation);
1701
- const before = first.type === "field.clear" || first.type === "value.clear" ? absent : "value" in first ? present(first.value) : inverseMismatch(operation);
1702
- add({
1703
- ...base(resolved, operation),
1704
- kind: "value",
1705
- before
1706
- });
1707
- };
1708
- const recordVariant = (resolved, operation, inverse) => {
1709
- if (find("variant", operation.at, resolved.addressHash)) return;
1710
- const first = inverse[0];
1711
- if (first?.type !== "variant.replace" && first?.type !== "value.clear") return inverseMismatch(operation);
1712
- add({
1713
- ...base(resolved, operation),
1714
- kind: "variant",
1715
- before: first.type === "value.clear" ? absent : present(absorb(first.value, operation.at))
1716
- });
1717
- };
1718
- const recordDict = (resolved, operation, inverse) => {
1719
- const first = inverse[0];
1720
- if (!first) return inverseMismatch(operation);
1721
- const subject = find("dict", operation.at, resolved.addressHash) ?? add({
1722
- ...base(resolved, operation),
1723
- kind: "dict",
1724
- state: {
1725
- mode: "incremental",
1726
- entries: /* @__PURE__ */ new Map()
1727
- }
1728
- });
1729
- if (subject.state.mode === "whole") return;
1730
- if (first.type === "dict.replace") {
1731
- const value = ownPayload(first.value, "journal");
1732
- restoreDict(value, subject.state);
1733
- subject.state = {
1734
- mode: "whole",
1735
- value
1736
- };
1737
- return;
1738
- }
1739
- if (first.type !== "dict.set" && first.type !== "dict.delete") return inverseMismatch(operation);
1740
- if (!subject.state.entries.has(first.key)) subject.state.entries.set(first.key, first.type === "dict.set" ? present(first.value) : absent);
1741
- };
1742
- const recordList = (resolved, operation, inverse) => {
1743
- if (resolved.node.kind !== "list") return inverseMismatch(operation);
1744
- const first = inverse[0];
1745
- if (!first) return inverseMismatch(operation);
1746
- let subject = find("list", operation.at, resolved.addressHash);
1747
- if (!subject) {
1748
- if (first.type === "value.clear") {
1749
- add({
1750
- ...base(resolved, operation),
1751
- kind: "list",
1752
- node: resolved.node,
1753
- state: {
1754
- mode: "whole",
1755
- value: void 0
1756
- }
1757
- });
1758
- return;
1759
- }
1760
- if (first.type === "list.replace") {
1761
- add({
1762
- ...base(resolved, operation),
1763
- kind: "list",
1764
- node: resolved.node,
1765
- state: {
1766
- mode: "whole",
1767
- value: first.value
1768
- }
1769
- });
1770
- return;
1771
- }
1772
- const value = read(root, operation.at);
1773
- if (!Array.isArray(value)) return inverseMismatch(operation);
1774
- subject = add({
1775
- ...base(resolved, operation),
1776
- kind: "list",
1777
- node: resolved.node,
1778
- state: {
1779
- mode: "incremental",
1780
- items: /* @__PURE__ */ new Map(),
1781
- order: listOrderBefore(value, resolved.node, first)
1782
- }
1783
- });
1784
- }
1785
- if (subject.state.mode === "whole") return;
1786
- if (first.type === "list.replace") {
1787
- const value = Array.from(ownPayload(first.value, "journal"));
1788
- restoreList(value, subject);
1789
- subject.state = {
1790
- mode: "whole",
1791
- value
1792
- };
1793
- return;
1794
- }
1795
- if (first.type !== "list.insert" && first.type !== "list.move" && first.type !== "list.remove") return inverseMismatch(operation);
1796
- recordListPresence(subject.state, first);
1797
- };
1798
- const trackEntityOrder = (subject, collection) => {
1799
- if (subject.order.kind !== "stable") return;
1800
- profile.journal.orderSnapshot(collection.ids.length);
1801
- const before = [...collection.ids];
1802
- restoreEntityOrder(before, subject.order.inverses);
1803
- subject.order = {
1804
- kind: "tracked",
1805
- before
1806
- };
1807
- };
1808
- const recordEntity = (resolved, operation, inverse) => {
1809
- if (resolved.node.kind !== "table" && resolved.node.kind !== "map") return inverseMismatch(operation);
1810
- const resolution = resolveEntityCollection(resolved.node, read(root, operation.at));
1811
- if (resolution.status === "invalid") return inverseMismatch(operation);
1812
- const collection = resolution.collection;
1813
- const subject = find("entity", operation.at, resolved.addressHash) ?? add({
1814
- ...base(resolved, operation),
1815
- kind: "entity",
1816
- node: resolved.node,
1817
- entries: /* @__PURE__ */ new Map(),
1818
- order: collection.kind === "table" ? {
1819
- kind: "stable",
1820
- inverses: []
1821
- } : { kind: "unordered" }
1822
- });
1823
- const first = inverse[0];
1824
- if (!first) return inverseMismatch(operation);
1825
- if (collection.kind === "table" && subject.order.kind === "stable") subject.order.inverses.push(entityOrderInverse(operation, first));
1826
- if (first.type === "entity.remove") {
1827
- for (const id of first.ids) if (!subject.entries.has(id)) subject.entries.set(id, absent);
1828
- if (collection.kind === "table" && first.ids.some((id) => subject.entries.get(id)?.status === "present")) trackEntityOrder(subject, collection);
548
+ rollback() {
549
+ if (this.resetBefore) {
550
+ this.state.document = this.resetBefore.value;
1829
551
  return;
1830
552
  }
1831
- if (first.type === "entity.create") {
1832
- for (const entry of first.entries) {
1833
- if (subject.entries.has(entry.id)) continue;
1834
- const address = [...operation.at, entry.id];
1835
- subject.entries.set(entry.id, present(absorb(entry.value, address)));
553
+ const facts = [...this.facts];
554
+ for (let i = facts.length - 1; i >= 0; i--) {
555
+ const fact = facts[i];
556
+ if (fact.kind === "members") for (const member of fact.members?.values() ?? []) {
557
+ if (!member) continue;
558
+ installMember(fact.parent, memberKey(fact, member.key), member.present, member.present ? copyValue(member.node, member.value) : void 0);
1836
559
  }
1837
- return;
1838
- }
1839
- if (first.type === "entity.move") {
1840
- if (collection.kind === "table" && subject.entries.get(first.id)?.status !== "absent") trackEntityOrder(subject, collection);
1841
- return;
1842
- }
1843
- return inverseMismatch(operation);
1844
- };
1845
- const recordTree = (resolved, operation, inverse) => {
1846
- const current = read(root, operation.at);
1847
- if (inverse.length === 1 && inverse[0].type === "value.clear" && operation.type === "tree.replace") {
1848
- const subject = find("tree", operation.at, resolved.addressHash);
1849
- if (subject) subject.state = {
1850
- mode: "whole",
1851
- value: void 0
1852
- };
1853
- else add({
1854
- ...base(resolved, operation),
1855
- kind: "tree",
1856
- state: {
1857
- mode: "whole",
1858
- value: void 0
1859
- }
1860
- });
1861
- return;
560
+ else if (fact.kind === "tree") restore(fact, this.state.schema, this.state.document);
1862
561
  }
1863
- if (!is(current)) return inverseMismatch(operation);
1864
- const inverses = inverse.filter(treeOperation);
1865
- if (inverses.length === 0 || inverses.length !== inverse.length) return inverseMismatch(operation);
1866
- const subject = find("tree", operation.at, resolved.addressHash);
1867
- const state = recordChange(subject?.state, current, operation, inverses);
1868
- if (subject) subject.state = state;
1869
- else add({
1870
- ...base(resolved, operation),
1871
- kind: "tree",
1872
- state
1873
- });
1874
- };
1875
- return {
1876
- record: (resolved, operation, inverse) => {
1877
- profile.batch.journalRecord();
1878
- if (ownerOf(operation.at)) return;
1879
- if (operation.type === "value.clear" && resolved.node.kind === "variant") {
1880
- recordVariant(resolved, operation, inverse);
1881
- return;
1882
- }
1883
- if (operation.type === "field.set" || operation.type === "field.clear" || operation.type === "value.clear") {
1884
- recordValue(resolved, operation, inverse);
1885
- return;
1886
- }
1887
- if (operation.type === "variant.replace") {
1888
- recordVariant(resolved, operation, inverse);
1889
- return;
1890
- }
1891
- if (operation.type === "dict.set" || operation.type === "dict.delete" || operation.type === "dict.replace") {
1892
- if (operation.type === "dict.replace" && inverse[0]?.type === "value.clear") recordValue(resolved, operation, inverse);
1893
- else recordDict(resolved, operation, inverse);
1894
- return;
1895
- }
1896
- if (operation.type === "entity.create" || operation.type === "entity.remove" || operation.type === "entity.move") {
1897
- recordEntity(resolved, operation, inverse);
1898
- return;
1899
- }
1900
- if (operation.type === "list.insert" || operation.type === "list.move" || operation.type === "list.remove" || operation.type === "list.replace") {
1901
- recordList(resolved, operation, inverse);
1902
- return;
1903
- }
1904
- recordTree(resolved, operation, inverse);
1905
- },
1906
- finish: () => {
1907
- const changed = [];
1908
- const entityChanges = /* @__PURE__ */ new Map();
1909
- for (const subject of active) {
1910
- profile.journal.comparison();
1911
- if (subject.kind === "entity") {
1912
- const analysis = analyzeEntity(root, subject);
1913
- if (!analysis.changed) continue;
1914
- entityChanges.set(subject, analysis);
1915
- changed.push(subject);
1916
- } else if (subjectChanged(root, subject)) changed.push(subject);
1917
- }
1918
- if (changed.length === 0) return { status: "unchanged" };
1919
- const collections = [];
1920
- const collectionBuckets = /* @__PURE__ */ new Map();
1921
- const changeAt = (address, addressHash) => {
1922
- const candidates = collectionBuckets.get(addressHash) ?? [];
1923
- collectionBuckets.set(addressHash, candidates);
1924
- let change = candidates.find((entry) => sameAddress(entry.address, address));
1925
- if (!change) {
1926
- change = {
1927
- address,
1928
- added: /* @__PURE__ */ new Set(),
1929
- removed: /* @__PURE__ */ new Set(),
1930
- updated: /* @__PURE__ */ new Set(),
1931
- orderChanged: false
1932
- };
1933
- candidates.push(change);
1934
- collections.push(change);
1935
- }
1936
- return change;
1937
- };
1938
- for (const subject of changed) {
1939
- if (subject.kind === "entity") {
1940
- const analysis = entityChanges.get(subject);
1941
- if (analysis) {
1942
- const change = changeAt(subject.address, subject.addressHash);
1943
- analysis.added.forEach((id) => change.added.add(id));
1944
- analysis.removed.forEach((id) => change.removed.add(id));
1945
- analysis.updated.forEach((id) => change.updated.add(id));
1946
- if (analysis.orderChanged) change.orderChanged = true;
1947
- }
1948
- }
1949
- for (let collection = subject.collection; collection; collection = collection.parent) changeAt(collection.address, collection.addressHash).updated.add(collection.id);
1950
- }
1951
- for (const change of collections) {
1952
- change.added.forEach((id) => change.updated.delete(id));
1953
- change.removed.forEach((id) => change.updated.delete(id));
1954
- }
1955
- return {
1956
- status: "changed",
1957
- paths: Object.freeze(changed.map((subject) => subject.address)),
1958
- collections
1959
- };
562
+ for (const fact of facts) if (fact.kind === "members" && fact.order) installOrder(fact.node, fact.value, fact.order);
563
+ }
564
+ seal() {
565
+ if (this.resetBefore) {
566
+ if (equalValue(this.state.schema, this.resetBefore.value, this.state.document)) return sealChanges([]);
567
+ profile.recorder("sealed");
568
+ return sealChanges([{
569
+ kind: "reset",
570
+ before: this.resetBefore.value,
571
+ after: copyValue(this.state.schema, this.state.document)
572
+ }]);
1960
573
  }
1961
- };
1962
- };
1963
- //#endregion
1964
- //#region core/src/mutation/operation.ts
1965
- const has = (value, key) => Object.prototype.hasOwnProperty.call(value, key);
1966
- const stringArray = (value) => Array.isArray(value) && value.every((entry) => typeof entry === "string");
1967
- const positions = (value) => Array.isArray(value) && value.every((entry) => Number.isInteger(entry) && entry >= 0);
1968
- const address = (value) => stringArray(value);
1969
- const anchor = (value) => {
1970
- if (!isRecord(value)) return false;
1971
- if (Number(has(value, "at")) + Number(has(value, "before")) + Number(has(value, "after")) !== 1) return false;
1972
- if (has(value, "at")) return value.at === "start" || value.at === "end";
1973
- return typeof (has(value, "before") ? value.before : value.after) === "string";
1974
- };
1975
- const optionalAnchor = (value) => !has(value, "anchor") || value.anchor === void 0 || anchor(value.anchor);
1976
- const optionalParent = (value) => !has(value, "parentId") || value.parentId === void 0 || typeof value.parentId === "string";
1977
- const optionalIndex = (value) => !has(value, "index") || value.index === void 0 || Number.isInteger(value.index) && value.index >= 0;
1978
- const entries = (value) => Array.isArray(value) && value.every((entry) => isRecord(entry) && typeof entry.id === "string" && has(entry, "value"));
1979
- const decoded = (operation) => ({
1980
- status: "decoded",
1981
- operation
1982
- });
1983
- const rejected = (code, message) => ({
1984
- status: "rejected",
1985
- issue: input(code, message)
1986
- });
1987
- const decode = (input) => {
1988
- if (!isRecord(input)) return rejected("invalid-operation", "Operation must be an object.");
1989
- if (!address(input.at)) return rejected("invalid-address", "Operation address is malformed.");
1990
- if (typeof input.type !== "string") return rejected("invalid-operation", "Operation type must be a string.");
1991
- switch (input.type) {
1992
- case "field.set":
1993
- case "variant.replace": return has(input, "value") ? decoded(input) : rejected("invalid-operation", "Operation value is required.");
1994
- case "field.clear":
1995
- case "value.clear": return decoded(input);
1996
- case "dict.set": return typeof input.key === "string" && has(input, "value") ? decoded(input) : rejected("invalid-operation", "Dictionary set requires a string key and value.");
1997
- case "dict.delete": return typeof input.key === "string" ? decoded(input) : rejected("invalid-operation", "Dictionary delete requires a string key.");
1998
- case "dict.replace": return isRecord(input.value) ? decoded(input) : rejected("invalid-operation", "Dictionary replacement must be an object.");
1999
- case "entity.create": return entries(input.entries) && optionalAnchor(input) && (!has(input, "positions") || input.positions === void 0 || positions(input.positions)) ? decoded(input) : rejected("invalid-operation", "Entity create payload is malformed.");
2000
- case "entity.remove": return stringArray(input.ids) ? decoded(input) : rejected("invalid-operation", "Entity remove requires string ids.");
2001
- case "entity.move": return typeof input.id === "string" && optionalAnchor(input) ? decoded(input) : rejected("invalid-operation", "Entity move payload is malformed.");
2002
- case "list.insert": return typeof input.key === "string" && has(input, "value") && optionalAnchor(input) ? decoded(input) : rejected("invalid-operation", "List insert payload is malformed.");
2003
- case "list.move": return typeof input.key === "string" && optionalAnchor(input) ? decoded(input) : rejected("invalid-operation", "List move payload is malformed.");
2004
- case "list.remove": return typeof input.key === "string" ? decoded(input) : rejected("invalid-operation", "List remove requires a string key.");
2005
- case "list.replace": return Array.isArray(input.value) && stringArray(input.keys) ? decoded(input) : rejected("invalid-operation", "List replacement payload is malformed.");
2006
- case "tree.insert": return typeof input.treeNodeId === "string" && optionalParent(input) && optionalIndex(input) ? decoded(input) : rejected("invalid-operation", "Tree insert payload is malformed.");
2007
- case "tree.move": return typeof input.treeNodeId === "string" && optionalParent(input) && optionalIndex(input) ? decoded(input) : rejected("invalid-operation", "Tree move payload is malformed.");
2008
- case "tree.remove": return typeof input.treeNodeId === "string" ? decoded(input) : rejected("invalid-operation", "Tree remove requires a node id.");
2009
- case "tree.set": return typeof input.treeNodeId === "string" && has(input, "value") ? decoded(input) : rejected("invalid-operation", "Tree set payload is malformed.");
2010
- case "tree.replace": return has(input, "value") ? decoded(input) : rejected("invalid-operation", "Tree replacement value is required.");
2011
- default: return rejected("unknown-operation", "Unknown document operation.");
574
+ const changes = [];
575
+ for (const fact of this.facts) if (fact.kind === "members") sealMembers(fact, changes);
576
+ else sealTree(fact, this.state, changes);
577
+ const result = sealChanges(changes);
578
+ profile.recorder("sealed", result.changes.length);
579
+ return result;
2012
580
  }
2013
581
  };
2014
- const decodeBatch = (input$1) => Array.isArray(input$1) ? {
2015
- status: "decoded",
2016
- operations: input$1
2017
- } : {
2018
- status: "rejected",
2019
- issue: input("invalid-operation", "Operation batch must be an array.")
2020
- };
2021
- const ownAddress = (value) => {
2022
- if (Object.isFrozen(value)) return value;
2023
- profile.address.arrayCopied();
2024
- return Object.freeze(value.slice());
2025
- };
2026
- const structuralPayload = (value) => Array.isArray(value) || isPlainObject(value);
2027
- const normalize = (operation) => {
2028
- profile.mutation.normalized();
2029
- const copy = {
2030
- ...operation,
2031
- at: ownAddress(operation.at)
2032
- };
2033
- if (copy.anchor) copy.anchor = Object.freeze({ ...copy.anchor });
2034
- if (copy.keys) copy.keys = Object.freeze([...copy.keys]);
2035
- if (copy.entries) copy.entries = Object.freeze(copy.entries.map((entry) => Object.freeze({ ...entry })));
2036
- if (copy.ids) copy.ids = Object.freeze([...copy.ids]);
2037
- if (copy.positions) copy.positions = Object.freeze([...copy.positions]);
2038
- return Object.freeze(copy);
2039
- };
2040
- const publish = (operation) => {
2041
- if (operation.type === "entity.create" && operation.entries.every((entry) => !structuralPayload(entry.value) || isStablePayload(entry.value))) return operation;
2042
- if ("value" in operation && (!structuralPayload(operation.value) || isStablePayload(operation.value))) return operation;
2043
- const copy = { ...operation };
2044
- if (copy.entries) copy.entries = Object.freeze(copy.entries.map((entry) => Object.freeze({
2045
- id: entry.id,
2046
- value: structuralPayload(entry.value) ? isStablePayload(entry.value) ? entry.value : snapshotPayload(entry.value, "commit") : entry.value
2047
- })));
2048
- else if ("value" in copy && structuralPayload(copy.value)) copy.value = isStablePayload(copy.value) ? copy.value : snapshotPayload(copy.value, "commit");
2049
- return Object.freeze(copy);
2050
- };
2051
- const requiresPayloadCopy = (operation) => {
2052
- if (operation.type === "entity.create") return operation.entries.some((entry) => isStablePayload(entry.value));
2053
- return "value" in operation && isStablePayload(operation.value);
2054
- };
2055
- const inverse = (operation) => {
2056
- const copy = { ...operation };
2057
- if (copy.anchor) copy.anchor = Object.freeze({ ...copy.anchor });
2058
- if (copy.keys) copy.keys = Object.freeze([...copy.keys]);
2059
- if (copy.entries) copy.entries = Object.freeze(copy.entries.map((entry) => Object.freeze({
2060
- id: entry.id,
2061
- value: structuralPayload(entry.value) ? isStablePayload(entry.value) ? entry.value : snapshotPayload(entry.value, "inverse") : entry.value
2062
- })));
2063
- if (copy.ids) copy.ids = Object.freeze([...copy.ids]);
2064
- if (copy.positions) copy.positions = Object.freeze([...copy.positions]);
2065
- if ("value" in copy && structuralPayload(copy.value)) {
2066
- const payload = copy;
2067
- payload.value = isStablePayload(payload.value) ? payload.value : snapshotPayload(payload.value, "inverse");
582
+ const lexical = (a, b) => a < b ? -1 : a === b ? 0 : 1;
583
+ //#endregion
584
+ //#region core/src/mutation/session.ts
585
+ var MutationSession = class {
586
+ identity = {};
587
+ generation = 0;
588
+ recorder;
589
+ resolver;
590
+ resolvedRoot;
591
+ constructor(state) {
592
+ this.state = state;
593
+ this.recorder = new ChangeRecorder(state);
594
+ this.resolver = createAddressResolver(state.schema, state.document);
595
+ this.resolvedRoot = state.document;
596
+ }
597
+ validate(node, value, at) {
598
+ const issue = checkValue(node, value, at);
599
+ if (issue) invalidValue(issue);
600
+ }
601
+ resolveContainer(at) {
602
+ return this.resolver.container(this.identity, this.generation, at) ?? fail(at, "invalid-address", "Container does not exist.");
603
+ }
604
+ /** Bind access-resolved canonical facts without walking the address again. */
605
+ bind(at, node, value) {
606
+ return resolveContainer(this.identity, this.generation, at, node, value) ?? fail(at, "invalid-address", "Container does not exist.");
607
+ }
608
+ bindTree(at, node, value) {
609
+ return resolveTreeContainer(this.identity, this.generation, at, node, value) ?? fail(at, "invalid-tree", "Expected a tree.");
610
+ }
611
+ resolveTree(at) {
612
+ const location = resolveValue(this.state.schema, this.state.document, at);
613
+ return this.bindTree(at, location?.node, location?.value);
614
+ }
615
+ replace(at, value) {
616
+ if (!at.length) {
617
+ this.validate(this.state.schema, value, at);
618
+ if (Object.is(this.state.document, value)) return;
619
+ this.recorder.reset();
620
+ this.state.document = copyValue(this.state.schema, value);
621
+ this.invalidate();
622
+ return;
623
+ }
624
+ this.writeMember(this.resolveContainer(at.slice(0, -1)), at[at.length - 1], value, "set");
625
+ }
626
+ assignMember(container, key, value) {
627
+ container = this.refresh(container);
628
+ const member = this.definition(container, key);
629
+ if (container.layout.kind === "fixed" && member.node.kind !== "field" && member.node.kind !== "variant" && !member.node.optional) return fail(container.at.concat(key), "invalid-value", "Replace fields, variants, or collection entries; edit object members individually.");
630
+ this.writeLocatedMember(container, key, member, memberKey(container, key), value, "set");
631
+ }
632
+ removeMember(container, key) {
633
+ this.writeMember(this.refresh(container), key, void 0, "remove");
634
+ }
635
+ refresh(container) {
636
+ if (container.owner !== this.identity) return fail(container.at, "invalid-address", "Container belongs to another mutation session.");
637
+ return container.generation === this.generation ? container : this.resolveContainer(container.at);
638
+ }
639
+ definition(container, key) {
640
+ const layout = container.layout;
641
+ const member = layout.kind === "dynamic" ? layout.entry : layout.members.get(key);
642
+ if (!member) return fail(container.at.concat(key), "invalid-address", "Address does not exist.");
643
+ return member;
644
+ }
645
+ /** Consume a current command-local container; bulk writes retain its storage. */
646
+ writeMember(container, key, value, operation, physicalKey = memberKey(container, key)) {
647
+ const member = this.definition(container, key);
648
+ return this.writeLocatedMember(container, key, member, physicalKey, value, operation);
649
+ }
650
+ /** Location and definition are resolved once by the calling operation. */
651
+ writeLocatedMember(container, key, member, physicalKey, value, operation) {
652
+ const parentNode = container.node;
653
+ const node = member.node;
654
+ const present = operation === "set";
655
+ if (parentNode.kind === "variant" && parentNode.tag === key) return fail(container.at.concat(key), "invalid-value", "Variant discriminants are read-only.");
656
+ const entry = container.layout.kind === "dynamic";
657
+ if (!present && !node.optional && !entry) return fail(container.at.concat(key), "required-field", "A required value cannot be removed.");
658
+ const existed = Object.hasOwn(container.parent, physicalKey);
659
+ const previous = container.parent[physicalKey];
660
+ if (existed === present && (!present || Object.is(previous, value))) return false;
661
+ if (parentNode.kind === "map" || parentNode.kind === "table") {
662
+ const issue = checkKey(parentNode.key, key, []);
663
+ if (issue) return fail(container.at.concat(key, ...issue.address), "invalid-key", issue.message);
664
+ }
665
+ if (present) {
666
+ if (node.kind === "field") {
667
+ const issue = checkValue(node, value, []);
668
+ if (issue) return invalidValue(issue, container.at.concat(key, ...issue.address));
669
+ } else this.validate(node, value, container.at.concat(key));
670
+ if (parentNode.kind === "list" && parentNode.keyOf(value) !== key) return fail(container.at.concat(key), "invalid-list-key", "Replacing an item must retain its addressed key.");
671
+ }
672
+ const membershipChanged = entry && existed !== present;
673
+ if (membershipChanged && (parentNode.kind === "list" || parentNode.kind === "table")) this.recorder.order(container);
674
+ this.recorder.member(container, key, member, physicalKey);
675
+ const next = present ? node.kind === "field" ? value : copyValue(node, value) : void 0;
676
+ installMember(container.parent, physicalKey, present, next);
677
+ if (node.kind !== "field" || membershipChanged) this.invalidate();
678
+ return membershipChanged;
679
+ }
680
+ invalidate() {
681
+ this.generation++;
682
+ if (this.resolvedRoot !== this.state.document) {
683
+ this.resolvedRoot = this.state.document;
684
+ this.resolver = createAddressResolver(this.state.schema, this.state.document);
685
+ } else this.resolver.invalidate();
686
+ }
687
+ finish() {
688
+ return this.recorder.seal();
689
+ }
690
+ rollback() {
691
+ this.recorder.rollback();
692
+ this.invalidate();
2068
693
  }
2069
- return Object.freeze(copy);
2070
694
  };
2071
695
  //#endregion
2072
- //#region core/src/mutation/session.ts
2073
- const createMutationSession = (root, schema, options = {}) => {
2074
- const steps = [];
2075
- const journal = createChangeJournal(root);
2076
- let state = "active";
2077
- const rollback = () => {
2078
- if (state === "rolled-back") return;
2079
- for (let stepIndex = steps.length - 1; stepIndex >= 0; stepIndex -= 1) for (const inverse of steps[stepIndex].inverse) {
2080
- const result = execute(root, inverse, resolveLocated(schema, root, inverse.at), true);
2081
- if (result.status === "rejected") throw new Error(`Document rollback failed: ${result.issue.message}`);
696
+ //#region core/src/mutation/operations/replay.ts
697
+ function apply(session, changes, direction) {
698
+ const side = direction === "forward" ? "after" : "before";
699
+ for (const change of changes.changes) if (change.kind === "reset") session.replace([], change[side]);
700
+ else if (change.kind === "members") {
701
+ const container = session.resolveContainer(change.at);
702
+ const { node, value: current } = container;
703
+ if (change.order) {
704
+ if (node.kind !== "table" && node.kind !== "list") return fail(change.at, "invalid-changes", "Order requires an ordered container.");
705
+ session.recorder.order(container);
706
+ }
707
+ let membershipChanged = false;
708
+ for (const member of change.members) {
709
+ const present = side === "after" ? member.kind !== "removed" : member.kind !== "added";
710
+ const value = side === "after" ? member.kind !== "removed" ? member.after : void 0 : member.kind !== "added" ? member.before : void 0;
711
+ membershipChanged = session.writeMember(container, member.key, value, present ? "set" : "remove") || membershipChanged;
712
+ }
713
+ if (change.order) {
714
+ const keys = node.kind === "table" ? Object.keys(container.parent) : orderOf(node, current);
715
+ if (!matches(change.order[side], keys)) return fail(change.at, "invalid-changes", "Order must contain exactly the resulting keys.");
716
+ installOrder(node, current, change.order[side]);
717
+ session.invalidate();
718
+ } else if (membershipChanged && node.kind === "table") {
719
+ const ids = current.ids;
720
+ if (!matches(ids, Object.keys(container.parent))) return fail(change.at, "invalid-changes", "Table membership changes require a matching order.");
2082
721
  }
2083
- state = "rolled-back";
2084
- };
2085
- return {
2086
- apply: (input) => {
2087
- if (state !== "active") throw new Error("Mutation session is closed.");
2088
- profile.batch.operation();
2089
- const decoded = decode(input);
2090
- if (decoded.status === "rejected") {
2091
- profile.batch.rejected();
2092
- return decoded.issue;
2093
- }
2094
- const normalized = normalize(decoded.operation);
2095
- profile.batch.entry(normalized.type === "entity.create" ? normalized.entries.length : normalized.type === "entity.remove" ? normalized.ids.length : 1);
2096
- const resolved = resolveLocated(schema, root, normalized.at);
2097
- if (resolved?.collection) profile.batch.collectionResolved();
2098
- profile.mutation.executed();
2099
- const result = execute(root, normalized, resolved, options.copyPayload === true || requiresPayloadCopy(normalized));
2100
- if (result.status === "rejected") {
2101
- profile.batch.rejected();
2102
- return result.issue;
2103
- }
2104
- if (result.status === "unchanged") return void 0;
2105
- const published = publish(normalized);
2106
- const group = [];
2107
- for (const rawInverse of result.inverse) {
2108
- const inverse$1 = inverse(rawInverse);
2109
- group.push(inverse$1);
2110
- profile.mutation.inverse();
2111
- profile.batch.inverse();
2112
- }
2113
- steps.push({
2114
- forward: published,
2115
- inverse: Object.freeze(group)
722
+ } else {
723
+ const container = session.resolveTree(change.at);
724
+ const { value: current, node } = container;
725
+ session.recorder.tree(container);
726
+ for (const item of change.nodes) session.recorder.tree(container, item.id);
727
+ const root = change[side];
728
+ if (root === null) delete current.rootId;
729
+ else current.rootId = root;
730
+ for (const item of change.nodes) {
731
+ const next = side === "after" ? item.kind !== "removed" ? item.after : void 0 : item.kind !== "added" ? item.before : void 0;
732
+ installMember(current.nodes, item.id, next !== void 0, next && {
733
+ ...next,
734
+ children: [...next.children]
2116
735
  });
2117
- try {
2118
- if (!resolved) throw new Error(`Changed operation '${normalized.type}' was not resolved.`);
2119
- journal.record(resolved, normalized, result.inverse);
2120
- } catch (error) {
2121
- rollback();
2122
- throw error;
2123
- }
2124
- },
2125
- finish: () => {
2126
- if (state !== "active") throw new Error("Mutation session is closed.");
2127
- const changes = journal.finish();
2128
- state = "finished";
2129
- if (changes.status === "unchanged") return { status: "unchanged" };
2130
- let inverseCount = 0;
2131
- for (const step of steps) inverseCount += step.inverse.length;
2132
- const inverse = new Array(inverseCount);
2133
- let writeIndex = 0;
2134
- for (let stepIndex = steps.length - 1; stepIndex >= 0; stepIndex -= 1) for (const entry of steps[stepIndex].inverse) inverse[writeIndex++] = entry;
2135
- return {
2136
- status: "changed",
2137
- operations: Object.freeze(steps.map((step) => step.forward)),
2138
- inverse: Object.freeze(inverse),
2139
- paths: changes.paths,
2140
- collections: changes.collections
2141
- };
2142
- },
2143
- rollback
2144
- };
2145
- };
2146
- const mutateOperations = (root, schema, operations, options = {}) => {
2147
- const decodedBatch = decodeBatch(operations);
2148
- if (decodedBatch.status === "rejected") return {
2149
- status: "rejected",
2150
- issues: Object.freeze([decodedBatch.issue])
2151
- };
2152
- const session = createMutationSession(root, schema, options);
2153
- for (const input of decodedBatch.operations) {
2154
- const issue = session.apply(input);
2155
- if (!issue) continue;
2156
- session.rollback();
2157
- return {
2158
- status: "rejected",
2159
- issues: Object.freeze([issue])
2160
- };
736
+ }
737
+ session.validate(node, current, change.at);
738
+ session.invalidate();
2161
739
  }
2162
- return session.finish();
2163
- };
740
+ }
2164
741
  //#endregion
2165
742
  //#region core/src/runtime.ts
2166
- var RejectedUpdate = class extends Error {
2167
- issues;
2168
- constructor(issues) {
2169
- super("Document update rejected.");
2170
- this.issues = Object.freeze(issues.slice());
2171
- }
2172
- };
2173
- const EMPTY = Object.freeze([]);
2174
- const publishDiagnostic = (value) => Object.freeze({
2175
- ...value,
2176
- source: "application",
2177
- ...value.address === void 0 ? {} : { address: Object.freeze(value.address.slice()) }
2178
- });
2179
743
  const createDocument = (input) => {
2180
- const initialTreeError = invalidDocument(input.schema, input.initial);
2181
- if (initialTreeError) throw new TypeError(`Initial document contains an invalid tree at '${initialTreeError.join(".")}'.`);
2182
- profile.clone.initialDocument();
744
+ if (input.schema.kind !== "object") throw new TypeError("Document schema must be a root object node.");
745
+ const invalid = checkValue(input.schema, input.initial);
746
+ if (invalid) throw new ParseError(invalid);
2183
747
  const state = {
2184
748
  schema: input.schema,
2185
- document: cloneValue(input.initial, "initial"),
749
+ document: copyValue(input.schema, input.initial),
2186
750
  disposed: false
2187
751
  };
2188
- let revision = 0;
2189
- let busy = false;
2190
- let runtime;
2191
- let notification;
2192
- const assertWritable = (intent) => {
752
+ let revision = 0, busy = false;
753
+ let runtime, notification;
754
+ const idle = () => {
2193
755
  if (state.disposed) throw new DocumentDisposedError();
2194
- if (busy) throw new DocumentReentrancyError();
2195
- if (accessOf(runtime).projectionLocks) throw new DocumentReentrancyError();
2196
- assertRuntimeWritable(runtime, intent);
756
+ if (busy || accessOf(runtime).projectionLocks) throw new DocumentReentrancyError();
2197
757
  };
2198
- const runTransaction = (session, run) => {
2199
- let reports;
2200
- let active = true;
2201
- const transaction = {
2202
- read: documentReader(input.schema, () => state.document, () => active),
2203
- write: documentWriter(input.schema, (operation) => {
2204
- if (!active) throw new Error("Document writer is no longer active.");
2205
- const rejected = session.apply(operation);
2206
- if (rejected) throw new RejectedUpdate([rejected]);
2207
- }),
2208
- reject: (diagnostic) => {
2209
- throw new RejectedUpdate((Array.isArray(diagnostic) ? diagnostic : [diagnostic]).map((entry) => publishDiagnostic(entry)));
2210
- },
2211
- report: (diagnostic) => {
2212
- if (!active) throw new Error("Document transaction is no longer active.");
2213
- (reports ??= []).push(publishDiagnostic(diagnostic));
2214
- }
2215
- };
2216
- try {
2217
- const value = run(transaction);
2218
- if (value !== null && typeof value === "object" && typeof value.then === "function") throw new TypeError("Document update callback must be synchronous.");
2219
- return {
2220
- value,
2221
- reports: Object.freeze(reports ? reports.slice() : EMPTY)
2222
- };
2223
- } finally {
2224
- active = false;
2225
- }
758
+ const writable = (intent) => {
759
+ idle();
760
+ assertRuntimeWritable(runtime, intent);
2226
761
  };
2227
- const impactFor = (batch, kind) => createImpact({
2228
- schema: input.schema,
2229
- operations: batch.operations,
2230
- paths: batch.paths,
2231
- collections: batch.collections,
2232
- reset: kind === "replace"
2233
- });
2234
762
  const history = createHistory({
2235
763
  capacity: input.history === false ? 0 : Math.max(0, input.history?.capacity ?? 100),
2236
764
  revision: () => revision,
2237
- apply: (operations) => applyBatch(operations, "history", false),
2238
- assertIdle: () => {
2239
- if (state.disposed) throw new DocumentDisposedError();
2240
- if (busy || accessOf(runtime).projectionLocks) throw new DocumentReentrancyError();
2241
- },
765
+ apply: (changes, direction) => mutate({
766
+ kind: "apply",
767
+ source: "history"
768
+ }, false, (session) => {
769
+ for (const change of changes) apply(session, change, direction);
770
+ }),
771
+ assertIdle: idle,
2242
772
  notify: (run) => {
2243
773
  busy = true;
2244
774
  try {
@@ -2248,197 +778,112 @@ const createDocument = (input) => {
2248
778
  }
2249
779
  }
2250
780
  });
2251
- const publish = (batch, source, kind, recordHistory) => {
2252
- revision += 1;
2253
- const commit = Object.freeze({
2254
- revision,
2255
- kind,
781
+ const publish = (changes, source, recordHistory) => {
782
+ const commit = {
783
+ revision: ++revision,
2256
784
  source,
2257
- operations: batch.operations,
2258
- inverse: batch.inverse,
2259
- impact: impactFor(batch, kind)
2260
- });
2261
- if (kind === "replace" || source === "remote") history.invalidate();
2262
- else if (recordHistory && (source === "local" || source === "system")) history.record(batch.operations, batch.inverse);
785
+ changes,
786
+ impact: createImpact(input.schema, changes)
787
+ };
788
+ if (source === "remote") history.invalidate();
789
+ else if (recordHistory && (source === "local" || source === "system")) history.record(changes);
2263
790
  else if (source === "history") history.publish();
2264
791
  else history.endGroup();
2265
- busy = true;
2266
- let observerErrors;
2267
- try {
2268
- observerErrors = notify(notification, commit, history.flush);
2269
- } finally {
2270
- busy = false;
2271
- }
2272
792
  return {
2273
793
  status: "committed",
2274
794
  commit,
2275
- observerErrors
795
+ observerErrors: notify(notification, commit, history.flush)
2276
796
  };
2277
797
  };
2278
- function applyBatch(operations, source, recordHistory) {
2279
- assertWritable({
2280
- kind: "apply",
2281
- source
2282
- });
2283
- const batch = mutateOperations(state.document, input.schema, operations, { copyPayload: source === "history" });
2284
- if (batch.status === "rejected") return {
2285
- status: "rejected",
2286
- issues: batch.issues,
2287
- revision
2288
- };
2289
- if (batch.status === "unchanged") return {
2290
- status: "unchanged",
2291
- revision
2292
- };
2293
- return publish(batch, source, "operations", recordHistory);
798
+ function mutate(intent, recordHistory, execute) {
799
+ writable(intent);
800
+ busy = true;
801
+ try {
802
+ const session = new MutationSession(state);
803
+ let changes;
804
+ try {
805
+ execute(session);
806
+ changes = session.finish();
807
+ } catch (error) {
808
+ session.rollback();
809
+ if (error instanceof MutationRejected) return {
810
+ status: "rejected",
811
+ issues: [error.issue],
812
+ revision
813
+ };
814
+ if (intent.kind === "update" && error instanceof TransactionRejected) return {
815
+ status: "rejected",
816
+ issues: error.issues,
817
+ revision
818
+ };
819
+ throw error;
820
+ }
821
+ if (!changes.changes.length) return {
822
+ status: "unchanged",
823
+ revision
824
+ };
825
+ return publish(changes, intent.source, recordHistory);
826
+ } finally {
827
+ busy = false;
828
+ }
2294
829
  }
2295
830
  runtime = {
2296
831
  schema: input.schema,
2297
832
  address: {
2298
- resolve: (address) => resolveAddress(input.schema, address, state.document),
2299
- read: (address) => read(state.document, address),
2300
- contains: contains$1,
833
+ resolve: (at) => resolveAddress(input.schema, at, state.document),
834
+ read: (at) => read(state.document, at, input.schema),
835
+ contains,
2301
836
  overlaps,
2302
837
  debugKey
2303
838
  },
2304
839
  revision: () => revision,
2305
840
  update: (run, options) => {
2306
841
  const source = options?.source ?? "local";
2307
- assertWritable({
842
+ let value;
843
+ const result = mutate({
2308
844
  kind: "update",
2309
845
  source
846
+ }, options?.history ?? true, (session) => {
847
+ value = run(createAccess({
848
+ state,
849
+ session
850
+ }));
851
+ if (value !== null && (typeof value === "object" || typeof value === "function") && typeof value.then === "function") throw new TypeError("Document updates must be synchronous.");
2310
852
  });
2311
- busy = true;
2312
- const session = createMutationSession(state.document, input.schema);
2313
- let committed = false;
2314
- try {
2315
- const { value, reports } = runTransaction(session, run);
2316
- const batch = session.finish();
2317
- if (batch.status === "unchanged") return {
2318
- status: "unchanged",
2319
- value,
2320
- revision,
2321
- reports
2322
- };
2323
- if (batch.status === "rejected") return {
2324
- status: "rejected",
2325
- issues: batch.issues,
2326
- revision
2327
- };
2328
- const result = publish(batch, source, "operations", options?.history ?? true);
2329
- committed = true;
2330
- return {
2331
- status: "committed",
2332
- value,
2333
- commit: result.commit,
2334
- reports,
2335
- observerErrors: result.observerErrors
2336
- };
2337
- } catch (error) {
2338
- try {
2339
- if (!committed) session.rollback();
2340
- } finally {
2341
- busy = false;
2342
- }
2343
- if (error instanceof RejectedUpdate) return {
2344
- status: "rejected",
2345
- issues: error.issues,
2346
- revision
2347
- };
2348
- throw error;
2349
- } finally {
2350
- busy = false;
2351
- }
2352
- },
2353
- prepare: (run) => {
2354
- assertWritable({ kind: "prepare" });
2355
- busy = true;
2356
- const session = createMutationSession(state.document, input.schema);
2357
- try {
2358
- const { value, reports } = runTransaction(session, run);
2359
- const batch = session.finish();
2360
- if (batch.status === "unchanged") {
2361
- session.rollback();
2362
- return {
2363
- status: "unchanged",
2364
- value,
2365
- reports
2366
- };
2367
- }
2368
- if (batch.status === "rejected") {
2369
- session.rollback();
2370
- return {
2371
- status: "rejected",
2372
- issues: batch.issues
2373
- };
2374
- }
2375
- const impact = impactFor(batch, "operations");
2376
- session.rollback();
2377
- return {
2378
- status: "prepared",
2379
- value,
2380
- operations: batch.operations,
2381
- inverse: batch.inverse,
2382
- impact,
2383
- footprint: commandFootprint(batch.operations),
2384
- reports
2385
- };
2386
- } catch (error) {
2387
- try {
2388
- session.rollback();
2389
- } finally {
2390
- busy = false;
2391
- }
2392
- if (error instanceof RejectedUpdate) return {
2393
- status: "rejected",
2394
- issues: error.issues
2395
- };
2396
- throw error;
2397
- } finally {
2398
- busy = false;
2399
- }
853
+ return result.status === "rejected" ? result : {
854
+ ...result,
855
+ value
856
+ };
2400
857
  },
2401
- apply: (operations, options) => applyBatch(operations, options?.source ?? "local", options?.history ?? true),
2402
- replace: (document, options) => {
858
+ apply: (inputChanges, options) => mutate({
859
+ kind: "apply",
860
+ source: options?.source ?? "local"
861
+ }, options?.history ?? true, (session) => {
862
+ if (!options || options.expectedRevision !== revision) fail([], "baseline-mismatch", "apply requires the current expectedRevision.");
863
+ apply(session, decodeChanges(inputChanges), "forward");
864
+ }),
865
+ replace: (value, options) => {
2403
866
  const source = options?.source ?? "system";
2404
- assertWritable({
867
+ return mutate({
2405
868
  kind: "replace",
2406
869
  source
2407
- });
2408
- const invalidTree = invalidDocument(input.schema, document);
2409
- if (invalidTree) return {
2410
- status: "rejected",
2411
- issues: [at(invalidTree, "invalid-tree", "Document replacement contains an invalid tree.")],
2412
- revision
2413
- };
2414
- if (deepEqual(state.document, document)) return {
2415
- status: "unchanged",
2416
- revision
2417
- };
2418
- state.document = cloneValue(document, "replace");
2419
- return publish({
2420
- status: "changed",
2421
- operations: EMPTY,
2422
- inverse: EMPTY,
2423
- paths: EMPTY,
2424
- collections: EMPTY
2425
- }, source, "replace", false);
870
+ }, source !== "remote", (session) => session.replace([], value));
2426
871
  },
2427
872
  snapshot: () => {
2428
873
  if (state.disposed) throw new DocumentDisposedError();
2429
- return cloneValue(state.document, "snapshot");
874
+ return copyValue(input.schema, state.document);
2430
875
  },
2431
- subscribe: ((targetOrListener, listener) => {
876
+ subscribe: ((pick, listener) => {
2432
877
  if (state.disposed) throw new DocumentDisposedError();
2433
- if (typeof targetOrListener === "function") return subscribeRoot(notification, targetOrListener);
2434
- const targets = Array.isArray(targetOrListener) ? targetOrListener : [targetOrListener];
2435
- if (targets.length === 0 || !listener) throw new TypeError("Filtered subscribe requires at least one target and a listener.");
2436
- return subscribeTargets(notification, targets, listener);
878
+ if (!listener) return subscribeRoot(notification, pick);
879
+ const picks = Array.isArray(pick) ? pick : [pick];
880
+ if (!picks.length) throw new TypeError("Expected at least one subscription path.");
881
+ return subscribeTargets(notification, picks.map((p) => compilePath(input.schema, "value", p)), listener);
2437
882
  }),
2438
883
  history: history.api,
2439
884
  dispose: () => {
2440
885
  if (state.disposed) return;
2441
- if (busy || accessOf(runtime).projectionLocks) throw new DocumentReentrancyError();
886
+ idle();
2442
887
  state.disposed = true;
2443
888
  try {
2444
889
  disposeNotification(notification);
@@ -2470,6 +915,6 @@ const asReadable = (runtime) => {
2470
915
  //#region core/src/projection/select.ts
2471
916
  const select = (runtime, selector) => readWith(runtime, selector);
2472
917
  //#endregion
2473
- export { DocumentDisposedError, DocumentReentrancyError, ProjectionDisposedError, ProjectionError, asReadable, createDocument, createProjectionRuntime, dict, field, list, map, object, optional, schema, select, table, impact_target_exports as target, tree, variant };
918
+ export { DocumentDisposedError, DocumentReentrancyError, ParseError, ProjectionDisposedError, ProjectionError, TransactionRejected, asReadable, assign, createDocument, createProjectionStore, field, input, list, map, object, optional, parse, project, select, snapshot, table, tree, variant };
2474
919
 
2475
920
  //# sourceMappingURL=index.js.map