doxum 0.1.28 → 0.1.29

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/index.js CHANGED
@@ -1,6 +1,6 @@
1
- import { C as defineSource, D as projectionRef, E as producerOf, O as profile, S as defineProcessor, T as ownProjections, _ as compileProjectionDependencies, a as createKeyedDependencyRuntime, b as mapRead, c as assertScope, d as ProjectionDisposedError, f as ProjectionError, g as diffCollection, h as createCollectionChange, i as compileKeyedDependencies, l as assertSynchronous, m as collectionHasStructuralChange, n as keyedAbsent, o as outputKind, p as collectionChange, r as assertKeyedProjection, s as createKeyRelation, t as createKeyedTransform, u as createScheduler, v as snapshotDependencyValue, w as isProjection, x as snapshotCollectionView, y as collectionView } from "./transform-B4T3AcJr.js";
1
+ import { A as ownProjections, C as snapshotDependencyValue, D as defineProcessor, E as snapshotCollectionView, M as projectionRef, N as profile, O as defineSource, S as compileProjectionDependencies, T as mapRead, _ as createCollectionChange, a as createKeyedDependencyRuntime, b as sameArray, c as assertScope, d as ProjectionDisposedError, f as ProjectionError, g as collectionHasStructuralChange, h as collectionChangeTouchesKey, i as compileKeyedDependencies, j as producerOf, k as isProjection, l as assertSynchronous, m as collectionChangeIntersects, n as keyedAbsent, o as outputKind, r as assertKeyedProjection, s as createKeyRelation, t as createKeyedTransform, u as createScheduler, v as diffCollection, w as collectionView, x as snapshotArray, y as iterableMatchesArray } from "./transform-BQY9I6il.js";
2
2
  import { n as isPlainObject, r as isRecord, t as installOwn } from "./record-B2yRhYsc.js";
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-B5vdPohL.js";
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
5
5
  const schemaNodeOf = (schema) => schema;
6
6
  const collectionEntryNode = (node) => node.kind === "map" || node.kind === "table" || node.kind === "list" ? node.value : void 0;
@@ -568,18 +568,44 @@ const moveSelection = (current, selection, position) => {
568
568
  selected.add(key);
569
569
  }
570
570
  const moved = [];
571
- const remaining = [];
572
- for (const key of current) (selected.has(key) ? moved : remaining).push(key);
571
+ let remainingLength = 0;
572
+ let anchorIndex = -1;
573
+ const anchoredKey = position && ("before" in position ? position.before : "after" in position ? position.after : void 0);
574
+ for (const key of current) {
575
+ if (selected.has(key)) {
576
+ moved.push(key);
577
+ continue;
578
+ }
579
+ if (key === anchoredKey) anchorIndex = remainingLength;
580
+ remainingLength++;
581
+ }
573
582
  if (moved.length !== selected.size) return { kind: "missing" };
574
- if (!valid(remaining, position)) return { kind: "invalid-anchor" };
575
- const insertion = index(remaining, position);
583
+ let insertion;
584
+ if (!position || "at" in position && position.at === "end") insertion = remainingLength;
585
+ else if ("at" in position) {
586
+ if (position.at !== "start") return { kind: "invalid-anchor" };
587
+ insertion = 0;
588
+ } else {
589
+ if (anchorIndex < 0) return { kind: "invalid-anchor" };
590
+ insertion = "before" in position ? anchorIndex : anchorIndex + 1;
591
+ }
592
+ const order = new Array(current.length);
593
+ let outputIndex = 0;
594
+ let remainingIndex = 0;
595
+ let inserted = false;
596
+ for (const key of current) {
597
+ if (selected.has(key)) continue;
598
+ if (!inserted && remainingIndex === insertion) {
599
+ for (const movedKey of moved) order[outputIndex++] = movedKey;
600
+ inserted = true;
601
+ }
602
+ order[outputIndex++] = key;
603
+ remainingIndex++;
604
+ }
605
+ if (!inserted) for (const movedKey of moved) order[outputIndex++] = movedKey;
576
606
  return {
577
607
  kind: "ok",
578
- order: [
579
- ...remaining.slice(0, insertion),
580
- ...moved,
581
- ...remaining.slice(insertion)
582
- ]
608
+ order
583
609
  };
584
610
  };
585
611
  const index = (keys, anchor) => {
@@ -1372,6 +1398,17 @@ const contains = (parent, child) => {
1372
1398
  }
1373
1399
  return true;
1374
1400
  };
