doxum 0.1.5 → 0.1.7

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 (53) hide show
  1. package/README.md +75 -9
  2. package/dist/{contract-4E3moCbY.d.ts → contract-DNZ4D53r.d.ts} +87 -76
  3. package/dist/{contract-uYJTmT8o.d.cts → contract-j3SLGAwh.d.cts} +87 -76
  4. package/dist/index.cjs +284 -947
  5. package/dist/index.cjs.map +1 -1
  6. package/dist/index.d.cts +3 -64
  7. package/dist/index.d.ts +3 -64
  8. package/dist/index.js +218 -872
  9. package/dist/index.js.map +1 -1
  10. package/dist/integration-B56u1l9V.js +30 -0
  11. package/dist/integration-B56u1l9V.js.map +1 -0
  12. package/dist/integration-CZwCwFBS.cjs +35 -0
  13. package/dist/integration-CZwCwFBS.cjs.map +1 -0
  14. package/dist/integration.cjs +12 -14
  15. package/dist/integration.d.cts +3 -2
  16. package/dist/integration.d.ts +3 -2
  17. package/dist/integration.js +3 -14
  18. package/dist/local-sync.cjs +57 -18
  19. package/dist/local-sync.cjs.map +1 -1
  20. package/dist/local-sync.d.cts +2 -2
  21. package/dist/local-sync.d.ts +2 -2
  22. package/dist/local-sync.js +57 -18
  23. package/dist/local-sync.js.map +1 -1
  24. package/dist/{ownership-CU-9DKJQ.cjs → ownership-CY0nPXGF.cjs} +4 -1
  25. package/dist/ownership-CY0nPXGF.cjs.map +1 -0
  26. package/dist/{ownership-B-VAPcce.js → ownership-CduRygE7.js} +4 -1
  27. package/dist/ownership-CduRygE7.js.map +1 -0
  28. package/dist/react.cjs +27 -19
  29. package/dist/react.cjs.map +1 -1
  30. package/dist/react.js +27 -19
  31. package/dist/react.js.map +1 -1
  32. package/dist/runtime-0mOFbe_H.cjs +2439 -0
  33. package/dist/runtime-0mOFbe_H.cjs.map +1 -0
  34. package/dist/runtime-B22tuj9A.d.cts +150 -0
  35. package/dist/runtime-BFmhzPpZ.d.ts +150 -0
  36. package/dist/runtime-DF1q9Gje.js +2129 -0
  37. package/dist/runtime-DF1q9Gje.js.map +1 -0
  38. package/package.json +1 -1
  39. package/skills/doxum-runtime/SKILL.md +2 -1
  40. package/skills/doxum-runtime/references/guide.en.md +38 -26
  41. package/skills/doxum-runtime/references/guide.zh-CN.md +24 -21
  42. package/skills/doxum-runtime/references/invariants.en.md +13 -10
  43. package/skills/doxum-runtime/references/invariants.zh-CN.md +3 -3
  44. package/skills/doxum-runtime/references/patterns.en.md +26 -64
  45. package/skills/doxum-runtime/references/patterns.zh-CN.md +15 -52
  46. package/dist/dependency-C5_R1tvQ.js +0 -496
  47. package/dist/dependency-C5_R1tvQ.js.map +0 -1
  48. package/dist/dependency-Dfzs3khk.cjs +0 -722
  49. package/dist/dependency-Dfzs3khk.cjs.map +0 -1
  50. package/dist/integration.cjs.map +0 -1
  51. package/dist/integration.js.map +0 -1
  52. package/dist/ownership-B-VAPcce.js.map +0 -1
  53. package/dist/ownership-CU-9DKJQ.cjs.map +0 -1
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import { A as optional, C as resolveLocated, D as list, E as field, F as tree, I as variant, M as schema, N as single, O as map, P as table, S as resolveAddress, T as dict, _ as debugKey, a as readWith, b as overlaps, c as DocumentReentrancyError, d as address$2, f as belongs, g as contains, h as impact_target_exports, i as documentOf, j as record, k as object, l as documentReader, m as id, n as accessOf, o as schemaOf, p as bucket, r as bindRuntimeAccess, s as DocumentDisposedError, t as createDependencyTracker, u as readerFor, v as locate, w as set$1, x as read, y as nodeAt$1 } from "./dependency-C5_R1tvQ.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-B-VAPcce.js";
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
3
  import { n as bindRuntimeDriver, r as disposeRuntimeDriver, t as assertRuntimeWritable } from "./driver-CbzfW5MR.js";
4
4
  //#region core/src/impact.ts
