doxum 0.1.40 → 0.1.41

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/advanced.cjs CHANGED
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_transform = require("./transform-8xXf0OKB.cjs");
2
+ const require_transform = require("./transform-D6njdP3k.cjs");
3
3
  const require_record = require("./record-ChYUi8PP.cjs");
4
4
  //#region core/src/projection/advanced.ts
5
5
  const resetCollectionChange = Object.freeze({ kind: "reset" });
package/dist/advanced.js CHANGED
@@ -1,4 +1,4 @@
1
- import { D as defineProcessor, S as compileProjectionDependencies, _ as assertSynchronous, r as collectionChange, t as createKeyedTransform, w as collectionView } from "./transform--WCn6_v0.js";
1
+ import { D as defineProcessor, S as compileProjectionDependencies, _ as assertSynchronous, r as collectionChange, t as createKeyedTransform, w as collectionView } from "./transform-CKJ1ivhz.js";
2
2
  import { n as isPlainObject } from "./record-B2yRhYsc.js";
3
3
  //#region core/src/projection/advanced.ts
4
4
  const resetCollectionChange = Object.freeze({ kind: "reset" });
package/dist/index.cjs CHANGED
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_transform = require("./transform-8xXf0OKB.cjs");
2
+ const require_transform = require("./transform-D6njdP3k.cjs");
3
3
  const require_record = require("./record-ChYUi8PP.cjs");
4
4
  const require_driver = require("./driver-CN3dGxQI.cjs");
5
5
  //#region core/src/schema/model.ts
@@ -652,7 +652,7 @@ function remove$2(session, container, ids) {
652
652
  for (const id of removed) session.writeMember(container, id, void 0, "remove");
653
653
  table.ids = table.ids.filter((id) => !removed.has(id));
654
654
  }