1401
+ /** Compare a virtual `[...base, segment]` address with an existing address without allocating it. */
1402
+ const compareExtended = (base, segment, other) => {
1403
+ profile.address.prefixComparison();
1404
+ const length = base.length + 1;
1405
+ const common = Math.min(length, other.length);
1406
+ for (let index = 0; index < common; index += 1) {
1407
+ profile.address.segmentCompared();
1408
+ if ((index < base.length ? base[index] : segment) !== other[index]) return "disjoint";
1409
+ }
1410
+ return length === other.length ? "equal" : length < other.length ? "ancestor" : "descendant";
1411
+ };
1375
1412
  //#endregion
1376
1413
  //#region core/src/impact.ts
1377
1414
  const createImpact = (schema, changes) => {
@@ -1798,7 +1835,7 @@ const reconstructBefore = (node, current, records, offset) => {
1798
1835
  };
1799
1836
  const sealMembers = (fact, changes) => {
1800
1837
  const members = [];
1801
- for (const member of fact.members?.values() ?? []) {
1838
+ if (fact.members) for (const member of fact.members.values()) {
1802
1839
  if (!member) continue;
1803
1840
  const key = memberKey(fact, member.key);
1804
1841
  const present = Object.hasOwn(fact.parent, key);
@@ -1882,7 +1919,7 @@ var ChangeRecorder = class {
1882
1919
  absorb(at, node, value, present) {
1883
1920
  const index = this.indexGroups();
1884
1921
  const children = [];
1885
- index.query((fact) => {
1922
+ index.forEachOverlap(at, (fact) => {
1886
1923
  if (fact.kind === "members") {
1887
1924
  if (fact.at.length < at.length) {
1888
1925
  if (fact.at.length + 1 === at.length) {
@@ -1901,18 +1938,19 @@ var ChangeRecorder = class {
1901
1938
  at: fact.at,
1902
1939
  group: fact
1903
1940
  });
1904
- for (const member of fact.members?.values() ?? []) {
1941
+ if (fact.members) for (const member of fact.members.values()) {
1905
1942
  if (!member) continue;
1906
- const memberAt = fact.at.concat(member.key);
1907
- if (contains(at, memberAt)) children.push({
1943
+ const compared = compareExtended(fact.at, member.key, at);
1944
+ if (compared !== "equal" && compared !== "descendant") continue;
1945
+ children.push({
1908
1946
  kind: "member",
1909
- at: memberAt,
1947
+ at: fact.at.concat(member.key),
1910
1948
  group: fact,
1911
1949
  member
1912
1950
  });
1913
1951
  }
1914
1952
  } else if (contains(at, fact.at)) children.push(fact);
1915
- })(at);
1953
+ });
1916
1954
  if (present) value = reconstructBefore(node, value, children, at.length);
1917
1955
  this.releaseCovered(children);
1918
1956
  return value;
@@ -1930,7 +1968,9 @@ var ChangeRecorder = class {
1930
1968
  (changedGroups ??= /* @__PURE__ */ new Set()).add(child.group);
1931
1969
  } else this.unregister(child);
1932
1970
  }
1933
- for (const group of changedGroups ?? []) if (emptyGroup(group)) this.unregister(group);
1971
+ if (changedGroups) {
1972
+ for (const group of changedGroups) if (emptyGroup(group)) this.unregister(group);
1973
+ }
1934
1974
  }
1935
1975
  register(fact) {
1936
1976
  this.facts.add(fact);
@@ -2879,12 +2919,7 @@ const createKeyedGet = (source, key, equality = Object.is) => {
2879
2919
  } else if (sourceRevision !== collection.revision) {
2880
2920
  const change = collection.change;
2881
2921
  if (!change || change.kind === "reset") affected = true;
2882
- else if (selected !== void 0) {
2883
- for (const changedKey of collectionChange.keys(change)) if (changedKey === selected) {
2884
- affected = true;
2885
- break;
2886
- }
2887
- }
2922
+ else if (selected !== void 0) affected = collectionChangeTouchesKey(change, selected);
2888
2923
  }
2889
2924
  if (affected) output.output.set(selected === void 0 ? void 0 : collection.read.get(selected));
2890
2925
  sourceRevision = collection.revision;
@@ -3048,9 +3083,7 @@ function createKeyedGroupBy(source, dependenciesOrSelector, maybeSelector) {
3048
3083
  outputs: [{
3049
3084
  kind: "collection",
3050
3085
  equality: (previous, next) => {
3051
- const left = previous;
3052
- const right = next;
3053
- return left.length === right.length && left.every((key, index) => key === right[index]);
3086
+ return sameArray(previous, next);
3054
3087
  }
3055
3088
  }],
3056
3089
  create: () => {
@@ -3094,11 +3127,14 @@ function createKeyedGroupBy(source, dependenciesOrSelector, maybeSelector) {
3094
3127
  output.output.set(group, Object.freeze(members));
3095
3128
  };
3096
3129
  const publishGroupOrder = (output) => {
3097
- const order = [...groupRanks.entries()].sort(([leftKey, left], [rightKey, right]) => {
3130
+ const order = [...groupRanks.keys()];
3131
+ order.sort((leftKey, rightKey) => {
3132
+ const left = groupRanks.get(leftKey);
3133
+ const right = groupRanks.get(rightKey);
3098
3134
  if (left.source !== right.source) return left.source - right.source;
3099
3135
  if (left.selector !== right.selector) return left.selector - right.selector;
3100
3136
  return leftKey < rightKey ? -1 : leftKey > rightKey ? 1 : 0;
3101
- }).map(([group]) => group);
3137
+ });
3102
3138
  output.output.order(order);
3103
3139
  };
3104
3140
  const rebuild = (driver, sources, output) => {
@@ -3367,13 +3403,14 @@ const createPublishedCollection = () => {
3367
3403
  },
3368
3404
  publish: (staged, nextIds, reset) => {
3369
3405
  if (reset) {
3370
- const entries = [];
3371
- for (const [key, entry] of staged) if (entry.present) entries.push([key, entry.value]);
3372
- index = PersistentKeyedIndex.from(entries);
3373
3406
  values.clear();
3374
- } else for (const [key, entry] of staged) index = entry.present ? index.set(key, entry.value) : index.remove(key);
3375
- for (const [key, entry] of staged) if (entry.present) values.set(key, entry.value);
3376
- else values.delete(key);
3407
+ for (const [key, entry] of staged) if (entry.present) values.set(key, entry.value);
3408
+ index = PersistentKeyedIndex.from(values);
3409
+ } else for (const [key, entry] of staged) {
3410
+ index = entry.present ? index.set(key, entry.value) : index.remove(key);
3411
+ if (entry.present) values.set(key, entry.value);
3412
+ else values.delete(key);
3413
+ }
3377
3414
  ids = nextIds;
3378
3415
  },
3379
3416
  release: () => {
@@ -3386,7 +3423,7 @@ const createPublishedCollection = () => {
3386
3423
  /** Owns staged keyed intent, exact transitions and published collection state. */
3387
3424
  const createCollectionOutput = (binding) => {
3388
3425
  const published = createPublishedCollection();
3389
- let staged = /* @__PURE__ */ new Map();
3426
+ const staged = /* @__PURE__ */ new Map();
3390
3427
  let nextIds = published.ids();
3391
3428
  let change;
3392
3429
  let initialized = false;
@@ -3402,12 +3439,15 @@ const createCollectionOutput = (binding) => {
3402
3439
  return entry ? entry.present ? entry.value : void 0 : cleared ? void 0 : published.get(key);
3403
3440
  };
3404
3441
  const deriveIds = () => {
3405
- const result = cleared ? [] : published.ids().filter(hasNext);
3442
+ const result = [];
3443
+ if (!cleared) {
3444
+ for (const key of published.ids()) if (hasNext(key)) result.push(key);
3445
+ }
3406
3446
  for (const [key, entry] of staged) if (entry.present && (cleared || !published.has(key))) result.push(key);
3407
3447
  return result;
3408
3448
  };
3409
3449
  const begin = (active, initialize) => {
3410
- staged = /* @__PURE__ */ new Map();
3450
+ staged.clear();
3411
3451
  nextIds = published.ids();
3412
3452
  change = void 0;
3413
3453
  explicitOrder = void 0;
@@ -3424,7 +3464,9 @@ const createCollectionOutput = (binding) => {
3424
3464
  },
3425
3465
  ids: () => {
3426
3466
  assertScope(active);
3427
- return next ? Object.freeze(explicitOrder ? [...explicitOrder] : [...deriveIds()]) : published.ids();
3467
+ if (!next) return published.ids();
3468
+ if (explicitOrder) return explicitOrder;
3469
+ return Object.freeze(deriveIds());
3428
3470
  }
3429
3471
  });
3430
3472
  return {
@@ -3445,7 +3487,7 @@ const createCollectionOutput = (binding) => {
3445
3487
  },
3446
3488
  order: (order) => {
3447
3489
  assertScope(active);
3448
- explicitOrder = Object.freeze([...order]);
3490
+ explicitOrder = snapshotArray(order);
3449
3491
  }
3450
3492
  }
3451
3493
  };
@@ -3455,8 +3497,6 @@ const createCollectionOutput = (binding) => {
3455
3497
  for (const key of published.keys()) if (!staged.has(key)) staged.set(key, { present: false });
3456
3498
  }
3457
3499
  profile.projection("touchedKeys", staged.size);
3458
- const added = /* @__PURE__ */ new Set();
3459
- const removed = /* @__PURE__ */ new Set();
3460
3500
  const addedEntries = [];
3461
3501
  const updatedEntries = [];
3462
3502
  const removedEntries = [];
@@ -3464,7 +3504,6 @@ const createCollectionOutput = (binding) => {
3464
3504
  const existed = published.has(key);
3465
3505
  if (entry.present) {
3466
3506
  if (!existed) {
3467
- added.add(key);
3468
3507
  addedEntries.push({
3469
3508
  key,
3470
3509
  after: entry.value
@@ -3480,19 +3519,28 @@ const createCollectionOutput = (binding) => {
3480
3519
  else if (!nextReset) staged.delete(key);
3481
3520
  continue;
3482
3521
  }
3483
- if (existed) {
3484
- removed.add(key);
3485
- removedEntries.push({
3486
- key,
3487
- before: published.get(key)
3488
- });
3489
- } else staged.delete(key);
3522
+ if (existed) removedEntries.push({
3523
+ key,
3524
+ before: published.get(key)
3525
+ });
3526
+ else staged.delete(key);
3490
3527
  }
3491
3528
  const ids = published.ids();
3492
- if (explicitOrder || added.size || removed.size) {
3493
- const order = explicitOrder ?? [...ids.filter((key) => !removed.has(key)), ...added];
3494
- if (new Set(order).size !== order.length || order.length !== published.size() + added.size - removed.size || order.some((key) => !hasNext(key))) throw new TypeError("Projection order must contain every key exactly once.");
3495
- nextIds = ids.length === order.length && ids.every((key, index) => key === order[index]) ? ids : Object.freeze([...order]);
3529
+ if (explicitOrder) {
3530
+ const expectedLength = published.size() + addedEntries.length - removedEntries.length;
3531
+ const seen = /* @__PURE__ */ new Set();
3532
+ for (const key of explicitOrder) {
3533
+ if (seen.has(key) || !hasNext(key)) throw new TypeError("Projection order must contain every key exactly once.");
3534
+ seen.add(key);
3535
+ }
3536
+ if (seen.size !== expectedLength) throw new TypeError("Projection order must contain every key exactly once.");
3537
+ nextIds = sameArray(ids, explicitOrder) ? ids : explicitOrder;
3538
+ } else if (addedEntries.length || removedEntries.length) {
3539
+ const order = new Array(published.size() + addedEntries.length - removedEntries.length);
3540
+ let index = 0;
3541
+ for (const key of ids) if (hasNext(key)) order[index++] = key;
3542
+ for (const entry of addedEntries) order[index++] = entry.key;
3543
+ nextIds = sameArray(ids, order) ? ids : Object.freeze(order);
3496
3544
  } else nextIds = ids;
3497
3545
  profile.projection("changedKeys", addedEntries.length + updatedEntries.length + removedEntries.length);
3498
3546
  change = nextReset ? Object.freeze({ kind: "reset" }) : createCollectionChange({
@@ -3809,18 +3857,19 @@ const createProcessor = (scheduler, definition, dependencies) => {
3809
3857
  };
3810
3858
  //#endregion
3811
3859
  //#region core/src/projection/readable/listeners.ts
3812
- /** Exhaustively notifies one projection-readable listener snapshot. */
3813
- const notifyProjectionListeners = (listeners, message) => {
3814
- const snapshot = [...listeners];
3815
- const failures = [];
3816
- for (const listener of snapshot) try {
3860
+ /** Exhaustively notifies an already-stable projection listener snapshot. */
3861
+ const notifyProjectionListenerSnapshot = (listeners, message) => {
3862
+ let failures;
3863
+ for (const listener of listeners) try {
3817
3864
  listener();
3818
3865
  } catch (error) {
3819
- failures.push(error);
3866
+ (failures ??= []).push(error);
3820
3867
  }
3821
- if (failures.length === 1) throw failures[0];
3822
- if (failures.length) throw new AggregateError(failures, message);
3868
+ if (failures?.length === 1) throw failures[0];
3869
+ if (failures?.length) throw new AggregateError(failures, message);
3823
3870
  };
3871
+ /** Snapshots a mutable listener collection before exhaustive notification. */
3872
+ const notifyProjectionListeners = (listeners, message) => notifyProjectionListenerSnapshot([...listeners], message);
3824
3873
  //#endregion
3825
3874
  //#region core/src/projection/readable/selection.ts
3826
3875
  let activeTracker;
@@ -3875,8 +3924,7 @@ const selectionAffects = (selection, change) => {
3875
3924
  if (!change || change.kind === "reset") return true;
3876
3925
  if (selection.all) return true;
3877
3926
  if (selection.structure && collectionHasStructuralChange(change)) return true;
3878
- for (const key of collectionChange.keys(change)) if (selection.keys.has(key)) return true;
3879
- return false;
3927
+ return collectionChangeIntersects(change, selection.keys);
3880
3928
  };
3881
3929
  const createDirectReadable = (source) => Object.freeze({
3882
3930
  current: source.current,
@@ -3918,7 +3966,7 @@ const createSelectorReadable = (source, selector, equality = Object.is) => {
3918
3966
  activeTracker = previousTracker;
3919
3967
  }
3920
3968
  const nextSelection = Object.freeze({
3921
- keys: new Set(tracker.keys),
3969
+ keys: tracker.keys,
3922
3970
  all: tracker.all || isMapLike(current) && tracker.keys.size === 0 && !tracker.structure,
3923
3971
  structure: tracker.structure
3924
3972
  });
@@ -4160,10 +4208,11 @@ const clearDocumentDirty = (dirty) => {
4160
4208
  dirty.treeNodes.clear();
4161
4209
  dirty.children.clear();
4162
4210
  };
4163
- const locate = (dirty, path) => {
4211
+ const locateRange = (dirty, path, start, tail) => {
4164
4212
  let current = dirty;
4165
- for (const key of path) {
4213
+ for (let index = start; index < path.length; index++) {
4166
4214
  if (current.replace) return void 0;
4215
+ const key = path[index];
4167
4216
  let child = current.children.get(key);
4168
4217
  if (!child) {
4169
4218
  child = createDocumentDirty();
@@ -4171,10 +4220,17 @@ const locate = (dirty, path) => {
4171
4220
  }
4172
4221
  current = child;
4173
4222
  }
4174
- return current;
4223
+ if (tail === void 0) return current;
4224
+ if (current.replace) return void 0;
4225
+ let child = current.children.get(tail);
4226
+ if (!child) {
4227
+ child = createDocumentDirty();
4228
+ current.children.set(tail, child);
4229
+ }
4230
+ return child;
4175
4231
  };
4176
- const markDocumentReplace = (dirty, path) => {
4177
- const current = locate(dirty, path);
4232
+ const replaceAt = (dirty, path, start, tail) => {
4233
+ const current = locateRange(dirty, path, start, tail);
4178
4234
  if (!current) return;
4179
4235
  current.replace = true;
4180
4236
  current.order = false;
@@ -4182,15 +4238,18 @@ const markDocumentReplace = (dirty, path) => {
4182
4238
  current.treeNodes.clear();
4183
4239
  current.children.clear();
4184
4240
  };
4185
- const markDocumentOrder = (dirty, path) => {
4186
- const current = locate(dirty, path);
4241
+ const markDocumentReplace = (dirty, path) => {
4242
+ replaceAt(dirty, path, 0);
4243
+ };
4244
+ const markDocumentOrderRange = (dirty, path, start) => {
4245
+ const current = locateRange(dirty, path, start);
4187
4246
  if (current && !current.replace) current.order = true;
4188
4247
  };
4189
- const markDocumentTree = (dirty, path, root, nodes) => {
4190
- const current = locate(dirty, path);
4248
+ const markDocumentTree = (dirty, path, start, root, nodes) => {
4249
+ const current = locateRange(dirty, path, start);
4191
4250
  if (!current || current.replace) return;
4192
4251
  current.treeRoot ||= root;
4193
- for (const id of nodes) current.treeNodes.add(id);
4252
+ for (const node of nodes) current.treeNodes.add(node.id);
4194
4253
  };
4195
4254
  const createCollectionPending = () => ({
4196
4255
  reset: false,
@@ -4220,19 +4279,19 @@ const mergeValueChanges = (selector, dirty, changes) => {
4220
4279
  const at = selector.address;
4221
4280
  for (const change of changes.changes) {
4222
4281
  if (change.kind === "reset") {
4223
- markDocumentReplace(dirty, []);
4282
+ replaceAt(dirty, at, at.length);
4224
4283
  return;
4225
4284
  }
4226
4285
  if (change.kind === "members") {
4227
4286
  for (const member of change.members) {
4228
- const changed = [...change.at, member.key];
4229
- if (contains(changed, at)) markDocumentReplace(dirty, []);
4230
- else if (contains(at, changed)) markDocumentReplace(dirty, changed.slice(at.length));
4287
+ const compared = compareExtended(change.at, member.key, at);
4288
+ if (compared === "ancestor" || compared === "equal") replaceAt(dirty, at, at.length);
4289
+ else if (compared === "descendant") replaceAt(dirty, change.at, at.length, member.key);
4231
4290
  }
4232
- if (change.order && contains(at, change.at)) markDocumentOrder(dirty, change.at.slice(at.length));
4291
+ if (change.order && contains(at, change.at)) markDocumentOrderRange(dirty, change.at, at.length);
4233
4292
  continue;
4234
4293
  }
4235
- if (contains(at, change.at)) markDocumentTree(dirty, change.at.slice(at.length), change.before !== change.after, change.nodes.map((node) => node.id));
4294
+ if (contains(at, change.at)) markDocumentTree(dirty, change.at, at.length, change.before !== change.after, change.nodes);
4236
4295
  }
4237
4296
  };
4238
4297
  const mergeCollectionChanges = (selector, pending, changes) => {
@@ -4244,13 +4303,15 @@ const mergeCollectionChanges = (selector, pending, changes) => {
4244
4303
  return;
4245
4304
  }
4246
4305
  if (selector.tree?.kind === "nodes") {
4247
- if (change.kind === "members") {
4248
- for (const member of change.members) if (contains([...change.at, member.key], at)) {
4306
+ if (change.kind === "members") for (const member of change.members) {
4307
+ const compared = compareExtended(change.at, member.key, at);
4308
+ if (compared === "ancestor" || compared === "equal") {
4249
4309
  resetCollectionPending(pending);
4250
4310
  return;
4251
4311
  }
4252
- } else if (change.at.length === at.length && contains(change.at, at)) for (const node of change.nodes) {
4253
- markDocumentReplace(dirtyEntry(pending, node.id), []);
4312
+ }
4313
+ else if (change.at.length === at.length && contains(change.at, at)) for (const node of change.nodes) {
4314
+ replaceAt(dirtyEntry(pending, node.id), at, at.length);
4254
4315
  pending.order ||= node.kind !== "updated";
4255
4316
  }
4256
4317
  else if (contains(change.at, at)) {
@@ -4261,34 +4322,27 @@ const mergeCollectionChanges = (selector, pending, changes) => {
4261
4322
  }
4262
4323
  if (change.kind === "members") {
4263
4324
  for (const member of change.members) {
4264
- const changed = [...change.at, member.key];
4265
- if (contains(changed, at)) {
4266
- resetCollectionPending(pending);
4267
- return;
4268
- }
4269
- if (!contains(at, changed)) continue;
4270
- const relative = changed.slice(at.length);
4271
- if (!relative.length) {
4325
+ const compared = compareExtended(change.at, member.key, at);
4326
+ if (compared === "ancestor" || compared === "equal") {
4272
4327
  resetCollectionPending(pending);
4273
4328
  return;
4274
4329
  }
4275
- markDocumentReplace(dirtyEntry(pending, relative[0]), relative.slice(1));
4276
- if (relative.length === 1 && member.kind !== "updated") pending.order = true;
4277
- }
4278
- if (change.order && contains(at, change.at)) {
4279
- const relative = change.at.slice(at.length);
4280
- if (!relative.length) pending.order = true;
4281
- else markDocumentOrder(dirtyEntry(pending, relative[0]), relative.slice(1));
4330
+ if (compared !== "descendant") continue;
4331
+ if (at.length === change.at.length) {
4332
+ replaceAt(dirtyEntry(pending, member.key), change.at, change.at.length);
4333
+ if (member.kind !== "updated") pending.order = true;
4334
+ } else replaceAt(dirtyEntry(pending, change.at[at.length]), change.at, at.length + 1, member.key);
4282
4335
  }
4336
+ if (change.order && contains(at, change.at)) if (change.at.length === at.length) pending.order = true;
4337
+ else markDocumentOrderRange(dirtyEntry(pending, change.at[at.length]), change.at, at.length + 1);
4283
4338
  continue;
4284
4339
  }
4285
4340
  if (!contains(at, change.at)) continue;
4286
- const relative = change.at.slice(at.length);
4287
- if (!relative.length) {
4341
+ if (change.at.length === at.length) {
4288
4342
  resetCollectionPending(pending);
4289
4343
  return;
4290
4344
  }
4291
- markDocumentTree(dirtyEntry(pending, relative[0]), relative.slice(1), change.before !== change.after, change.nodes.map((node) => node.id));
4345
+ markDocumentTree(dirtyEntry(pending, change.at[at.length]), change.at, at.length + 1, change.before !== change.after, change.nodes);
4292
4346
  }
4293
4347
  };
4294
4348
  //#endregion
@@ -4417,16 +4471,29 @@ const stageCollectionRead = (state, read, active, options) => {
4417
4471
  evaluation.output.order(ids);
4418
4472
  } else {
4419
4473
  const previous = state.current();
4420
- const keys = options.candidates ? [...options.candidates] : [...new Set([...previous.ids(), ...read.ids()])];
4421
- if (!options.candidates) profile.collectionView.idsScanned(keys.length);
4422
- for (const key of keys) if (read.has(key)) {
4474
+ let currentIds;
4475
+ const ids = () => currentIds ??= read.ids();
4476
+ if (options.candidates) for (const key of options.candidates) if (read.has(key)) {
4423
4477
  profile.collectionView.mapped();
4424
4478
  evaluation.output.set(key, read.get(key));
4425
4479
  } else evaluation.output.remove(key);
4480
+ else {
4481
+ const previousIds = previous.ids();
4482
+ const nextIds = ids();
4483
+ profile.collectionView.idsScanned(previousIds.length + nextIds.length);
4484
+ for (const key of previousIds) if (read.has(key)) {
4485
+ profile.collectionView.mapped();
4486
+ evaluation.output.set(key, read.get(key));
4487
+ } else evaluation.output.remove(key);
4488
+ for (const key of nextIds) if (!previous.has(key)) {
4489
+ profile.collectionView.mapped();
4490
+ evaluation.output.set(key, read.get(key));
4491
+ }
4492
+ }
4426
4493
  if (options.orderMayChange || !options.candidates) {
4427
- const ids = read.ids();
4428
- if (options.candidates) profile.collectionView.idsScanned(ids.length);
4429
- evaluation.output.order(ids);
4494
+ const nextIds = ids();
4495
+ if (options.candidates) profile.collectionView.idsScanned(nextIds.length);
4496
+ evaluation.output.order(nextIds);
4430
4497
  }
4431
4498
  }
4432
4499
  return state.seal(options.reset, options.isEqual ?? Object.is);
@@ -4720,13 +4787,15 @@ const createDocumentSourceRegistry = (scheduler) => {
4720
4787
  const snapshots = /* @__PURE__ */ new Map();
4721
4788
  const replacement = createDocumentDirty();
4722
4789
  if (pending.reset) markDocumentReplace(replacement, []);
4790
+ let cachedIds;
4723
4791
  const ids = () => {
4724
4792
  assertScope(active);
4725
- if (treeNodes) return node?.kind === "tree" && isRecord(value) && isRecord(value.nodes) ? Object.freeze(Object.keys(value.nodes)) : Object.freeze([]);
4726
- if (node?.kind === "map" && isRecord(value)) return Object.freeze(Object.keys(value));
4727
- if (node?.kind === "table" && isRecord(value) && Array.isArray(value.ids)) return Object.freeze([...value.ids]);
4728
- if (node?.kind === "list" && Array.isArray(value)) return Object.freeze(toArray(indexedKeys(value, node.keyOf)));
4729
- return Object.freeze([]);
4793
+ if (cachedIds) return cachedIds;
4794
+ if (treeNodes) return cachedIds = node?.kind === "tree" && isRecord(value) && isRecord(value.nodes) ? Object.freeze(Object.keys(value.nodes)) : Object.freeze([]);
4795
+ if (node?.kind === "map" && isRecord(value)) return cachedIds = Object.freeze(Object.keys(value));
4796
+ if (node?.kind === "table" && isRecord(value) && Array.isArray(value.ids)) return cachedIds = Object.freeze([...value.ids]);
4797
+ if (node?.kind === "list" && Array.isArray(value)) return cachedIds = Object.freeze(toArray(indexedKeys(value, node.keyOf)));
4798
+ return cachedIds = Object.freeze([]);
4730
4799
  };
4731
4800
  const has = (key) => {
4732
4801
  assertScope(active);
@@ -5214,7 +5283,6 @@ const createSourceRegistry = (scheduler) => {
5214
5283
  };
5215
5284
  //#endregion
5216
5285
  //#region core/src/projection/readable/keyed.ts
5217
- const sameKeys = (previous, next) => previous.length === next.length && previous.every((key, index) => key === next[index]);
5218
5286
  /** Runtime-owned keyed consumer family over one materialized collection output. */
5219
5287
  const createProjectionItems = (source, checkOwner) => {
5220
5288
  if (source.kind !== "collection") throw new TypeError("Projection items require a keyed collection projection.");
@@ -5234,7 +5302,11 @@ const createProjectionItems = (source, checkOwner) => {
5234
5302
  for (const listener of listeners) notifications.push(listener);
5235
5303
  };
5236
5304
  const flushNotifications = () => {
5237
- notifyProjectionListeners(notifications.splice(0), "Projection item listeners failed.");
5305
+ try {
5306
+ notifyProjectionListenerSnapshot(notifications, "Projection item listeners failed.");
5307
+ } finally {
5308
+ notifications.length = 0;
5309
+ }
5238
5310
  };
5239
5311
  const unregisterWaiting = (state) => {
5240
5312
  const states = waiting.get(state.key);
@@ -5277,16 +5349,11 @@ const createProjectionItems = (source, checkOwner) => {
5277
5349
  publishState(state, void 0);
5278
5350
  state.ended = true;
5279
5351
  };
5280
- const statesFor = (key) => {
5281
- const result = [];
5352
+ const terminateKey = (key) => {
5282
5353
  const canonical = active.get(key);
5283
- if (canonical) result.push(canonical);
5354
+ if (canonical) terminateState(canonical);
5284
5355
  const extra = followers.get(key);
5285
- if (extra) result.push(...extra);
5286
- return result;
5287
- };
5288
- const terminateKey = (key) => {
5289
- for (const state of statesFor(key)) terminateState(state);
5356
+ if (extra) for (const state of extra) terminateState(state);
5290
5357
  active.delete(key);
5291
5358
  followers.delete(key);
5292
5359
  };
@@ -5297,12 +5364,14 @@ const createProjectionItems = (source, checkOwner) => {
5297
5364
  for (const state of states) attachActive(state, value);
5298
5365
  };
5299
5366
  const updateKey = (key, value) => {
5300
- for (const state of statesFor(key)) publishState(state, value);
5367
+ const canonical = active.get(key);
5368
+ if (canonical) publishState(canonical, value);
5369
+ const extra = followers.get(key);
5370
+ if (extra) for (const state of extra) publishState(state, value);
5301
5371
  };
5302
5372
  const updateKeys = (current) => {
5303
- const next = Object.freeze([...current.keys()]);
5304
- if (sameKeys(keysValue, next)) return;
5305
- keysValue = next;
5373
+ if (iterableMatchesArray(current.keys(), keysValue)) return;
5374
+ keysValue = Object.freeze([...current.keys()]);
5306
5375
  keysRevision++;
5307
5376
  notify(keyListeners);
5308
5377
  };
@@ -5345,9 +5414,9 @@ const createProjectionItems = (source, checkOwner) => {
5345
5414
  return state;
5346
5415
  };
5347
5416
  const reconcile = (current) => {
5348
- for (const key of [...active.keys()]) if (!current.has(key)) terminateKey(key);
5417
+ for (const key of active.keys()) if (!current.has(key)) terminateKey(key);
5349
5418
  else updateKey(key, current.get(key));
5350
- for (const key of [...waiting.keys()]) if (current.has(key)) activateWaiting(key, current.get(key));
5419
+ for (const key of waiting.keys()) if (current.has(key)) activateWaiting(key, current.get(key));
5351
5420
  updateKeys(current);
5352
5421
  };
5353
5422
  const onChange = (change) => {