5
5
  const emptyCollectionImpact = Object.freeze({
@@ -38,9 +38,9 @@ const createImpact = (input) => {
38
38
  profile.impact.affects();
39
39
  if (kind === "reset") return true;
40
40
  if (!belongs(value, input.schema)) return false;
41
- if (pathsAffect(address$2(value))) return true;
41
+ if (pathsAffect(address$1(value))) return true;
42
42
  if (value.kind === "collection") {
43
- const collection = findCollection(address$2(value));
43
+ const collection = findCollection(address$1(value));
44
44
  if (!collection) return false;
45
45
  const id$1 = id(value);
46
46
  if (id$1 !== void 0) return collection.added.has(id$1) || collection.removed.has(id$1) || collection.updated.has(id$1);
@@ -54,7 +54,7 @@ const createImpact = (input) => {
54
54
  const cached = collectionCache?.find((entry) => entry.address.length === selector.address.length && entry.address.every((segment, index) => segment === selector.address[index]))?.value;
55
55
  if (cached) return cached;
56
56
  const exact = findCollection(selector.address);
57
- if (somePath(selector.address, (changed) => contains(changed, selector.address) && changed.length < selector.address.length)) {
57
+ if (somePath(selector.address, (changed) => contains$1(changed, selector.address) && changed.length < selector.address.length)) {
58
58
  const reset = { kind: "reset" };
59
59
  (collectionCache ??= []).push({
60
60
  address: selector.address,
@@ -88,78 +88,170 @@ const createImpact = (input) => {
88
88
  //#endregion
89
89
  //#region core/src/history.ts
90
90
  const createHistory = (input) => {
91
- const undos = [];
92
- const redos = [];
91
+ let undos = [];
92
+ let redos = [];
93
93
  const listeners = /* @__PURE__ */ new Set();
94
94
  let state = Object.freeze({
95
95
  undoDepth: 0,
96
96
  redoDepth: 0
97
97
  });
98
- const current = () => {
99
- if (state.undoDepth !== undos.length || state.redoDepth !== redos.length) state = Object.freeze({
98
+ let revision = 0;
99
+ let pending = false;
100
+ let disposed = false;
101
+ let group;
102
+ const unchanged = () => ({
103
+ status: "unchanged",
104
+ revision: input.revision()
105
+ });
106
+ const publish = () => {
107
+ if (state.undoDepth === undos.length && state.redoDepth === redos.length) return;
108
+ state = Object.freeze({
100
109
  undoDepth: undos.length,
101
110
  redoDepth: redos.length
102
111
  });
103
- return state;
104
- };
105
- const emit = () => {
106
- if (listeners.size === 0) return;
107
- current();
108
- Array.from(listeners).forEach((listener) => listener());
112
+ revision++;
113
+ pending = true;
114
+ };
115
+ const flush = () => {
116
+ if (!pending) return [];
117
+ pending = false;
118
+ const errors = [];
119
+ for (const listener of [...listeners]) {
120
+ if (!listeners.has(listener)) continue;
121
+ try {
122
+ listener();
123
+ } catch (error) {
124
+ errors.push({
125
+ phase: "listener",
126
+ error
127
+ });
128
+ }
129
+ }
130
+ return errors;
109
131
  };
110
132
  const clear = () => {
111
- if (undos.length || redos.length) {
112
- undos.length = 0;
113
- redos.length = 0;
114
- emit();
133
+ group = void 0;
134
+ undos.length = redos.length = 0;
135
+ publish();
136
+ };
137
+ const travel = (direction) => {
138
+ input.assertIdle();
139
+ const from = direction === "redo" ? redos : undos;
140
+ const to = direction === "redo" ? undos : redos;
141
+ const previousUndos = undos;
142
+ const previousRedos = redos;
143
+ const entry = direction === "cancel" ? from.at(-1) : from.pop();
144
+ if (!entry) return unchanged();
145
+ if (direction === "cancel") {
146
+ if (!group || group.entry !== entry) throw new Error("History group no longer owns its entry.");
147
+ undos = group.undos;
148
+ redos = group.redos;
149
+ } else to.push(entry);
150
+ let committed = false;
151
+ 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);
154
+ committed = result.status !== "rejected";
155
+ if (committed && direction === "cancel") group = void 0;
156
+ if (result.status === "unchanged") {
157
+ publish();
158
+ const errors = input.notify(flush);
159
+ if (errors.length) throw new AggregateError(errors.map((entry) => entry.error), "History listeners failed.");
160
+ }
161
+ return result;
162
+ } finally {
163
+ if (!committed) if (direction === "cancel") {
164
+ undos = previousUndos;
165
+ redos = previousRedos;
166
+ } else {
167
+ to.pop();
168
+ from.push(entry);
169
+ }
115
170
  }
116
171
  };
117
172
  return {
118
173
  api: {
119
- current,
174
+ current: () => state,
175
+ revision: () => revision,
120
176
  subscribe: (listener) => {
177
+ if (disposed) throw new DocumentDisposedError();
121
178
  listeners.add(listener);
122
- return () => listeners.delete(listener);
179
+ return () => {
180
+ listeners.delete(listener);
181
+ };
123
182
  },
124
183
  undo: () => {
125
- const entry = undos.pop();
126
- if (!entry) return {
127
- status: "unchanged",
128
- revision: input.revision()
129
- };
130
- const result = input.apply(entry.inverse);
131
- if (result.status === "committed") {
132
- redos.push(entry);
133
- emit();
134
- } else undos.push(entry);
135
- return result;
184
+ input.assertIdle();
185
+ group = void 0;
186
+ return travel("undo");
136
187
  },
137
188
  redo: () => {
138
- const entry = redos.pop();
139
- if (!entry) return {
140
- status: "unchanged",
141
- revision: input.revision()
142
- };
143
- const result = input.apply(entry.operations);
144
- if (result.status === "committed") {
145
- undos.push(entry);
146
- emit();
147
- } else redos.push(entry);
148
- return result;
189
+ input.assertIdle();
190
+ group = void 0;
191
+ return travel("redo");
149
192
  },
150
- clear
193
+ clear: () => {
194
+ input.assertIdle();
195
+ clear();
196
+ const errors = input.notify(flush);
197
+ if (errors.length) throw new AggregateError(errors.map((entry) => entry.error), "History listeners failed.");
198
+ },
199
+ group: () => {
200
+ input.assertIdle();
201
+ if (group) throw new Error("A history group is already active.");
202
+ if (input.capacity <= 0) throw new Error("History is disabled.");
203
+ const scope = {
204
+ undos: undos.slice(),
205
+ redos: redos.slice()
206
+ };
207
+ group = scope;
208
+ return Object.freeze({
209
+ end: () => {
210
+ input.assertIdle();
211
+ if (group === scope) group = void 0;
212
+ },
213
+ cancel: () => {
214
+ input.assertIdle();
215
+ if (group !== scope) return unchanged();
216
+ if (!scope.entry) {
217
+ group = void 0;
218
+ return unchanged();
219
+ }
220
+ const result = travel("cancel");
221
+ if (result.status !== "rejected") group = void 0;
222
+ return result;
223
+ }
224
+ });
225
+ }
151
226
  },
152
227
  record: (operations, inverse) => {
153
228
  if (input.capacity <= 0) return;
154
- undos.push({
229
+ const batch = {
155
230
  operations,
156
231
  inverse
157
- });
158
- if (undos.length > input.capacity) undos.shift();
232
+ };
233
+ if (group?.entry) group.entry.batches.push(batch);
234
+ else {
235
+ const entry = { batches: [batch] };
236
+ undos.push(entry);
237
+ if (group) group.entry = entry;
238
+ if (undos.length > input.capacity) undos.shift();
239
+ }
159
240
  redos.length = 0;
160
- emit();
241
+ publish();
242
+ },
243
+ publish,
244
+ flush,
245
+ endGroup: () => {
246
+ group = void 0;
161
247
  },
162
- invalidate: clear
248
+ invalidate: clear,
249
+ dispose: () => {
250
+ disposed = true;
251
+ listeners.clear();
252
+ clear();
253
+ pending = false;
254
+ }
163
255
  };
164
256
  };
165
257
  //#endregion
@@ -179,7 +271,7 @@ const writerFor = (node, inputAddress, sink) => {
179
271
  at: address
180
272
  }) : void 0
181
273
  };
182
- if (node.kind === "dict" || node.kind === "record") return {
274
+ if (node.kind === "dict") return {
183
275
  set: (key, value) => emit(sink, {
184
276
  type: "dict.set",
185
277
  at: address,
@@ -232,20 +324,20 @@ const writerFor = (node, inputAddress, sink) => {
232
324
  }) } : {}
233
325
  };
234
326
  if (node.kind === "tree") return {
235
- insert: (treeNodeId, value, parentId, index) => emit(sink, {
327
+ insert: (treeNodeId, value, position) => emit(sink, {
236
328
  type: "tree.insert",
237
329
  at: address,
238
330
  treeNodeId,
239
331
  value,
240
- parentId,
241
- index
332
+ parentId: position?.parentId,
333
+ index: position?.index
242
334
  }),
243
- move: (treeNodeId, parentId, index) => emit(sink, {
335
+ move: (treeNodeId, position) => emit(sink, {
244
336
  type: "tree.move",
245
337
  at: address,
246
338
  treeNodeId,
247
- parentId,
248
- index
339
+ parentId: position?.parentId,
340
+ index: position?.index
249
341
  }),
250
342
  remove: (treeNodeId) => emit(sink, {
251
343
  type: "tree.remove",
@@ -329,7 +421,6 @@ const writerFor = (node, inputAddress, sink) => {
329
421
  })
330
422
  };
331
423
  }
332
- if (node.kind === "single") return writerFor(node.value, address, sink);
333
424
  if (node.kind === "variant") return {
334
425
  replace: (value) => emit(sink, {
335
426
  type: "variant.replace",
@@ -355,69 +446,15 @@ const documentWriter = (schema, sink) => writerFor(schemaRoot(schema), [], sink)
355
446
  //#endregion
356
447
  //#region core/src/mutation/issue.ts
357
448
  const ownAddress$1 = (address) => Object.freeze(address.slice());
358
- const at$1 = (address, code, message, operation) => Object.freeze({
449
+ const at = (address, code, message, operation) => Object.freeze({
359
450
  source: "mutation",
360
451
  code,
361
452
  address: ownAddress$1(address),
362
453
  message,
363
454
  ...operation === void 0 ? {} : { operation }
364
455
  });
365
- const from = (operation, code, message) => at$1(operation.at, code, message, operation.type);
366
- const input = (code, message) => at$1([], code, message);
367
- //#endregion
368
- //#region core/src/mutation/anchor.ts
369
- const length = (keys) => keys.length;
370
- const keyAt = (keys, index) => Array.isArray(keys) ? keys[index] : keys.at(index);
371
- const keyIndex = (keys, key) => Array.isArray(keys) ? keys.indexOf(key) : keys.index(key);
372
- const keys = (values, keyOf) => ({
373
- length: values.length,
374
- at: (index) => index >= 0 && index < values.length ? keyOf(values[index]) : void 0,
375
- index: (key) => {
376
- for (let index = 0; index < values.length; index += 1) if (keyOf(values[index]) === key) return index;
377
- return -1;
378
- }
379
- });
380
- const index = (keys, anchor) => {
381
- if (!anchor) return length(keys);
382
- if ("at" in anchor) return anchor.at === "start" ? 0 : length(keys);
383
- if ("before" in anchor) return keyIndex(keys, anchor.before);
384
- return keyIndex(keys, anchor.after) + 1;
385
- };
386
- const at = (keys, position) => position <= 0 ? { at: "start" } : { after: keyAt(keys, position - 1) };
387
- const valid = (keys, anchor) => {
388
- if (!anchor) return true;
389
- if (typeof anchor !== "object" || anchor === null) return false;
390
- if (Number("at" in anchor) + Number("before" in anchor) + Number("after" in anchor) !== 1) return false;
391
- if ("at" in anchor) return anchor.at === "start" || anchor.at === "end";
392
- return keyIndex(keys, "before" in anchor ? anchor.before : anchor.after) >= 0;
393
- };
394
- const afterRemove = (keys, removedIndex, anchor) => {
395
- const position = index(keys, anchor);
396
- return position > removedIndex ? position - 1 : position;
397
- };
398
- const validPositions = (length, positions) => {
399
- let previous = -1;
400
- for (let index = 0; index < positions.length; index += 1) {
401
- const position = positions[index];
402
- if (!Number.isInteger(position) || position < 0 || position <= previous || position > length + index) return false;
403
- previous = position;
404
- }
405
- return true;
406
- };
407
- const restore = (keys, inserted, positions) => {
408
- const currentLength = keys.length;
409
- if (inserted.length !== positions.length || !validPositions(currentLength, positions)) throw new Error("Ordered restore positions are invalid.");
410
- let readIndex = currentLength - 1;
411
- let insertedIndex = inserted.length - 1;
412
- keys.length = currentLength + inserted.length;
413
- for (let writeIndex = keys.length - 1; writeIndex >= 0; writeIndex -= 1) if (insertedIndex >= 0 && positions[insertedIndex] === writeIndex) {
414
- keys[writeIndex] = inserted[insertedIndex];
415
- insertedIndex -= 1;
416
- } else {
417
- keys[writeIndex] = keys[readIndex];
418
- readIndex -= 1;
419
- }
420
- };
456
+ const from = (operation, code, message) => at(operation.at, code, message, operation.type);
457
+ const input = (code, message) => at([], code, message);
421
458
  //#endregion
422
459
  //#region core/src/mutation/execute-entity.ts
423
460
  const resolveEntityCollection = (node, value) => {
@@ -542,7 +579,7 @@ const executeEntity = (target, operation, copyPayload) => {
542
579
  type: "entity.move",
543
580
  at: operation.at,
544
581
  id: operation.id,
545
- anchor: at(ids, position)
582
+ anchor: at$1(ids, position)
546
583
  };
547
584
  ids.splice(position, 1);
548
585
  ids.splice(nextIndex, 0, operation.id);
@@ -617,7 +654,7 @@ const executeList = (target, operation, copyPayload) => {
617
654
  at: operation.at,
618
655
  key: operation.key,
619
656
  value: cloneValue(list[position], "inverse"),
620
- anchor: at(orderedKeys, position)
657
+ anchor: at$1(orderedKeys, position)
621
658
  };
622
659
  list.splice(position, 1);
623
660
  return {
@@ -633,7 +670,7 @@ const executeList = (target, operation, copyPayload) => {
633
670
  type: "list.move",
634
671
  at: operation.at,
635
672
  key: operation.key,
636
- anchor: at(orderedKeys, position)
673
+ anchor: at$1(orderedKeys, position)
637
674
  };
638
675
  const [entry] = list.splice(position, 1);
639
676
  list.splice(nextIndex, 0, entry);
@@ -816,7 +853,7 @@ const changeChanged = (change, value) => {
816
853
  return false;
817
854
  };
818
855
  const is = (value) => isRecord(value) && isRecord(value.nodes);
819
- const contains$1 = (tree, id) => has$1(tree.nodes, id);
856
+ const contains = (tree, id) => has$1(tree.nodes, id);
820
857
  const validate = (value) => {
821
858
  if (!is(value)) return false;
822
859
  const ids = Object.keys(value.nodes);
@@ -858,7 +895,6 @@ const documentTreeError = (node, value, address) => {
858
895
  }
859
896
  return;
860
897
  }
861
- if (node.kind === "single") return documentTreeError(node.value, value, address);
862
898
  if (node.kind === "variant") {
863
899
  if (!isRecord(value) || typeof value[node.tag] !== "string") return void 0;
864
900
  const variant = node.variants[value[node.tag]];
@@ -897,7 +933,7 @@ const replace = (current, operation) => {
897
933
  };
898
934
  };
899
935
  const insert = (tree, operation, value) => {
900
- if (contains$1(tree, operation.treeNodeId)) return rejected$2("duplicate-tree-node", `Tree node '${operation.treeNodeId}' already exists.`);
936
+ if (contains(tree, operation.treeNodeId)) return rejected$2("duplicate-tree-node", `Tree node '${operation.treeNodeId}' already exists.`);
901
937
  if (!validIndex(operation.index)) return rejected$2("invalid-tree-index", "Tree index is invalid.");
902
938
  if (tree.rootId === void 0) {
903
939
  if (operation.parentId !== void 0) return rejected$2("missing-tree-parent", `Tree parent '${operation.parentId}' does not exist.`);
@@ -1022,9 +1058,8 @@ const move = (tree, operation) => {
1022
1058
  if (!currentParent || parentId === void 0) return rejected$2("invalid-tree", "Tree parent links are inconsistent.");
1023
1059
  const originalIndex = childIndex(currentParent, operation.treeNodeId);
1024
1060
  if (originalIndex < 0) return rejected$2("invalid-tree", "Tree parent links are inconsistent.");
1025
- let nextIndex = Math.min(operation.index ?? nextParent.children.length, nextParent.children.length);
1026
1061
  currentParent.children.splice(originalIndex, 1);
1027
- if (parentId === operation.parentId && nextIndex > originalIndex) nextIndex -= 1;
1062
+ const nextIndex = Math.min(operation.index ?? nextParent.children.length, nextParent.children.length);
1028
1063
  if (parentId === operation.parentId && nextIndex === originalIndex) {
1029
1064
  currentParent.children.splice(originalIndex, 0, operation.treeNodeId);
1030
1065
  return { status: "unchanged" };
@@ -1128,6 +1163,16 @@ const executeValue = (target, operation, copyPayload) => {
1128
1163
  }
1129
1164
  if (operation.type === "variant.replace") {
1130
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
+ }
1131
1176
  const inverse = {
1132
1177
  type: "variant.replace",
1133
1178
  at: operation.at,
@@ -1206,7 +1251,7 @@ const executeClear = (target, operation) => {
1206
1251
  issue: from(operation, "required-field", "Only optional values can be cleared.")
1207
1252
  };
1208
1253
  if (!hasOwn$1(target.parent, target.key)) return { status: "unchanged" };
1209
- if (target.node.kind !== "field" && target.node.kind !== "variant" && target.node.kind !== "dict" && target.node.kind !== "record" && target.node.kind !== "list" && target.node.kind !== "tree") return {
1254
+ if (target.node.kind !== "field" && target.node.kind !== "variant" && target.node.kind !== "dict" && target.node.kind !== "list" && target.node.kind !== "tree") return {
1210
1255
  status: "rejected",
1211
1256
  issue: from(operation, "invalid-operation", "This optional value has no clear operation.")
1212
1257
  };
@@ -1222,8 +1267,7 @@ const executeClear = (target, operation) => {
1222
1267
  at: operation.at,
1223
1268
  value: cloneValue(target.value, "inverse")
1224
1269
  };
1225
- case "dict":
1226
- case "record": return {
1270
+ case "dict": return {
1227
1271
  type: "dict.replace",
1228
1272
  at: operation.at,
1229
1273
  value: cloneValue(target.value, "inverse")
@@ -1262,7 +1306,7 @@ const execute = (root, operation, target, copyPayload) => {
1262
1306
  if (operation.type === "value.clear") return !target ? invalidTarget(operation) : executeClear(target, operation);
1263
1307
  if (operation.type === "field.set" || operation.type === "field.clear") return !target || target.node.kind !== "field" ? invalidTarget(operation) : executeValue(target, operation, copyPayload);
1264
1308
  if (operation.type === "variant.replace") return !target || target.node.kind !== "variant" ? invalidTarget(operation) : executeValue(target, operation, copyPayload);
1265
- if (operation.type === "dict.set" || operation.type === "dict.delete" || operation.type === "dict.replace") return !target || target.node.kind !== "dict" && target.node.kind !== "record" ? 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);
1266
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);
1267
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);
1268
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);
@@ -1275,7 +1319,7 @@ const execute = (root, operation, target, copyPayload) => {
1275
1319
  //#region core/src/mutation/journal.ts
1276
1320
  const indexNode = () => ({ children: /* @__PURE__ */ new Map() });
1277
1321
  const hasOwn = (value, key) => Object.prototype.hasOwnProperty.call(value, key);
1278
- const sameAddress$1 = (left, right) => {
1322
+ const sameAddress = (left, right) => {
1279
1323
  if (left.length !== right.length) return false;
1280
1324
  for (let index = 0; index < left.length; index += 1) if (left[index] !== right[index]) return false;
1281
1325
  return true;
@@ -1374,7 +1418,7 @@ const restoreEntity = (value, subject) => {
1374
1418
  };
1375
1419
  const restoreRootSubject = (value, subject) => {
1376
1420
  if (subject.kind === "value") return subject.before.status === "present" ? subject.before.value : void 0;
1377
- if (subject.kind === "variant") return subject.before;
1421
+ if (subject.kind === "variant") return subject.before.status === "present" ? subject.before.value : void 0;
1378
1422
  if (subject.kind === "dict") {
1379
1423
  if (isRecord(value)) restoreDict(value, subject.state);
1380
1424
  return value;
@@ -1390,14 +1434,14 @@ const restoreRootSubject = (value, subject) => {
1390
1434
  return restoreChange(value, subject.state);
1391
1435
  };
1392
1436
  const restoreSubject = (value, prefix, subject) => {
1393
- if (sameAddress$1(prefix, subject.address)) return restoreRootSubject(value, subject);
1437
+ if (sameAddress(prefix, subject.address)) return restoreRootSubject(value, subject);
1394
1438
  const address = subject.address.slice(prefix.length);
1395
1439
  if (subject.kind === "value") {
1396
1440
  writePresence(value, address, subject.before);
1397
1441
  return value;
1398
1442
  }
1399
1443
  if (subject.kind === "variant") {
1400
- writeContainer(value, address, subject.before);
1444
+ writePresence(value, address, subject.before);
1401
1445
  return value;
1402
1446
  }
1403
1447
  const current = read(value, address);
@@ -1498,7 +1542,11 @@ const analyzeEntity = (root, subject) => {
1498
1542
  };
1499
1543
  const subjectChanged = (root, subject) => {
1500
1544
  if (subject.kind === "value") return valueChanged(root, subject);
1501
- if (subject.kind === "variant") return !sameStructuralValue(subject.before, read(root, subject.address));
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
+ }
1502
1550
  if (subject.kind === "dict") return dictChanged(root, subject);
1503
1551
  if (subject.kind === "list") return listChanged(root, subject);
1504
1552
  return changeChanged(subject.state, read(root, subject.address));
@@ -1617,7 +1665,7 @@ const createChangeJournal = (root) => {
1617
1665
  const candidates = buckets.get(addressHash) ?? [];
1618
1666
  for (const subject of candidates) {
1619
1667
  profile.journal.comparison();
1620
- if (subject.kind === kind && sameAddress$1(subject.address, address)) return subject;
1668
+ if (subject.kind === kind && sameAddress(subject.address, address)) return subject;
1621
1669
  }
1622
1670
  }
1623
1671
  const ownerOf = (address) => {
@@ -1660,11 +1708,11 @@ const createChangeJournal = (root) => {
1660
1708
  const recordVariant = (resolved, operation, inverse) => {
1661
1709
  if (find("variant", operation.at, resolved.addressHash)) return;
1662
1710
  const first = inverse[0];
1663
- if (first?.type !== "variant.replace") return inverseMismatch(operation);
1711
+ if (first?.type !== "variant.replace" && first?.type !== "value.clear") return inverseMismatch(operation);
1664
1712
  add({
1665
1713
  ...base(resolved, operation),
1666
1714
  kind: "variant",
1667
- before: absorb(first.value, operation.at)
1715
+ before: first.type === "value.clear" ? absent : present(absorb(first.value, operation.at))
1668
1716
  });
1669
1717
  };
1670
1718
  const recordDict = (resolved, operation, inverse) => {
@@ -1828,6 +1876,10 @@ const createChangeJournal = (root) => {
1828
1876
  record: (resolved, operation, inverse) => {
1829
1877
  profile.batch.journalRecord();
1830
1878
  if (ownerOf(operation.at)) return;
1879
+ if (operation.type === "value.clear" && resolved.node.kind === "variant") {
1880
+ recordVariant(resolved, operation, inverse);
1881
+ return;
1882
+ }
1831
1883
  if (operation.type === "field.set" || operation.type === "field.clear" || operation.type === "value.clear") {
1832
1884
  recordValue(resolved, operation, inverse);
1833
1885
  return;
@@ -1869,7 +1921,7 @@ const createChangeJournal = (root) => {
1869
1921
  const changeAt = (address, addressHash) => {
1870
1922
  const candidates = collectionBuckets.get(addressHash) ?? [];
1871
1923
  collectionBuckets.set(addressHash, candidates);
1872
- let change = candidates.find((entry) => sameAddress$1(entry.address, address));
1924
+ let change = candidates.find((entry) => sameAddress(entry.address, address));
1873
1925
  if (!change) {
1874
1926
  change = {
1875
1927
  address,
@@ -1913,7 +1965,7 @@ const createChangeJournal = (root) => {
1913
1965
  const has = (value, key) => Object.prototype.hasOwnProperty.call(value, key);
1914
1966
  const stringArray = (value) => Array.isArray(value) && value.every((entry) => typeof entry === "string");
1915
1967
  const positions = (value) => Array.isArray(value) && value.every((entry) => Number.isInteger(entry) && entry >= 0);
1916
- const address$1 = (value) => stringArray(value);
1968
+ const address = (value) => stringArray(value);
1917
1969
  const anchor = (value) => {
1918
1970
  if (!isRecord(value)) return false;
1919
1971
  if (Number(has(value, "at")) + Number(has(value, "before")) + Number(has(value, "after")) !== 1) return false;
@@ -1934,7 +1986,7 @@ const rejected = (code, message) => ({
1934
1986
  });
1935
1987
  const decode = (input) => {
1936
1988
  if (!isRecord(input)) return rejected("invalid-operation", "Operation must be an object.");
1937
- if (!address$1(input.at)) return rejected("invalid-address", "Operation address is malformed.");
1989
+ if (!address(input.at)) return rejected("invalid-address", "Operation address is malformed.");
1938
1990
  if (typeof input.type !== "string") return rejected("invalid-operation", "Operation type must be a string.");
1939
1991
  switch (input.type) {
1940
1992
  case "field.set":
@@ -2110,352 +2162,6 @@ const mutateOperations = (root, schema, operations, options = {}) => {
2110
2162
  return session.finish();
2111
2163
  };
2112
2164
  //#endregion
2113
- //#region core/src/mutation/footprint.ts
2114
- const sameAddress = (left, right) => left.length === right.length && left.every((segment, index) => segment === right[index]);
2115
- const containsAddress = (parent, child) => parent.length <= child.length && parent.every((segment, index) => segment === child[index]);
2116
- const address = (value) => Array.isArray(value) && value.every((segment) => typeof segment === "string") ? Object.freeze(value.slice()) : void 0;
2117
- const targetKey = (target) => {
2118
- const at = target.at.join("\0");
2119
- if (target.kind === "dictionary-key" || target.kind === "list-item") return `${target.kind}:${at}:${target.key}`;
2120
- if (target.kind === "entity" || target.kind === "tree-node") return `${target.kind}:${at}:${target.id}`;
2121
- return `${target.kind}:${at}`;
2122
- };
2123
- const freezeTarget = (target) => Object.freeze({
2124
- ...target,
2125
- at: Object.freeze(target.at.slice())
2126
- });
2127
- const add = (targets, target) => {
2128
- const stable = freezeTarget(target);
2129
- targets.set(targetKey(stable), stable);
2130
- };
2131
- const commandFootprint = (operations) => {
2132
- const targets = /* @__PURE__ */ new Map();
2133
- for (const operation of operations) switch (operation.type) {
2134
- case "field.set":
2135
- case "field.clear":
2136
- case "value.clear":
2137
- case "variant.replace":
2138
- add(targets, {
2139
- kind: "value",
2140
- at: operation.at
2141
- });
2142
- break;
2143
- case "dict.replace":
2144
- add(targets, {
2145
- kind: "dictionary",
2146
- at: operation.at
2147
- });
2148
- break;
2149
- case "dict.set":
2150
- case "dict.delete":
2151
- add(targets, {
2152
- kind: "dictionary-key",
2153
- at: operation.at,
2154
- key: operation.key
2155
- });
2156
- break;
2157
- case "entity.create":
2158
- add(targets, {
2159
- kind: "collection",
2160
- at: operation.at
2161
- });
2162
- for (const entry of operation.entries) add(targets, {
2163
- kind: "entity",
2164
- at: operation.at,
2165
- id: entry.id
2166
- });
2167
- break;
2168
- case "entity.remove":
2169
- add(targets, {
2170
- kind: "collection",
2171
- at: operation.at
2172
- });
2173
- for (const id of operation.ids) add(targets, {
2174
- kind: "entity",
2175
- at: operation.at,
2176
- id
2177
- });
2178
- break;
2179
- case "entity.move":
2180
- add(targets, {
2181
- kind: "collection",
2182
- at: operation.at
2183
- });
2184
- add(targets, {
2185
- kind: "entity",
2186
- at: operation.at,
2187
- id: operation.id
2188
- });
2189
- break;
2190
- case "list.insert":
2191
- case "list.move":
2192
- case "list.remove":
2193
- add(targets, {
2194
- kind: "list",
2195
- at: operation.at
2196
- });
2197
- add(targets, {
2198
- kind: "list-item",
2199
- at: operation.at,
2200
- key: operation.key
2201
- });
2202
- break;
2203
- case "list.replace":
2204
- add(targets, {
2205
- kind: "list",
2206
- at: operation.at
2207
- });
2208
- for (const key of operation.keys) add(targets, {
2209
- kind: "list-item",
2210
- at: operation.at,
2211
- key
2212
- });
2213
- break;
2214
- case "tree.insert":
2215
- case "tree.move":
2216
- case "tree.remove":
2217
- case "tree.set":
2218
- add(targets, {
2219
- kind: "tree",
2220
- at: operation.at
2221
- });
2222
- add(targets, {
2223
- kind: "tree-node",
2224
- at: operation.at,
2225
- id: operation.treeNodeId
2226
- });
2227
- break;
2228
- case "tree.replace":
2229
- add(targets, {
2230
- kind: "tree",
2231
- at: operation.at
2232
- });
2233
- break;
2234
- }
2235
- return Object.freeze(Array.from(targets.values()));
2236
- };
2237
- const overlapsValueAndEntity = (value, entity) => containsAddress([...entity.at, entity.id], value.at) || containsAddress(value.at, [...entity.at, entity.id]);
2238
- const targetsOverlap = (left, right) => {
2239
- if (left.kind === "value" && right.kind === "value") return containsAddress(left.at, right.at) || containsAddress(right.at, left.at);
2240
- if (left.kind === "value" && right.kind === "entity") return overlapsValueAndEntity(left, right);
2241
- if (left.kind === "entity" && right.kind === "value") return overlapsValueAndEntity(right, left);
2242
- if (left.kind === "dictionary" && right.kind === "dictionary") return sameAddress(left.at, right.at);
2243
- if (left.kind === "dictionary" && right.kind === "dictionary-key") return sameAddress(left.at, right.at);
2244
- if (left.kind === "dictionary-key" && right.kind === "dictionary") return sameAddress(left.at, right.at);
2245
- if (left.kind === "dictionary-key" && right.kind === "dictionary-key") return sameAddress(left.at, right.at) && left.key === right.key;
2246
- if (left.kind === "collection" && right.kind === "collection") return sameAddress(left.at, right.at);
2247
- if (left.kind === "collection" && right.kind === "entity") return sameAddress(left.at, right.at);
2248
- if (left.kind === "entity" && right.kind === "collection") return sameAddress(left.at, right.at);
2249
- if (left.kind === "entity" && right.kind === "entity") return sameAddress(left.at, right.at) && left.id === right.id;
2250
- if (left.kind === "list" && (right.kind === "list" || right.kind === "list-item")) return sameAddress(left.at, right.at);
2251
- if (right.kind === "list" && left.kind === "list-item") return sameAddress(left.at, right.at);
2252
- if (left.kind === "list-item" && right.kind === "list-item") return sameAddress(left.at, right.at) && left.key === right.key;
2253
- if (left.kind === "tree" && (right.kind === "tree" || right.kind === "tree-node")) return sameAddress(left.at, right.at);
2254
- if (right.kind === "tree" && left.kind === "tree-node") return sameAddress(left.at, right.at);
2255
- if (left.kind === "tree-node" && right.kind === "tree-node") return sameAddress(left.at, right.at) && left.id === right.id;
2256
- return false;
2257
- };
2258
- const footprintsOverlap = (left, right) => left.some((leftTarget) => right.some((rightTarget) => targetsOverlap(leftTarget, rightTarget)));
2259
- const decodedTarget = (value) => {
2260
- if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
2261
- const record = value;
2262
- const at = address(record.at);
2263
- if (!at || typeof record.kind !== "string") return void 0;
2264
- switch (record.kind) {
2265
- case "value":
2266
- case "dictionary":
2267
- case "collection":
2268
- case "list":
2269
- case "tree": return freezeTarget({
2270
- kind: record.kind,
2271
- at
2272
- });
2273
- case "dictionary-key":
2274
- case "list-item": return typeof record.key === "string" ? freezeTarget({
2275
- kind: record.kind,
2276
- at,
2277
- key: record.key
2278
- }) : void 0;
2279
- case "entity":
2280
- case "tree-node": return typeof record.id === "string" ? freezeTarget({
2281
- kind: record.kind,
2282
- at,
2283
- id: record.id
2284
- }) : void 0;
2285
- default: return;
2286
- }
2287
- };
2288
- const decodeCommandFootprint = (value) => {
2289
- if (!Array.isArray(value)) return void 0;
2290
- const targets = /* @__PURE__ */ new Map();
2291
- for (const entry of value) {
2292
- const target = decodedTarget(entry);
2293
- if (!target) return void 0;
2294
- targets.set(targetKey(target), target);
2295
- }
2296
- return Object.freeze(Array.from(targets.values()));
2297
- };
2298
- //#endregion
2299
- //#region core/src/runtime/notification.ts
2300
- const ROOT_BUCKET = Symbol("document-root");
2301
- const notifications = /* @__PURE__ */ new WeakMap();
2302
- const key = (value) => bucket(value) ?? ROOT_BUCKET;
2303
- const createNotification = (runtime) => {
2304
- const notification = {
2305
- root: /* @__PURE__ */ new Set(),
2306
- filtered: /* @__PURE__ */ new Set(),
2307
- buckets: /* @__PURE__ */ new Map(),
2308
- processors: [],
2309
- candidates: /* @__PURE__ */ new Set(),
2310
- rootSnapshot: [],
2311
- notifying: false
2312
- };
2313
- notifications.set(runtime, notification);
2314
- return notification;
2315
- };
2316
- const notificationOf = (runtime) => {
2317
- const value = notifications.get(runtime);
2318
- if (!value) throw new Error("Unknown Doxum runtime.");
2319
- return value;
2320
- };
2321
- const shareNotification = (source, target) => {
2322
- notifications.set(target, notificationOf(source));
2323
- };
2324
- const registerProcessor = (runtime, processor) => {
2325
- const notification = notificationOf(runtime);
2326
- const entry = {
2327
- processor,
2328
- active: true
2329
- };
2330
- notification.processors.push(entry);
2331
- return () => {
2332
- if (!entry.active) return;
2333
- entry.active = false;
2334
- if (!notification.notifying) {
2335
- const index = notification.processors.indexOf(entry);
2336
- if (index >= 0) notification.processors.splice(index, 1);
2337
- }
2338
- };
2339
- };
2340
- const subscribeRoot = (notification, listener) => {
2341
- const entry = {
2342
- listener,
2343
- active: true
2344
- };
2345
- notification.root.add(entry);
2346
- return () => {
2347
- entry.active = false;
2348
- if (!notification.notifying) notification.root.delete(entry);
2349
- };
2350
- };
2351
- const subscribeTargets = (notification, targets, listener) => {
2352
- const entry = {
2353
- targets: Object.freeze(targets.slice()),
2354
- listener,
2355
- active: true
2356
- };
2357
- notification.filtered.add(entry);
2358
- const keys = new Set(targets.map(key));
2359
- keys.forEach((key) => {
2360
- const bucket = notification.buckets.get(key) ?? /* @__PURE__ */ new Set();
2361
- bucket.add(entry);
2362
- notification.buckets.set(key, bucket);
2363
- });
2364
- return () => {
2365
- if (!entry.active) return;
2366
- entry.active = false;
2367
- if (notification.notifying) return;
2368
- notification.filtered.delete(entry);
2369
- keys.forEach((key) => {
2370
- const bucket = notification.buckets.get(key);
2371
- bucket?.delete(entry);
2372
- if (bucket?.size === 0) notification.buckets.delete(key);
2373
- });
2374
- };
2375
- };
2376
- const notify = (notification, commit) => {
2377
- notification.notifying = true;
2378
- const errors = [];
2379
- const call = (listener) => {
2380
- try {
2381
- listener(commit);
2382
- } catch (error) {
2383
- errors.push(Object.freeze({
2384
- phase: "listener",
2385
- error
2386
- }));
2387
- }
2388
- };
2389
- try {
2390
- const processorCount = notification.processors.length;
2391
- for (let index = 0; index < processorCount; index += 1) {
2392
- const entry = notification.processors[index];
2393
- if (!entry?.active) continue;
2394
- try {
2395
- entry.processor.process(commit);
2396
- } catch (error) {
2397
- errors.push(Object.freeze({
2398
- phase: "processor",
2399
- error
2400
- }));
2401
- }
2402
- }
2403
- for (let index = 0; index < processorCount; index += 1) {
2404
- const entry = notification.processors[index];
2405
- if (!entry?.active) continue;
2406
- try {
2407
- entry.processor.flush();
2408
- } catch (error) {
2409
- errors.push(Object.freeze({
2410
- phase: "flush",
2411
- error
2412
- }));
2413
- }
2414
- }
2415
- const candidates = notification.candidates;
2416
- candidates.clear();
2417
- if (commit.impact.kind === "reset") notification.filtered.forEach((entry) => candidates.add(entry));
2418
- else for (const operation of commit.operations) {
2419
- const key = operation.at[0] ?? ROOT_BUCKET;
2420
- notification.buckets.get(key)?.forEach((entry) => candidates.add(entry));
2421
- notification.buckets.get(ROOT_BUCKET)?.forEach((entry) => candidates.add(entry));
2422
- }
2423
- candidates.forEach((entry) => {
2424
- if (entry.active && entry.targets.some(commit.impact.affects)) call(entry.listener);
2425
- });
2426
- const rootSnapshot = notification.rootSnapshot;
2427
- rootSnapshot.length = 0;
2428
- notification.root.forEach((entry) => rootSnapshot.push(entry));
2429
- for (const entry of rootSnapshot) if (entry.active) call(entry.listener);
2430
- } finally {
2431
- notification.notifying = false;
2432
- let writeIndex = 0;
2433
- for (const entry of notification.processors) if (entry.active) notification.processors[writeIndex++] = entry;
2434
- notification.processors.length = writeIndex;
2435
- notification.root.forEach((entry) => {
2436
- if (!entry.active) notification.root.delete(entry);
2437
- });
2438
- notification.filtered.forEach((entry) => {
2439
- if (!entry.active) notification.filtered.delete(entry);
2440
- });
2441
- notification.buckets.forEach((bucket, key) => {
2442
- bucket.forEach((entry) => {
2443
- if (!entry.active) bucket.delete(entry);
2444
- });
2445
- if (bucket.size === 0) notification.buckets.delete(key);
2446
- });
2447
- }
2448
- return Object.freeze(errors);
2449
- };
2450
- const disposeNotification = (notification) => {
2451
- notification.root.clear();
2452
- notification.filtered.clear();
2453
- notification.buckets.clear();
2454
- notification.processors.length = 0;
2455
- notification.candidates.clear();
2456
- notification.rootSnapshot.length = 0;
2457
- };
2458
- //#endregion
2459
2165
  //#region core/src/runtime.ts
2460
2166
  var RejectedUpdate = class extends Error {
2461
2167
  issues;
@@ -2467,6 +2173,7 @@ var RejectedUpdate = class extends Error {
2467
2173
  const EMPTY = Object.freeze([]);
2468
2174
  const publishDiagnostic = (value) => Object.freeze({
2469
2175
  ...value,
2176
+ source: "application",
2470
2177
  ...value.address === void 0 ? {} : { address: Object.freeze(value.address.slice()) }
2471
2178
  });
2472
2179
  const createDocument = (input) => {
@@ -2485,6 +2192,7 @@ const createDocument = (input) => {
2485
2192
  const assertWritable = (intent) => {
2486
2193
  if (state.disposed) throw new DocumentDisposedError();
2487
2194
  if (busy) throw new DocumentReentrancyError();
2195
+ if (accessOf(runtime).projectionLocks) throw new DocumentReentrancyError();
2488
2196
  assertRuntimeWritable(runtime, intent);
2489
2197
  };
2490
2198
  const runTransaction = (session, run) => {
@@ -2526,7 +2234,19 @@ const createDocument = (input) => {
2526
2234
  const history = createHistory({
2527
2235
  capacity: input.history === false ? 0 : Math.max(0, input.history?.capacity ?? 100),
2528
2236
  revision: () => revision,
2529
- apply: (operations) => applyBatch(operations, "history", false)
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
+ },
2242
+ notify: (run) => {
2243
+ busy = true;
2244
+ try {
2245
+ return run();
2246
+ } finally {
2247
+ busy = false;
2248
+ }
2249
+ }
2530
2250
  });
2531
2251
  const publish = (batch, source, kind, recordHistory) => {
2532
2252
  revision += 1;
@@ -2540,10 +2260,12 @@ const createDocument = (input) => {
2540
2260
  });
2541
2261
  if (kind === "replace" || source === "remote") history.invalidate();
2542
2262
  else if (recordHistory && (source === "local" || source === "system")) history.record(batch.operations, batch.inverse);
2263
+ else if (source === "history") history.publish();
2264
+ else history.endGroup();
2543
2265
  busy = true;
2544
2266
  let observerErrors;
2545
2267
  try {
2546
- observerErrors = notify(notification, commit);
2268
+ observerErrors = notify(notification, commit, history.flush);
2547
2269
  } finally {
2548
2270
  busy = false;
2549
2271
  }
@@ -2575,7 +2297,7 @@ const createDocument = (input) => {
2575
2297
  address: {
2576
2298
  resolve: (address) => resolveAddress(input.schema, address, state.document),
2577
2299
  read: (address) => read(state.document, address),
2578
- contains,
2300
+ contains: contains$1,
2579
2301
  overlaps,
2580
2302
  debugKey
2581
2303
  },
@@ -2686,7 +2408,7 @@ const createDocument = (input) => {
2686
2408
  const invalidTree = invalidDocument(input.schema, document);
2687
2409
  if (invalidTree) return {
2688
2410
  status: "rejected",
2689
- issues: [at$1(invalidTree, "invalid-tree", "Document replacement contains an invalid tree.")],
2411
+ issues: [at(invalidTree, "invalid-tree", "Document replacement contains an invalid tree.")],
2690
2412
  revision
2691
2413
  };
2692
2414
  if (deepEqual(state.document, document)) return {
@@ -2716,15 +2438,20 @@ const createDocument = (input) => {
2716
2438
  history: history.api,
2717
2439
  dispose: () => {
2718
2440
  if (state.disposed) return;
2441
+ if (busy || accessOf(runtime).projectionLocks) throw new DocumentReentrancyError();
2719
2442
  state.disposed = true;
2720
- disposeNotification(notification);
2721
- history.api.clear();
2722
- disposeRuntimeDriver(runtime);
2443
+ try {
2444
+ disposeNotification(notification);
2445
+ } finally {
2446
+ history.dispose();
2447
+ disposeRuntimeDriver(runtime);
2448
+ }
2723
2449
  }
2724
2450
  };
2725
2451
  bindRuntimeAccess(runtime, state);
2726
2452
  bindRuntimeDriver(runtime);
2727
2453
  notification = createNotification(runtime);
2454
+ bindDocumentReadable(history.api, runtime);
2728
2455
  return runtime;
2729
2456
  };
2730
2457
  //#endregion
@@ -2743,387 +2470,6 @@ const asReadable = (runtime) => {
2743
2470
  //#region core/src/projection/select.ts
2744
2471
  const select = (runtime, selector) => readWith(runtime, selector);
2745
2472
  //#endregion
2746
- //#region core/src/projection/collection-view.ts
2747
- const createCollectionView = (input) => {
2748
- if (schemaOf(input.runtime) !== input.source.schema) throw new Error("Collection selector belongs to another schema.");
2749
- const idsListeners = /* @__PURE__ */ new Set();
2750
- const allListeners = /* @__PURE__ */ new Set();
2751
- const itemListeners = /* @__PURE__ */ new Map();
2752
- const itemReadables = /* @__PURE__ */ new Map();
2753
- let disposed = false;
2754
- const sourceNode = nodeAt$1(input.source.schema, input.source.address);
2755
- const entryNode = sourceNode?.kind === "table" || sourceNode?.kind === "map" ? sourceNode.value : {
2756
- kind: "object",
2757
- shape: {}
2758
- };
2759
- const mapEntry = (document, id) => {
2760
- profile.collectionView.mapped();
2761
- const address = [...input.source.address, String(id)];
2762
- let active = true;
2763
- try {
2764
- return input.map(id, readerFor(entryNode, {
2765
- root: () => document,
2766
- active: () => active
2767
- }, address));
2768
- } finally {
2769
- active = false;
2770
- }
2771
- };
2772
- const build = (previous, changed) => {
2773
- const document = documentOf(input.runtime);
2774
- const collection = read(document, input.source.address);
2775
- const ids = collection && typeof collection === "object" && "ids" in collection && Array.isArray(collection.ids) ? [...collection.ids] : Object.keys(collection ?? {});
2776
- profile.collectionView.idsScanned(ids.length);
2777
- const values = previous && changed ? previous.values : /* @__PURE__ */ new Map();
2778
- if (previous && changed) {
2779
- const nextIds = new Set(ids);
2780
- for (const id of previous.ids) if (!nextIds.has(id)) values.delete(id);
2781
- }
2782
- for (const id of ids) {
2783
- if (previous && changed && previous.values.has(id) && !changed.has(id)) continue;
2784
- const next = mapEntry(document, id);
2785
- if (previous && changed && previous.values.has(id)) {
2786
- const before = previous.values.get(id);
2787
- if (input.isEqual ? input.isEqual(before, next) : Object.is(before, next)) {
2788
- changed.delete(id);
2789
- continue;
2790
- }
2791
- }
2792
- values.set(id, next);
2793
- }
2794
- return {
2795
- ids: Object.freeze(ids),
2796
- values,
2797
- all: void 0,
2798
- allDirty: true
2799
- };
2800
- };
2801
- let state = build();
2802
- const ensureAll = () => {
2803
- if (!state.allDirty && state.all) return state.all;
2804
- profile.collectionView.arrayCopied();
2805
- const all = new Array(state.ids.length);
2806
- for (let index = 0; index < state.ids.length; index += 1) all[index] = state.values.get(state.ids[index]);
2807
- const stable = Object.freeze(all);
2808
- state.all = stable;
2809
- state.allDirty = false;
2810
- return stable;
2811
- };
2812
- const emit = (listeners) => {
2813
- Array.from(listeners).forEach((listener) => listener());
2814
- };
2815
- const unsubscribe = input.runtime.subscribe(input.source, (commit) => {
2816
- if (disposed) return;
2817
- const impact = commit.impact.collection(input.source);
2818
- if (impact.kind === "incremental" && impact.added.size === 0 && impact.removed.size === 0 && !impact.orderChanged) {
2819
- const previous = state;
2820
- const changes = [];
2821
- const document = documentOf(input.runtime);
2822
- impact.updated.forEach((id) => {
2823
- if (!previous.values.has(id)) return;
2824
- const before = previous.values.get(id);
2825
- const after = mapEntry(document, id);
2826
- if (input.isEqual ? input.isEqual(before, after) : Object.is(before, after)) return;
2827
- changes.push([id, after]);
2828
- });
2829
- if (changes.length === 0) return;
2830
- for (const [id, value] of changes) previous.values.set(id, value);
2831
- previous.all = void 0;
2832
- previous.allDirty = true;
2833
- emit(allListeners);
2834
- for (const [id] of changes) {
2835
- const listeners = itemListeners.get(id);
2836
- if (listeners) emit(listeners);
2837
- }
2838
- return;
2839
- }
2840
- const changed = impact.kind === "reset" ? void 0 : new Set([...impact.added, ...impact.updated]);
2841
- const previous = state;
2842
- const next = build(previous, changed);
2843
- const changedIds = /* @__PURE__ */ new Set();
2844
- if (changed) {
2845
- changed.forEach((id) => changedIds.add(id));
2846
- previous.ids.forEach((id) => {
2847
- if (!next.values.has(id)) changedIds.add(id);
2848
- });
2849
- next.ids.forEach((id) => {
2850
- if (!previous.values.has(id)) changedIds.add(id);
2851
- });
2852
- } else {
2853
- previous.ids.forEach((id) => {
2854
- if (!next.values.has(id)) changedIds.add(id);
2855
- else if (!(input.isEqual ? input.isEqual(previous.values.get(id), next.values.get(id)) : Object.is(previous.values.get(id), next.values.get(id)))) changedIds.add(id);
2856
- });
2857
- next.ids.forEach((id) => {
2858
- if (!previous.values.has(id)) changedIds.add(id);
2859
- });
2860
- }
2861
- const idsChanged = impact.kind === "incremental" ? impact.added.size > 0 || impact.removed.size > 0 || impact.orderChanged : previous.ids.length !== next.ids.length || previous.ids.some((id, index) => next.ids[index] !== id);
2862
- state = idsChanged || changedIds.size ? next : previous;
2863
- if (idsChanged) emit(idsListeners);
2864
- if (idsChanged || changedIds.size) emit(allListeners);
2865
- changedIds.forEach((id) => {
2866
- const listeners = itemListeners.get(id);
2867
- if (listeners) emit(listeners);
2868
- });
2869
- });
2870
- return {
2871
- ids: {
2872
- current: () => state.ids,
2873
- revision: () => input.runtime.revision(),
2874
- subscribe: (listener) => {
2875
- if (disposed) return () => void 0;
2876
- idsListeners.add(listener);
2877
- return () => idsListeners.delete(listener);
2878
- }
2879
- },
2880
- all: {
2881
- current: ensureAll,
2882
- revision: () => input.runtime.revision(),
2883
- subscribe: (listener) => {
2884
- if (disposed) return () => void 0;
2885
- allListeners.add(listener);
2886
- return () => allListeners.delete(listener);
2887
- }
2888
- },
2889
- item: (id) => {
2890
- const cached = itemReadables.get(id);
2891
- if (cached) return cached;
2892
- const readable = {
2893
- current: () => state.values.get(id),
2894
- revision: () => input.runtime.revision(),
2895
- subscribe: (listener) => {
2896
- if (disposed) return () => void 0;
2897
- const listeners = itemListeners.get(id) ?? /* @__PURE__ */ new Set();
2898
- itemListeners.set(id, listeners);
2899
- listeners.add(listener);
2900
- return () => {
2901
- listeners.delete(listener);
2902
- if (listeners.size === 0) itemListeners.delete(id);
2903
- };
2904
- }
2905
- };
2906
- itemReadables.set(id, readable);
2907
- return readable;
2908
- },
2909
- revision: () => input.runtime.revision(),
2910
- dispose: () => {
2911
- if (disposed) return;
2912
- disposed = true;
2913
- unsubscribe();
2914
- idsListeners.clear();
2915
- allListeners.clear();
2916
- itemListeners.clear();
2917
- itemReadables.clear();
2918
- state.values.clear();
2919
- state = {
2920
- ids: Object.freeze([]),
2921
- values: /* @__PURE__ */ new Map(),
2922
- all: Object.freeze([]),
2923
- allDirty: false
2924
- };
2925
- }
2926
- };
2927
- };
2928
- //#endregion
2929
- //#region core/src/projection/materialized-view.ts
2930
- const materializedMeta = /* @__PURE__ */ new WeakMap();
2931
- const createMaterializedView = (runtime, spec) => {
2932
- const listeners = /* @__PURE__ */ new Set();
2933
- let disposed = false;
2934
- let fault;
2935
- let revision = runtime.revision();
2936
- let instance;
2937
- let currentValue;
2938
- let pendingEmit = false;
2939
- const dependencies = createDependencyTracker();
2940
- const sources = spec.sources ?? {};
2941
- const sourceEntries = Object.keys(sources).map((key) => [key, sources[key]]);
2942
- sourceEntries.forEach(([, source]) => {
2943
- const meta = materializedMeta.get(source);
2944
- if (!meta || meta.runtime !== runtime) throw new Error("Materialized sources must belong to the same runtime and be created first.");
2945
- });
2946
- const sourceValuesRecord = {};
2947
- let sourceChanged = false;
2948
- for (const [key] of sourceEntries) sourceValuesRecord[key] = {
2949
- value: void 0,
2950
- change: void 0
2951
- };
2952
- const sourceValues = (commitRevision) => {
2953
- sourceChanged = false;
2954
- for (const [key, source] of sourceEntries) {
2955
- const meta = materializedMeta.get(source);
2956
- const entry = sourceValuesRecord[key];
2957
- entry.value = meta?.value ?? source.current();
2958
- entry.change = meta?.change;
2959
- if (commitRevision !== void 0 && meta?.changedRevision === commitRevision) {
2960
- sourceChanged = true;
2961
- profile.materialized.sourceChanged();
2962
- }
2963
- }
2964
- return sourceValuesRecord;
2965
- };
2966
- let activeImpact;
2967
- const assertImpact = () => {
2968
- if (!activeImpact) throw new Error("Materialized impact is no longer active.");
2969
- return activeImpact;
2970
- };
2971
- const trackedImpact = {
2972
- get kind() {
2973
- return assertImpact().kind;
2974
- },
2975
- affects: (target) => {
2976
- dependencies.record(target);
2977
- return assertImpact().affects(target);
2978
- },
2979
- collection: (selector) => {
2980
- dependencies.record(selector);
2981
- return assertImpact().collection(selector);
2982
- },
2983
- get operations() {
2984
- dependencies.record({
2985
- kind: "value",
2986
- at: []
2987
- });
2988
- return assertImpact().operations;
2989
- }
2990
- };
2991
- let view;
2992
- const writeMeta = (change, value, nextRevision, changed) => {
2993
- const meta = materializedMeta.get(view);
2994
- if (meta) {
2995
- meta.value = value;
2996
- meta.change = change;
2997
- meta.revision = nextRevision;
2998
- if (changed) meta.changedRevision = nextRevision;
2999
- } else materializedMeta.set(view, {
3000
- runtime,
3001
- value,
3002
- change,
3003
- revision: nextRevision,
3004
- changedRevision: changed ? nextRevision : -1
3005
- });
3006
- };
3007
- const build = () => {
3008
- profile.materialized.rebuilt();
3009
- dependencies.clear();
3010
- let active = true;
3011
- try {
3012
- instance = spec.build({
3013
- read: documentReader(schemaOf(runtime), () => documentOf(runtime), () => active),
3014
- sources: sourceValues()
3015
- });
3016
- currentValue = instance.value;
3017
- fault = void 0;
3018
- revision = runtime.revision();
3019
- } finally {
3020
- active = false;
3021
- }
3022
- };
3023
- build();
3024
- const emit = () => {
3025
- profile.materialized.notification();
3026
- Array.from(listeners).forEach((listener) => listener());
3027
- };
3028
- const process = (commit) => {
3029
- if (disposed) return;
3030
- try {
3031
- if (commit.kind === "replace" || fault) {
3032
- build();
3033
- writeMeta(void 0, currentValue, commit.revision, true);
3034
- pendingEmit = true;
3035
- return;
3036
- }
3037
- const sourceSnapshot = sourceValues(commit.revision);
3038
- if (!sourceChanged && dependencies.size() > 0 && !dependencies.some(commit.impact.affects)) {
3039
- profile.materialized.skipped();
3040
- revision = commit.revision;
3041
- writeMeta(void 0, currentValue, commit.revision, false);
3042
- return;
3043
- }
3044
- dependencies.clear();
3045
- profile.materialized.updated();
3046
- let active = true;
3047
- let result;
3048
- try {
3049
- activeImpact = commit.impact;
3050
- result = instance.update({
3051
- read: documentReader(schemaOf(runtime), () => documentOf(runtime), () => active),
3052
- revision: commit.revision,
3053
- source: commit.source,
3054
- impact: trackedImpact,
3055
- sources: sourceSnapshot
3056
- });
3057
- } finally {
3058
- activeImpact = void 0;
3059
- active = false;
3060
- }
3061
- if (result.kind === "rebuild") {
3062
- build();
3063
- writeMeta(void 0, currentValue, commit.revision, true);
3064
- pendingEmit = true;
3065
- } else if (result.kind === "changed") {
3066
- currentValue = result.value;
3067
- revision = commit.revision;
3068
- writeMeta(result.change, currentValue, commit.revision, true);
3069
- pendingEmit = true;
3070
- } else {
3071
- revision = commit.revision;
3072
- writeMeta(void 0, currentValue, commit.revision, false);
3073
- }
3074
- } catch (error) {
3075
- try {
3076
- build();
3077
- writeMeta(void 0, currentValue, commit.revision, true);
3078
- pendingEmit = true;
3079
- } catch (rebuildError) {
3080
- fault = rebuildError;
3081
- throw rebuildError;
3082
- }
3083
- throw error;
3084
- }
3085
- };
3086
- view = {
3087
- current: () => {
3088
- if (fault) throw fault;
3089
- return currentValue;
3090
- },
3091
- revision: () => revision,
3092
- subscribe: (listener) => {
3093
- listeners.add(listener);
3094
- return () => listeners.delete(listener);
3095
- },
3096
- rebuild: () => {
3097
- build();
3098
- writeMeta(void 0, currentValue, runtime.revision(), true);
3099
- emit();
3100
- },
3101
- dispose: () => {
3102
- if (disposed) return;
3103
- disposed = true;
3104
- unregister();
3105
- listeners.clear();
3106
- materializedMeta.delete(view);
3107
- }
3108
- };
3109
- materializedMeta.set(view, {
3110
- runtime,
3111
- value: currentValue,
3112
- change: void 0,
3113
- revision,
3114
- changedRevision: -1
3115
- });
3116
- const unregister = registerProcessor(runtime, {
3117
- process,
3118
- flush: () => {
3119
- if (!pendingEmit) return;
3120
- pendingEmit = false;
3121
- emit();
3122
- }
3123
- });
3124
- return view;
3125
- };
3126
- //#endregion
3127
- export { DocumentDisposedError, DocumentReentrancyError, asReadable, commandFootprint, contains, createCollectionView, createDocument, createMaterializedView, debugKey, decodeCommandFootprint, dict, field, footprintsOverlap, list, map, object, optional, overlaps, read as readAddress, record, resolveAddress, schema, select, single, table, impact_target_exports as target, tree, variant };
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 };
3128
2474
 
3129
2475
  //# sourceMappingURL=index.js.map