655
- function replace$3(session, container, id, value) {
655
+ function replace$4(session, container, id, value) {
656
656
  const { at, node, value: tableValue } = container;
657
657
  if (node.kind !== "table") return require_driver.fail(at, "invalid-collection", "Expected a table.");
658
658
  const table = tableValue;
@@ -675,7 +675,7 @@ function insert$1(session, container, value, position) {
675
675
  insert$2(items, index(keys$1, position), value);
676
676
  session.invalidate();
677
677
  }
678
- function replace$2(session, container, id, value) {
678
+ function replace$3(session, container, id, value) {
679
679
  const { at, node, value: items } = container;
680
680
  if (node.kind !== "list") return require_driver.fail(at, "invalid-list-key", "Expected a list.");
681
681
  const index = indexedKeys(items, node.keyOf).index(id);
@@ -721,7 +721,7 @@ const insert = (session, container, id, value, position) => {
721
721
  else tree.rootId = id;
722
722
  session.invalidate();
723
723
  };
724
- const replace$1 = (session, container, id, value) => {
724
+ const replace$2 = (session, container, id, value) => {
725
725
  const { at, node: schema, value: tree } = container;
726
726
  const entry = node$1(tree, id);
727
727
  if (!entry) return require_driver.fail(at, "missing-tree-node", "Tree node does not exist.");
@@ -1180,7 +1180,7 @@ const createAccess = (context, initial = []) => {
1180
1180
  const [valueOrId, value] = args;
1181
1181
  if (args.length === 1) return replaceCollection(target, node, valueOrId);
1182
1182
  const writable = writableCollection(target, node);
1183
- replace$3(context.session, writable, valueOrId, value);
1183
+ replace$4(context.session, writable, valueOrId, value);
1184
1184
  };
1185
1185
  return orderedMethod(target, property, node);
1186
1186
  };
@@ -1197,7 +1197,7 @@ const createAccess = (context, initial = []) => {
1197
1197
  const [valueOrId, value] = args;
1198
1198
  if (args.length === 1) return replaceCollection(target, node, valueOrId);
1199
1199
  const writable = writableCollection(target, node);
1200
- replace$2(context.session, writable, valueOrId, value);
1200
+ replace$3(context.session, writable, valueOrId, value);
1201
1201
  };
1202
1202
  return orderedMethod(target, property, node);
1203
1203
  };
@@ -1219,7 +1219,7 @@ const createAccess = (context, initial = []) => {
1219
1219
  const [valueOrId, value] = args;
1220
1220
  if (args.length === 1) return replaceCollection(target, node, valueOrId);
1221
1221
  const writable = writableTree(target, node);
1222
- replace$1(context.session, writable, valueOrId, value);
1222
+ replace$2(context.session, writable, valueOrId, value);
1223
1223
  };
1224
1224
  const current = () => collectionValue(target, node);
1225
1225
  if (property === "rootId") return () => {
@@ -3798,49 +3798,109 @@ const node = (key, value, left, right) => {
3798
3798
  height: Math.max(height(left), height(right)) + 1
3799
3799
  });
3800
3800
  };
3801
- const rotateLeft = (current) => {
3802
- const right = current.right;
3803
- return node(right.key, right.value, node(current.key, current.value, current.left, right.left), right.right);
3804
- };
3805
- const rotateRight = (current) => {
3806
- const left = current.left;
3807
- return node(left.key, left.value, left.left, node(current.key, current.value, left.right, current.right));
3808
- };
3809
- const rebalance = (current) => {
3810
- const balance = height(current.left) - height(current.right);
3811
- if (balance > 1) {
3812
- if (height(current.left.left) < height(current.left.right)) return rotateRight(node(current.key, current.value, rotateLeft(current.left), current.right));
3813
- return rotateRight(current);
3814
- }
3815
- if (balance < -1) {
3816
- if (height(current.right.right) < height(current.right.left)) return rotateLeft(node(current.key, current.value, current.left, rotateRight(current.right)));
3817
- return rotateLeft(current);
3818
- }
3819
- return current;
3820
- };
3821
- const setNode = (current, key, value) => {
3822
- if (!current) return node(key, value);
3823
- if (key === current.key) return node(key, value, current.left, current.right);
3824
- if (key < current.key) return rebalance(node(current.key, current.value, setNode(current.left, key, value), current.right));
3825
- return rebalance(node(current.key, current.value, current.left, setNode(current.right, key, value)));
3826
- };
3827
- const minNode = (current) => current.left ? minNode(current.left) : current;
3828
- const removeNode = (current, key) => {
3829
- if (!current) return void 0;
3830
- if (key < current.key) return rebalance(node(current.key, current.value, removeNode(current.left, key), current.right));
3831
- if (key > current.key) return rebalance(node(current.key, current.value, current.left, removeNode(current.right, key)));
3801
+ /** Local repair after a join descent or a single deletion (height difference at most two). */
3802
+ const balance = (key, value, left, right) => {
3803
+ if (height(left) > height(right) + 1) {
3804
+ const heavy = left;
3805
+ if (height(heavy.left) >= height(heavy.right)) return node(heavy.key, heavy.value, heavy.left, node(key, value, heavy.right, right));
3806
+ const middle = heavy.right;
3807
+ return node(middle.key, middle.value, node(heavy.key, heavy.value, heavy.left, middle.left), node(key, value, middle.right, right));
3808
+ }
3809
+ if (height(right) > height(left) + 1) {
3810
+ const heavy = right;
3811
+ if (height(heavy.right) >= height(heavy.left)) return node(heavy.key, heavy.value, node(key, value, left, heavy.left), heavy.right);
3812
+ const middle = heavy.left;
3813
+ return node(middle.key, middle.value, node(key, value, left, middle.left), node(heavy.key, heavy.value, middle.right, heavy.right));
3814
+ }
3815
+ return node(key, value, left, right);
3816
+ };
3817
+ /** All left keys precede key, all right keys follow it; heights may differ arbitrarily. */
3818
+ const join = (key, value, left, right) => {
3819
+ if (height(left) > height(right) + 1) {
3820
+ require_transform.profile.collectionIndex.visit();
3821
+ return balance(left.key, left.value, left.left, join(key, value, left.right, right));
3822
+ }
3823
+ if (height(right) > height(left) + 1) {
3824
+ require_transform.profile.collectionIndex.visit();
3825
+ return balance(right.key, right.value, join(key, value, left, right.left), right.right);
3826
+ }
3827
+ return node(key, value, left, right);
3828
+ };
3829
+ const removeMin = (current) => {
3830
+ require_transform.profile.collectionIndex.visit();
3832
3831
  if (!current.left) return current.right;
3833
- if (!current.right) return current.left;
3834
- const next = minNode(current.right);
3835
- return rebalance(node(next.key, next.value, current.left, removeNode(current.right, next.key)));
3832
+ return balance(current.key, current.value, removeMin(current.left), current.right);
3833
+ };
3834
+ const concat = (left, right) => {
3835
+ if (!left) return right;
3836
+ if (!right) return left;
3837
+ let first = right;
3838
+ while (first.left) {
3839
+ require_transform.profile.collectionIndex.visit();
3840
+ first = first.left;
3841
+ }
3842
+ return join(first.key, first.value, left, removeMin(right));
3843
+ };
3844
+ const replace$1 = (current, entry, left, right) => {
3845
+ if (entry && !entry.present) return concat(left, right);
3846
+ const value = entry ? entry.value : current.value;
3847
+ if (left === current.left && right === current.right && Object.is(value, current.value)) return current;
3848
+ return join(current.key, value, left, right);
3849
+ };
3850
+ const edit = (current, key, entry) => {
3851
+ if (!current) return entry.present ? node(key, entry.value) : void 0;
3852
+ require_transform.profile.collectionIndex.visit();
3853
+ if (key === current.key) return replace$1(current, entry, current.left, current.right);
3854
+ if (key < current.key) {
3855
+ const left = edit(current.left, key, entry);
3856
+ return left === current.left ? current : balance(current.key, current.value, left, current.right);
3857
+ }
3858
+ const right = edit(current.right, key, entry);
3859
+ return right === current.right ? current : balance(current.key, current.value, current.left, right);
3860
+ };
3861
+ /** Builds a new subtree in linear time, ignoring removals of absent keys. */
3862
+ const buildChanges = (keys, changes, start, end) => {
3863
+ let count = 0;
3864
+ for (let i = start; i < end; i++) if (changes.get(keys[i]).present) count++;
3865
+ let cursor = start;
3866
+ const build = (size) => {
3867
+ if (!size) return void 0;
3868
+ const leftSize = size >> 1;
3869
+ const left = build(leftSize);
3870
+ let key;
3871
+ let entry;
3872
+ do {
3873
+ key = keys[cursor++];
3874
+ entry = changes.get(key);
3875
+ } while (!entry.present);
3876
+ return node(key, entry.value, left, build(size - leftSize - 1));
3877
+ };
3878
+ return build(count);
3879
+ };
3880
+ const applyChanges = (current, keys, changes, start, end) => {
3881
+ if (start === end) return current;
3882
+ if (end - start === 1) return edit(current, keys[start], changes.get(keys[start]));
3883
+ if (!current) return buildChanges(keys, changes, start, end);
3884
+ require_transform.profile.collectionIndex.visit();
3885
+ let low = start;
3886
+ let high = end;
3887
+ while (low < high) {
3888
+ const middle = low + high >>> 1;
3889
+ if (keys[middle] < current.key) low = middle + 1;
3890
+ else high = middle;
3891
+ }
3892
+ const matched = low < end && keys[low] === current.key;
3893
+ const left = applyChanges(current.left, keys, changes, start, low);
3894
+ const right = applyChanges(current.right, keys, changes, matched ? low + 1 : low, end);
3895
+ return replace$1(current, matched ? changes.get(current.key) : void 0, left, right);
3836
3896
  };
3837
3897
  const fromSorted = (entries, start = 0, end = entries.length) => {
3838
3898
  if (start >= end) return void 0;
3839
- const middle = start + end >> 1;
3899
+ const middle = start + end >>> 1;
3840
3900
  const [key, value] = entries[middle];
3841
3901
  return node(key, value, fromSorted(entries, start, middle), fromSorted(entries, middle + 1, end));
3842
3902
  };
3843
- /** Immutable keyed lookup used to keep published collection snapshots durable across revisions. */
3903
+ /** Immutable keyed lookup used to keep collection snapshots durable across revisions. */
3844
3904
  var PersistentKeyedIndex = class PersistentKeyedIndex {
3845
3905
  constructor(root) {
3846
3906
  this.root = root;
@@ -3848,6 +3908,7 @@ var PersistentKeyedIndex = class PersistentKeyedIndex {
3848
3908
  static empty() {
3849
3909
  return new PersistentKeyedIndex();
3850
3910
  }
3911
+ /** The input must contain unique keys. Formal collection order is owned separately. */
3851
3912
  static from(entries) {
3852
3913
  const sorted = [...entries].sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0);
3853
3914
  require_transform.profile.collectionIndex.build(sorted.length);
@@ -3868,11 +3929,19 @@ var PersistentKeyedIndex = class PersistentKeyedIndex {
3868
3929
  }
3869
3930
  return false;
3870
3931
  }
3871
- set(key, value) {
3872
- return new PersistentKeyedIndex(setNode(this.root, key, value));
3873
- }
3874
- remove(key) {
3875
- return new PersistentKeyedIndex(removeNode(this.root, key));
3932
+ /** Applies accepted final entry intents together; never calls user equality or changes old roots. */
3933
+ apply(changes) {
3934
+ if (!changes.size) return this;
3935
+ require_transform.profile.collectionIndex.batch(changes.size > 1 ? changes.size : 0);
3936
+ let root;
3937
+ if (changes.size === 1) {
3938
+ const [key, entry] = changes.entries().next().value;
3939
+ root = edit(this.root, key, entry);
3940
+ } else {
3941
+ const keys = [...changes.keys()].sort();
3942
+ root = applyChanges(this.root, keys, changes, 0, keys.length);
3943
+ }
3944
+ return root === this.root ? this : new PersistentKeyedIndex(root);
3876
3945
  }
3877
3946
  };
3878
3947
  //#endregion
@@ -3910,10 +3979,11 @@ const createCollectionState = (initial) => {
3910
3979
  values.clear();
3911
3980
  for (const [key, entry] of staged) if (entry.present) values.set(key, entry.value);
3912
3981
  if (index) index = PersistentKeyedIndex.from(values);
3913
- } else for (const [key, entry] of staged) {
3914
- if (index) index = entry.present ? index.set(key, entry.value) : index.remove(key);
3915
- if (entry.present) values.set(key, entry.value);
3982
+ } else {
3983
+ const nextIndex = index?.apply(staged);
3984
+ for (const [key, entry] of staged) if (entry.present) values.set(key, entry.value);
3916
3985
  else values.delete(key);
3986
+ index = nextIndex;
3917
3987
  }
3918
3988
  ids = nextIds;
3919
3989
  },
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { A as ownProjections, C as snapshotProjectionValues, D as defineProcessor, E as snapshotCollectionView, M as projectionRef, N as profile, O as defineSource, S as compileProjectionDependencies, T as mapRead, _ as assertSynchronous, a as collectionChangeTouchesKey, b as ProjectionError, c as assertKeyedProjection, d as outputKind, f as createKeyRelation, g as assertScope, h as snapshotArray, i as collectionChangeIntersects, j as producerOf, k as isProjection, l as compileKeyedDependencies, m as sameArray, n as keyedAbsent, o as collectionHasStructuralChange, p as iterableMatchesArray, s as createCollectionChange, t as createKeyedTransform, u as createKeyedDependencyRuntime, v as createScheduler, w as collectionView, x as collectKeyedInvalidation, y as ProjectionDisposedError } from "./transform--WCn6_v0.js";
1
+ import { A as ownProjections, C as snapshotProjectionValues, D as defineProcessor, E as snapshotCollectionView, M as projectionRef, N as profile, O as defineSource, S as compileProjectionDependencies, T as mapRead, _ as assertSynchronous, a as collectionChangeTouchesKey, b as ProjectionError, c as assertKeyedProjection, d as outputKind, f as createKeyRelation, g as assertScope, h as snapshotArray, i as collectionChangeIntersects, j as producerOf, k as isProjection, l as compileKeyedDependencies, m as sameArray, n as keyedAbsent, o as collectionHasStructuralChange, p as iterableMatchesArray, s as createCollectionChange, t as createKeyedTransform, u as createKeyedDependencyRuntime, v as createScheduler, w as collectionView, x as collectKeyedInvalidation, y as ProjectionDisposedError } from "./transform-CKJ1ivhz.js";
2
2
  import { n as isPlainObject, r as isRecord, t as installOwn } from "./record-B2yRhYsc.js";
3
3
  import { c as AddressIndex, d as invalidValue, f as children, g as validate, h as parent, i as contextOf, l as MutationRejected, m as is, o as decodeChanges, p as contains$1, r as bindContext, s as sealChanges, t as assertRuntimeWritable, u as fail } from "./driver-B7RR-VfX.js";
4
4
  //#region core/src/schema/model.ts
@@ -651,7 +651,7 @@ function remove$2(session, container, ids) {
651
651
  for (const id of removed) session.writeMember(container, id, void 0, "remove");
652
652
  table.ids = table.ids.filter((id) => !removed.has(id));
653
653
  }
654
- function replace$3(session, container, id, value) {
654
+ function replace$4(session, container, id, value) {
655
655
  const { at, node, value: tableValue } = container;
656
656
  if (node.kind !== "table") return fail(at, "invalid-collection", "Expected a table.");
657
657
  const table = tableValue;
@@ -674,7 +674,7 @@ function insert$1(session, container, value, position) {
674
674
  insert$2(items, index(keys$1, position), value);
675
675
  session.invalidate();
676
676
  }
677
- function replace$2(session, container, id, value) {
677
+ function replace$3(session, container, id, value) {
678
678
  const { at, node, value: items } = container;
679
679
  if (node.kind !== "list") return fail(at, "invalid-list-key", "Expected a list.");
680
680
  const index = indexedKeys(items, node.keyOf).index(id);
@@ -720,7 +720,7 @@ const insert = (session, container, id, value, position) => {
720
720
  else tree.rootId = id;
721
721
  session.invalidate();
722
722
  };
723
- const replace$1 = (session, container, id, value) => {
723
+ const replace$2 = (session, container, id, value) => {
724
724
  const { at, node: schema, value: tree } = container;
725
725
  const entry = node$1(tree, id);
726
726
  if (!entry) return fail(at, "missing-tree-node", "Tree node does not exist.");
@@ -1179,7 +1179,7 @@ const createAccess = (context, initial = []) => {
1179
1179
  const [valueOrId, value] = args;
1180
1180
  if (args.length === 1) return replaceCollection(target, node, valueOrId);
1181
1181
  const writable = writableCollection(target, node);
1182
- replace$3(context.session, writable, valueOrId, value);
1182
+ replace$4(context.session, writable, valueOrId, value);
1183
1183
  };
1184
1184
  return orderedMethod(target, property, node);
1185
1185
  };
@@ -1196,7 +1196,7 @@ const createAccess = (context, initial = []) => {
1196
1196
  const [valueOrId, value] = args;
1197
1197
  if (args.length === 1) return replaceCollection(target, node, valueOrId);
1198
1198
  const writable = writableCollection(target, node);
1199
- replace$2(context.session, writable, valueOrId, value);
1199
+ replace$3(context.session, writable, valueOrId, value);
1200
1200
  };
1201
1201
  return orderedMethod(target, property, node);
1202
1202
  };
@@ -1218,7 +1218,7 @@ const createAccess = (context, initial = []) => {
1218
1218
  const [valueOrId, value] = args;
1219
1219
  if (args.length === 1) return replaceCollection(target, node, valueOrId);
1220
1220
  const writable = writableTree(target, node);
1221
- replace$1(context.session, writable, valueOrId, value);
1221
+ replace$2(context.session, writable, valueOrId, value);
1222
1222
  };
1223
1223
  const current = () => collectionValue(target, node);
1224
1224
  if (property === "rootId") return () => {
@@ -3797,49 +3797,109 @@ const node = (key, value, left, right) => {
3797
3797
  height: Math.max(height(left), height(right)) + 1
3798
3798
  });
3799
3799
  };
3800
- const rotateLeft = (current) => {
3801
- const right = current.right;
3802
- return node(right.key, right.value, node(current.key, current.value, current.left, right.left), right.right);
3803
- };
3804
- const rotateRight = (current) => {
3805
- const left = current.left;
3806
- return node(left.key, left.value, left.left, node(current.key, current.value, left.right, current.right));
3807
- };
3808
- const rebalance = (current) => {
3809
- const balance = height(current.left) - height(current.right);
3810
- if (balance > 1) {
3811
- if (height(current.left.left) < height(current.left.right)) return rotateRight(node(current.key, current.value, rotateLeft(current.left), current.right));
3812
- return rotateRight(current);
3813
- }
3814
- if (balance < -1) {
3815
- if (height(current.right.right) < height(current.right.left)) return rotateLeft(node(current.key, current.value, current.left, rotateRight(current.right)));
3816
- return rotateLeft(current);
3817
- }
3818
- return current;
3819
- };
3820
- const setNode = (current, key, value) => {
3821
- if (!current) return node(key, value);
3822
- if (key === current.key) return node(key, value, current.left, current.right);
3823
- if (key < current.key) return rebalance(node(current.key, current.value, setNode(current.left, key, value), current.right));
3824
- return rebalance(node(current.key, current.value, current.left, setNode(current.right, key, value)));
3825
- };
3826
- const minNode = (current) => current.left ? minNode(current.left) : current;
3827
- const removeNode = (current, key) => {
3828
- if (!current) return void 0;
3829
- if (key < current.key) return rebalance(node(current.key, current.value, removeNode(current.left, key), current.right));
3830
- if (key > current.key) return rebalance(node(current.key, current.value, current.left, removeNode(current.right, key)));
3800
+ /** Local repair after a join descent or a single deletion (height difference at most two). */
3801
+ const balance = (key, value, left, right) => {
3802
+ if (height(left) > height(right) + 1) {
3803
+ const heavy = left;
3804
+ if (height(heavy.left) >= height(heavy.right)) return node(heavy.key, heavy.value, heavy.left, node(key, value, heavy.right, right));
3805
+ const middle = heavy.right;
3806
+ return node(middle.key, middle.value, node(heavy.key, heavy.value, heavy.left, middle.left), node(key, value, middle.right, right));
3807
+ }
3808
+ if (height(right) > height(left) + 1) {
3809
+ const heavy = right;
3810
+ if (height(heavy.right) >= height(heavy.left)) return node(heavy.key, heavy.value, node(key, value, left, heavy.left), heavy.right);
3811
+ const middle = heavy.left;
3812
+ return node(middle.key, middle.value, node(key, value, left, middle.left), node(heavy.key, heavy.value, middle.right, heavy.right));
3813
+ }
3814
+ return node(key, value, left, right);
3815
+ };
3816
+ /** All left keys precede key, all right keys follow it; heights may differ arbitrarily. */
3817
+ const join = (key, value, left, right) => {
3818
+ if (height(left) > height(right) + 1) {
3819
+ profile.collectionIndex.visit();
3820
+ return balance(left.key, left.value, left.left, join(key, value, left.right, right));
3821
+ }
3822
+ if (height(right) > height(left) + 1) {
3823
+ profile.collectionIndex.visit();
3824
+ return balance(right.key, right.value, join(key, value, left, right.left), right.right);
3825
+ }
3826
+ return node(key, value, left, right);
3827
+ };
3828
+ const removeMin = (current) => {
3829
+ profile.collectionIndex.visit();
3831
3830
  if (!current.left) return current.right;
3832
- if (!current.right) return current.left;
3833
- const next = minNode(current.right);
3834
- return rebalance(node(next.key, next.value, current.left, removeNode(current.right, next.key)));
3831
+ return balance(current.key, current.value, removeMin(current.left), current.right);
3832
+ };
3833
+ const concat = (left, right) => {
3834
+ if (!left) return right;
3835
+ if (!right) return left;
3836
+ let first = right;
3837
+ while (first.left) {
3838
+ profile.collectionIndex.visit();
3839
+ first = first.left;
3840
+ }
3841
+ return join(first.key, first.value, left, removeMin(right));
3842
+ };
3843
+ const replace$1 = (current, entry, left, right) => {
3844
+ if (entry && !entry.present) return concat(left, right);
3845
+ const value = entry ? entry.value : current.value;
3846
+ if (left === current.left && right === current.right && Object.is(value, current.value)) return current;
3847
+ return join(current.key, value, left, right);
3848
+ };
3849
+ const edit = (current, key, entry) => {
3850
+ if (!current) return entry.present ? node(key, entry.value) : void 0;
3851
+ profile.collectionIndex.visit();
3852
+ if (key === current.key) return replace$1(current, entry, current.left, current.right);
3853
+ if (key < current.key) {
3854
+ const left = edit(current.left, key, entry);
3855
+ return left === current.left ? current : balance(current.key, current.value, left, current.right);
3856
+ }
3857
+ const right = edit(current.right, key, entry);
3858
+ return right === current.right ? current : balance(current.key, current.value, current.left, right);
3859
+ };
3860
+ /** Builds a new subtree in linear time, ignoring removals of absent keys. */
3861
+ const buildChanges = (keys, changes, start, end) => {
3862
+ let count = 0;
3863
+ for (let i = start; i < end; i++) if (changes.get(keys[i]).present) count++;
3864
+ let cursor = start;
3865
+ const build = (size) => {
3866
+ if (!size) return void 0;
3867
+ const leftSize = size >> 1;
3868
+ const left = build(leftSize);
3869
+ let key;
3870
+ let entry;
3871
+ do {
3872
+ key = keys[cursor++];
3873
+ entry = changes.get(key);
3874
+ } while (!entry.present);
3875
+ return node(key, entry.value, left, build(size - leftSize - 1));
3876
+ };
3877
+ return build(count);
3878
+ };
3879
+ const applyChanges = (current, keys, changes, start, end) => {
3880
+ if (start === end) return current;
3881
+ if (end - start === 1) return edit(current, keys[start], changes.get(keys[start]));
3882
+ if (!current) return buildChanges(keys, changes, start, end);
3883
+ profile.collectionIndex.visit();
3884
+ let low = start;
3885
+ let high = end;
3886
+ while (low < high) {
3887
+ const middle = low + high >>> 1;
3888
+ if (keys[middle] < current.key) low = middle + 1;
3889
+ else high = middle;
3890
+ }
3891
+ const matched = low < end && keys[low] === current.key;
3892
+ const left = applyChanges(current.left, keys, changes, start, low);
3893
+ const right = applyChanges(current.right, keys, changes, matched ? low + 1 : low, end);
3894
+ return replace$1(current, matched ? changes.get(current.key) : void 0, left, right);
3835
3895
  };
3836
3896
  const fromSorted = (entries, start = 0, end = entries.length) => {
3837
3897
  if (start >= end) return void 0;
3838
- const middle = start + end >> 1;
3898
+ const middle = start + end >>> 1;
3839
3899
  const [key, value] = entries[middle];
3840
3900
  return node(key, value, fromSorted(entries, start, middle), fromSorted(entries, middle + 1, end));
3841
3901
  };
3842
- /** Immutable keyed lookup used to keep published collection snapshots durable across revisions. */
3902
+ /** Immutable keyed lookup used to keep collection snapshots durable across revisions. */
3843
3903
  var PersistentKeyedIndex = class PersistentKeyedIndex {
3844
3904
  constructor(root) {
3845
3905
  this.root = root;
@@ -3847,6 +3907,7 @@ var PersistentKeyedIndex = class PersistentKeyedIndex {
3847
3907
  static empty() {
3848
3908
  return new PersistentKeyedIndex();
3849
3909
  }
3910
+ /** The input must contain unique keys. Formal collection order is owned separately. */
3850
3911
  static from(entries) {
3851
3912
  const sorted = [...entries].sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0);
3852
3913
  profile.collectionIndex.build(sorted.length);
@@ -3867,11 +3928,19 @@ var PersistentKeyedIndex = class PersistentKeyedIndex {
3867
3928
  }
3868
3929
  return false;
3869
3930
  }
3870
- set(key, value) {
3871
- return new PersistentKeyedIndex(setNode(this.root, key, value));
3872
- }
3873
- remove(key) {
3874
- return new PersistentKeyedIndex(removeNode(this.root, key));
3931
+ /** Applies accepted final entry intents together; never calls user equality or changes old roots. */
3932
+ apply(changes) {
3933
+ if (!changes.size) return this;
3934
+ profile.collectionIndex.batch(changes.size > 1 ? changes.size : 0);
3935
+ let root;
3936
+ if (changes.size === 1) {
3937
+ const [key, entry] = changes.entries().next().value;
3938
+ root = edit(this.root, key, entry);
3939
+ } else {
3940
+ const keys = [...changes.keys()].sort();
3941
+ root = applyChanges(this.root, keys, changes, 0, keys.length);
3942
+ }
3943
+ return root === this.root ? this : new PersistentKeyedIndex(root);
3875
3944
  }
3876
3945
  };
3877
3946
  //#endregion
@@ -3909,10 +3978,11 @@ const createCollectionState = (initial) => {
3909
3978
  values.clear();
3910
3979
  for (const [key, entry] of staged) if (entry.present) values.set(key, entry.value);
3911
3980
  if (index) index = PersistentKeyedIndex.from(values);
3912
- } else for (const [key, entry] of staged) {
3913
- if (index) index = entry.present ? index.set(key, entry.value) : index.remove(key);
3914
- if (entry.present) values.set(key, entry.value);
3981
+ } else {
3982
+ const nextIndex = index?.apply(staged);
3983
+ for (const [key, entry] of staged) if (entry.present) values.set(key, entry.value);
3915
3984
  else values.delete(key);
3985
+ index = nextIndex;
3916
3986
  }
3917
3987
  ids = nextIds;
3918
3988
  },
@@ -29,6 +29,8 @@ const profile = {
29
29
  segmentCompared: () => {}
30
30
  },
31
31
  collectionIndex: {
32
+ batch: (sortedKeys) => {},
33
+ visit: () => {},
32
34
  node: () => {},
33
35
  build: (items) => {}
34
36
  },
@@ -29,6 +29,8 @@ const profile = {
29
29
  segmentCompared: () => {}
30
30
  },
31
31
  collectionIndex: {
32
+ batch: (sortedKeys) => {},
33
+ visit: () => {},
32
34
  node: () => {},
33
35
  build: (items) => {}
34
36
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "doxum",
3
- "version": "0.1.40",
3
+ "version": "0.1.41",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "description": "Doxum is a typed runtime for complex mutable documents.",
@@ -61,6 +61,16 @@ This file is maintainer-only. Application/library consumers should use [Public A
61
61
  initialization failure releases already-created projection resources and preserves the
62
62
  original error. Document dirty/pending state has one write owner; structural
63
63
  materialization only reads it.
64
+ Collection storage installs an accepted entry batch through one persistent index
65
+ application. The index owns AVL batch merge/balance; storage owns the current Map
66
+ and formal ids; output sealing owns equality and publication. Share untouched
67
+ subtrees and copy the affected path union once for pure value updates. Batch
68
+ structural changes require height-aware join/concat, not just single-edit repair.
69
+ Preserve lazy index creation, durable old snapshots, zero index work for pure
70
+ reorder, and per-install versions for demanded reads inside a Runtime batch.
71
+ Sorting changed keys is internal lookup work, never formal order. Count node
72
+ allocations and sorting separately from selector calls; this is not an O(delta)
73
+ guarantee for all collection maintenance.
64
74
  10. Local sync uses Web Lock leadership and contiguous durable sequence. Writes precede
65
75
  asynchronous persistence. Version 5 / format 3 rejects old databases without editing
66
76
  them. External replace and remote apply are prohibited while attached.