doxum 0.1.32 → 0.1.34

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.
@@ -119,22 +119,40 @@ const mapRead = (value) => {
119
119
  /** Immutable map-like view over a CollectionRead. */
120
120
  const collectionView = (read, knownIds) => {
121
121
  const ids = knownIds ?? read.ids();
122
- const entries = function* () {
123
- for (const key of ids) yield [key, read.get(key)];
124
- };
125
- const values = function* () {
126
- for (const key of ids) yield read.get(key);
122
+ const iterate = (select) => {
123
+ read.ids();
124
+ let index = 0;
125
+ return {
126
+ next() {
127
+ read.ids();
128
+ if (index === ids.length) return {
129
+ done: true,
130
+ value: void 0
131
+ };
132
+ return {
133
+ done: false,
134
+ value: select(ids[index++])
135
+ };
136
+ },
137
+ [Symbol.iterator]() {
138
+ return this;
139
+ }
140
+ };
127
141
  };
142
+ const entries = () => iterate((key) => [key, read.get(key)]);
143
+ const values = () => iterate((key) => read.get(key));
128
144
  const view = {
129
145
  get: (key) => read.get(key),
130
146
  has: (key) => read.has(key),
131
147
  get size() {
148
+ read.ids();
132
149
  return ids.length;
133
150
  },
134
- keys: () => ids[Symbol.iterator](),
151
+ keys: () => iterate((key) => key),
135
152
  values,
136
153
  entries,
137
154
  forEach: (callback, thisArg) => {
155
+ read.ids();
138
156
  for (const key of ids) callback.call(thisArg, read.get(key), key, view);
139
157
  },
140
158
  [Symbol.iterator]: entries
@@ -183,141 +201,36 @@ const compileProjectionDependencies = (dependencies, label) => {
183
201
  });
184
202
  };
185
203
  //#endregion
186
- //#region core/src/value/array.ts
187
- /** Allocation-free strict equality for ordered readonly arrays. */
188
- const sameArray = (left, right) => {
189
- if (left === right) return true;
190
- if (left.length !== right.length) return false;
191
- for (let index = 0; index < left.length; index++) if (left[index] !== right[index]) return false;
192
- return true;
193
- };
194
- /** Owns an immutable shallow snapshot while reusing arrays that are already frozen. */
195
- const snapshotArray = (values) => Object.isFrozen(values) ? values : Object.freeze([...values]);
196
- /** Compares an iterable with an ordered array without materializing the iterable. */
197
- const iterableMatchesArray = (values, expected) => {
198
- let index = 0;
199
- for (const value of values) {
200
- if (index >= expected.length || value !== expected[index]) return false;
201
- index++;
202
- }
203
- return index === expected.length;
204
- };
205
- //#endregion
206
- //#region core/src/projection/collection/change.ts
207
- const transitionContains = (entries, key) => {
208
- for (const entry of entries) if (entry.key === key) return true;
209
- return false;
210
- };
211
- const transitionSet = (entries) => {
212
- if (entries.length <= 4) return void 0;
213
- const keys = /* @__PURE__ */ new Set();
214
- for (const entry of entries) keys.add(entry.key);
215
- return keys;
216
- };
217
- const nextExcluding = (iterator, excluded, index) => {
218
- while (true) {
219
- const next = iterator.next();
220
- if (next.done || !(index ? index.has(next.value) : transitionContains(excluded, next.value))) return next;
221
- }
222
- };
223
- /** Compares common-member order from known transitions without materializing common arrays. */
224
- const transitionOrderChanged = (before, after, removed, added) => {
225
- const removedKeys = transitionSet(removed);
226
- const addedKeys = transitionSet(added);
227
- const beforeIterator = before[Symbol.iterator]();
228
- const afterIterator = after[Symbol.iterator]();
229
- while (true) {
230
- const left = nextExcluding(beforeIterator, removed, removedKeys);
231
- const right = nextExcluding(afterIterator, added, addedKeys);
232
- if (left.done || right.done) return left.done !== right.done;
233
- if (left.value !== right.value) return true;
234
- }
235
- };
236
- const nextPresentIn = (iterator, membership) => {
237
- while (true) {
238
- const next = iterator.next();
239
- if (next.done || membership.has(next.value)) return next;
204
+ //#region core/src/projection/keyed/invalidation.ts
205
+ /** Shared driver/dependency invalidation policy; owns no revisions or member cache. */
206
+ const collectKeyedInvalidation = (driver, sources, dependencies, reset) => {
207
+ const changed = dependencies.driverChanged(driver);
208
+ const change = changed ? driver.change : void 0;
209
+ if (reset || changed && (!change || change.kind === "reset")) return {
210
+ rebuild: true,
211
+ keys: driver.read.ids(),
212
+ removed: [],
213
+ structural: true
214
+ };
215
+ const dirty = /* @__PURE__ */ new Set();
216
+ let removed = [];
217
+ let structural = false;
218
+ if (change?.kind === "incremental") {
219
+ removed = change.removed;
220
+ for (const entry of change.added) dirty.add(entry.key);
221
+ for (const entry of change.updated) dirty.add(entry.key);
222
+ structural = Boolean(change.added.length || change.removed.length || change.order);
240
223
  }
241
- };
242
- /** Compares common-member order between two map snapshots without key snapshots or closures. */
243
- const mapOrderChanged = (before, after) => {
244
- const beforeIterator = before.keys();
245
- const afterIterator = after.keys();
246
- while (true) {
247
- const left = nextPresentIn(beforeIterator, after);
248
- const right = nextPresentIn(afterIterator, before);
249
- if (left.done || right.done) return left.done !== right.done;
250
- if (left.value !== right.value) return true;
224
+ const all = dependencies.collectInvalidated(sources, dirty);
225
+ if (!all) {
226
+ for (const key of dirty) if (!driver.read.has(key)) dirty.delete(key);
251
227
  }
252
- };
253
- const sealIncrementalChange = (added, updated, removed, order) => {
254
- if (!added.length && !updated.length && !removed.length && !order) return void 0;
255
- return Object.freeze({
256
- kind: "incremental",
257
- added: Object.freeze(added),
258
- updated: Object.freeze(updated),
259
- removed: Object.freeze(removed),
260
- ...order ? { order: Object.freeze({
261
- before: snapshotArray(order.before),
262
- after: snapshotArray(order.after)
263
- }) } : {}
264
- });
265
- };
266
- /** Builds the canonical incremental CollectionChange from already-known net transitions. */
267
- const createCollectionChange = (input) => {
268
- let orderChanged = false;
269
- if (input.beforeOrder !== input.afterOrder) if (!input.added.length && !input.removed.length) orderChanged = !sameArray(input.beforeOrder, input.afterOrder);
270
- else orderChanged = transitionOrderChanged(input.beforeOrder, input.afterOrder, input.removed, input.added);
271
- return sealIncrementalChange(input.added, input.updated, input.removed, orderChanged ? {
272
- before: input.beforeOrder,
273
- after: input.afterOrder
274
- } : void 0);
275
- };
276
- /** Exact fallback diff between two published map-like snapshots. */
277
- const diffCollection = (previous, current, isEqual = Object.is) => {
278
- const added = [];
279
- const updated = [];
280
- const removed = [];
281
- for (const [key, before] of previous) if (!current.has(key)) removed.push({
282
- key,
283
- before
284
- });
285
- else {
286
- const after = current.get(key);
287
- if (!isEqual(before, after)) updated.push({
288
- key,
289
- before,
290
- after
291
- });
292
- }
293
- for (const [key, after] of current) if (!previous.has(key)) added.push({
294
- key,
295
- after
296
- });
297
- return sealIncrementalChange(added, updated, removed, mapOrderChanged(previous, current) ? {
298
- before: Object.freeze([...previous.keys()]),
299
- after: Object.freeze([...current.keys()])
300
- } : void 0);
301
- };
302
- const changedKeys = function* (change) {
303
- if (change.kind !== "incremental") throw new TypeError("collectionChange.keys requires an incremental CollectionChange.");
304
- for (const entry of change.added) yield entry.key;
305
- for (const entry of change.updated) yield entry.key;
306
- for (const entry of change.removed) yield entry.key;
307
- };
308
- const collectionChange = Object.freeze({ keys: changedKeys });
309
- const collectionHasStructuralChange = (change) => change.kind === "reset" || Boolean(change.added.length || change.removed.length || change.order);
310
- const collectionChangeTouchesKey = (change, key) => {
311
- for (const entry of change.added) if (entry.key === key) return true;
312
- for (const entry of change.updated) if (entry.key === key) return true;
313
- for (const entry of change.removed) if (entry.key === key) return true;
314
- return false;
315
- };
316
- const collectionChangeIntersects = (change, keys) => {
317
- for (const entry of change.added) if (keys.has(entry.key)) return true;
318
- for (const entry of change.updated) if (keys.has(entry.key)) return true;
319
- for (const entry of change.removed) if (keys.has(entry.key)) return true;
320
- return false;
228
+ return {
229
+ rebuild: false,
230
+ keys: all ? driver.read.ids() : dirty,
231
+ removed,
232
+ structural
233
+ };
321
234
  };
322
235
  //#endregion
323
236
  //#region core/src/projection/contract.ts
@@ -348,10 +261,11 @@ const createScheduler = (onError) => {
348
261
  const sources = /* @__PURE__ */ new Set();
349
262
  const processors = /* @__PURE__ */ new Set();
350
263
  const pendingSources = /* @__PURE__ */ new Set();
351
- const heap = [];
264
+ const dirty = /* @__PURE__ */ new Set();
352
265
  const queued = /* @__PURE__ */ new Set();
353
266
  const completed = /* @__PURE__ */ new Set();
354
267
  const emissions = /* @__PURE__ */ new Set();
268
+ const forced = /* @__PURE__ */ new Set();
355
269
  const errors = [];
356
270
  let reportingFailures = [];
357
271
  const guards = /* @__PURE__ */ new Set();
@@ -360,38 +274,22 @@ const createScheduler = (onError) => {
360
274
  };
361
275
  const assertIdle = () => {
362
276
  assertActive();
363
- if (phase !== "idle") throw new Error("Projection cannot be re-entered during processing or notification.");
277
+ if (phase !== "idle") throw new Error("Projection cannot be re-entered during input callbacks, processing or notification.");
364
278
  };
365
279
  const lock = (value) => guards.forEach((guard) => guard(value));
280
+ const markDirty = (producer) => {
281
+ const pending = [producer];
282
+ for (let index = 0; index < pending.length; index++) {
283
+ const current = pending[index];
284
+ if (current.disposed || dirty.has(current)) continue;
285
+ dirty.add(current);
286
+ for (const output of current.outputs) output.forEachConsumer((consumer) => pending.push(consumer));
287
+ }
288
+ };
366
289
  const enqueue = (processor) => {
367
290
  if (processor.disposed || queued.has(processor)) return;
368
291
  queued.add(processor);
369
292
  profile.projection("scheduledNodes");
370
- let index = heap.length;
371
- heap.push(processor);
372
- while (index > 0) {
373
- const parent = index - 1 >> 1;
374
- if (heap[parent].order < processor.order) break;
375
- heap[index] = heap[parent];
376
- index = parent;
377
- }
378
- heap[index] = processor;
379
- };
380
- const pop = () => {
381
- const first = heap[0];
382
- const last = heap.pop();
383
- if (heap.length) {
384
- let index = 0;
385
- while (index * 2 + 1 < heap.length) {
386
- let child = index * 2 + 1;
387
- if (child + 1 < heap.length && heap[child + 1].order < heap[child].order) child++;
388
- if (last.order < heap[child].order) break;
389
- heap[index] = heap[child];
390
- index = child;
391
- }
392
- heap[index] = last;
393
- }
394
- return first;
395
293
  };
396
294
  const errorFor = (producer, cause, kind = producer.kind === "source" ? "source" : "processor") => new ProjectionError(kind, cause);
397
295
  const capture = (source) => {
@@ -402,6 +300,7 @@ const createScheduler = (onError) => {
402
300
  throw source.fault;
403
301
  }
404
302
  pendingSources.add(source);
303
+ markDirty(source);
405
304
  profile.projection("sourceEvents");
406
305
  if (source.fault) errors.push(source.fault);
407
306
  };
@@ -412,88 +311,137 @@ const createScheduler = (onError) => {
412
311
  if (cause !== void 0) return cause;
413
312
  }
414
313
  };
415
- const settle = () => {
416
- if (disposed || depth || phase !== "idle") return;
417
- phase = "compute";
418
- lock(true);
419
- const cause = settleCause();
420
- try {
421
- for (const source of pendingSources) {
422
- if (source.disposed) continue;
423
- const wasFaulted = source.fault !== void 0;
424
- let changed = false;
425
- let force = false;
426
- if (!source.fault || source.recovering()) try {
427
- const prepared = source.prepare(cause);
428
- changed = prepared.changed;
429
- force = prepared.force;
430
- source.fault = void 0;
431
- } catch (failure) {
432
- source.fault = errorFor(source, failure, "source");
433
- errors.push(source.fault);
434
- }
435
- const recovered = wasFaulted && !source.fault;
436
- if (changed) source.publish();
437
- if (source.fault || changed || force || recovered) {
438
- const output = source.outputs[0];
439
- emissions.add(output);
440
- output.forEachConsumer(enqueue);
314
+ const propagate = (output, force = false) => {
315
+ emissions.add(output);
316
+ if (force) forced.add(output);
317
+ output.forEachConsumer(enqueue);
318
+ };
319
+ const advanceSource = (source, cause) => {
320
+ pendingSources.delete(source);
321
+ const wasFaulted = source.fault !== void 0;
322
+ let changed = false;
323
+ let force = false;
324
+ if (!source.fault || source.recovering()) try {
325
+ source.fault = void 0;
326
+ const prepared = source.prepare(cause);
327
+ changed = prepared.changed;
328
+ force = prepared.force;
329
+ source.fault = void 0;
330
+ } catch (failure) {
331
+ source.fault = errorFor(source, failure, "source");
332
+ errors.push(source.fault);
333
+ }
334
+ if (changed || force) source.publish();
335
+ if (source.fault || changed || force || wasFaulted) propagate(source.outputs[0], force);
336
+ source.clear();
337
+ };
338
+ const advanceProcessor = (processor, cause) => {
339
+ queued.delete(processor);
340
+ const wasFaulted = processor.fault !== void 0;
341
+ profile.projection("processedNodes");
342
+ let changedOutputs = [];
343
+ const blocked = processor.dependencies.find((output) => output.owner.fault || output.owner.disposed);
344
+ if (blocked) processor.fault = errorFor(processor, blocked.owner.fault ?? new ProjectionDisposedError(), "blocked");
345
+ else {
346
+ const reset = wasFaulted || processor.dependencies.some((output) => output.reset(processor));
347
+ try {
348
+ changedOutputs = processor.evaluate(reset, cause, wasFaulted);
349
+ const failure = processor.dependencies.find((output) => output.owner.fault)?.owner.fault;
350
+ if (failure) throw failure;
351
+ processor.fault = void 0;
352
+ } catch (failure) {
353
+ const error = errorFor(processor, failure);
354
+ processor.fault = error;
355
+ if (!reset) try {
356
+ changedOutputs = processor.evaluate(true, cause, true);
357
+ const dependencyFailure = processor.dependencies.find((output) => output.owner.fault)?.owner.fault;
358
+ if (dependencyFailure) throw dependencyFailure;
359
+ processor.fault = void 0;
360
+ errors.push(error);
361
+ } catch (rebuildFailure) {
362
+ processor.fault = errorFor(processor, rebuildFailure);
363
+ errors.push(processor.fault);
441
364
  }
365
+ else errors.push(error);
442
366
  }
443
- while (heap.length) {
444
- const processor = pop();
445
- if (processor.disposed) continue;
446
- const wasFaulted = processor.fault !== void 0;
447
- profile.projection("processedNodes");
448
- let changedOutputs = Object.freeze([]);
449
- const blocked = processor.dependencies.find((output) => output.owner.fault || output.owner.disposed);
450
- if (blocked) processor.fault = errorFor(processor, blocked.owner.fault ?? new ProjectionDisposedError(), "blocked");
451
- else {
452
- const reset = wasFaulted || processor.dependencies.some((output) => output.reset());
453
- try {
454
- changedOutputs = processor.evaluate(reset, cause, wasFaulted);
455
- const dependencyFailure = processor.dependencies.find((output) => output.owner.fault)?.owner.fault;
456
- if (dependencyFailure) throw dependencyFailure;
457
- processor.fault = void 0;
458
- } catch (failure) {
459
- const error = errorFor(processor, failure);
460
- processor.fault = error;
461
- if (!reset) try {
462
- changedOutputs = processor.evaluate(true, cause, true);
463
- const dependencyFailure = processor.dependencies.find((output) => output.owner.fault)?.owner.fault;
464
- if (dependencyFailure) throw dependencyFailure;
465
- processor.fault = void 0;
466
- errors.push(error);
467
- } catch (rebuildFailure) {
468
- processor.fault = errorFor(processor, rebuildFailure);
469
- errors.push(processor.fault);
470
- }
471
- else errors.push(error);
367
+ }
368
+ completed.add(processor);
369
+ if (processor.fault) processor.outputs.forEach((output) => propagate(output));
370
+ else {
371
+ if (changedOutputs.length) processor.publish();
372
+ (wasFaulted && !processor.fault ? processor.outputs : changedOutputs).forEach((output) => propagate(output));
373
+ }
374
+ if (processor.fault || changedOutputs.length) profile.projection("publishedNodes");
375
+ };
376
+ const advance = (target, cause) => {
377
+ const stack = [{
378
+ producer: target,
379
+ dependency: 0
380
+ }];
381
+ while (stack.length) {
382
+ const frame = stack[stack.length - 1];
383
+ const { producer } = frame;
384
+ if (producer.disposed || !dirty.has(producer)) {
385
+ dirty.delete(producer);
386
+ stack.pop();
387
+ continue;
388
+ }
389
+ if (producer.kind === "processor") {
390
+ let dependency;
391
+ while (frame.dependency < producer.dependencies.length) {
392
+ const candidate = producer.dependencies[frame.dependency++].owner;
393
+ if (dirty.has(candidate)) {
394
+ dependency = candidate;
395
+ break;
472
396
  }
473
397
  }
474
- completed.add(processor);
475
- const recovered = wasFaulted && !processor.fault;
476
- if (processor.fault) processor.outputs.forEach((output) => {
477
- emissions.add(output);
478
- output.forEachConsumer(enqueue);
479
- });
480
- else {
481
- if (changedOutputs.length) {
482
- processor.publish();
483
- changedOutputs.forEach((output) => emissions.add(output));
484
- }
485
- (recovered ? processor.outputs : changedOutputs).forEach((output) => output.forEachConsumer(enqueue));
398
+ if (dependency) {
399
+ stack.push({
400
+ producer: dependency,
401
+ dependency: 0
402
+ });
403
+ continue;
486
404
  }
487
- if (processor.fault || changedOutputs.length) profile.projection("publishedNodes");
488
405
  }
406
+ stack.pop();
407
+ dirty.delete(producer);
408
+ if (producer.kind === "source") advanceSource(producer, cause);
409
+ else if (queued.has(producer)) if (producer.fault || producer.dependencies.some((output) => output.owner.fault || output.pending(producer))) advanceProcessor(producer, cause);
410
+ else {
411
+ queued.delete(producer);
412
+ producer.dependencies.forEach((output) => output.acknowledge(producer));
413
+ }
414
+ }
415
+ };
416
+ const compute = (run) => {
417
+ phase = "compute";
418
+ lock(true);
419
+ try {
420
+ return run();
489
421
  } finally {
490
422
  lock(false);
491
423
  phase = "idle";
492
424
  }
493
425
  };
426
+ const settle = () => {
427
+ if (disposed || depth || phase !== "idle") return;
428
+ const cause = settleCause();
429
+ compute(() => {
430
+ for (const producer of dirty) advance(producer, cause);
431
+ });
432
+ };
433
+ const ensureCurrent = (output) => {
434
+ assertActive();
435
+ if (phase === "notify") return;
436
+ assertIdle();
437
+ if (dirty.has(output.owner)) {
438
+ const cause = settleCause();
439
+ compute(() => advance(output.owner, cause));
440
+ }
441
+ };
494
442
  const flush = () => {
495
443
  if (disposed || depth) return [];
496
- if (pendingSources.size || completed.size) profile.projection("flushes");
444
+ if (emissions.size || completed.size) profile.projection("flushes");
497
445
  phase = "notify";
498
446
  lock(true);
499
447
  const reportFailures = [];
@@ -504,7 +452,7 @@ const createScheduler = (onError) => {
504
452
  } catch (cause) {
505
453
  errors.push(errorFor(output.owner, cause, "listener"));
506
454
  }
507
- }));
455
+ }, forced.has(output) || Boolean(output.owner.fault)));
508
456
  for (const error of errors.slice()) try {
509
457
  onError(error);
510
458
  } catch (cause) {
@@ -518,12 +466,13 @@ const createScheduler = (onError) => {
518
466
  error
519
467
  }))]);
520
468
  } finally {
521
- pendingSources.forEach((source) => source.clear());
469
+ emissions.forEach((output) => output.finish());
522
470
  completed.forEach((processor) => processor.clear());
523
471
  pendingSources.clear();
524
472
  queued.clear();
525
473
  completed.clear();
526
474
  emissions.clear();
475
+ forced.clear();
527
476
  errors.length = 0;
528
477
  reportingFailures = reportFailures;
529
478
  lock(false);
@@ -569,6 +518,7 @@ const createScheduler = (onError) => {
569
518
  assertIdle();
570
519
  if (producer.outputs.some((output) => output.hasConsumers())) throw new Error("Projection producer is still used by another processor.");
571
520
  producer.disposed = true;
521
+ dirty.delete(producer);
572
522
  if (producer.kind === "processor") {
573
523
  producer.dependencies.forEach((output) => output.detachConsumer(producer));
574
524
  processors.delete(producer);
@@ -597,6 +547,7 @@ const createScheduler = (onError) => {
597
547
  assertIdle,
598
548
  capture,
599
549
  settle,
550
+ ensureCurrent,
600
551
  flush,
601
552
  run,
602
553
  registerGuard(guard) {
@@ -622,10 +573,10 @@ const createScheduler = (onError) => {
622
573
  processor.dependencies.forEach((output) => output.attachConsumer(processor));
623
574
  },
624
575
  releaseProducer,
625
- order: () => sequence++,
626
- initialize(callback) {
576
+ nextId: () => sequence++,
577
+ acceptInput(callback) {
627
578
  assertIdle();
628
- phase = "compute";
579
+ phase = "input";
629
580
  lock(true);
630
581
  try {
631
582
  return callback();
@@ -634,6 +585,10 @@ const createScheduler = (onError) => {
634
585
  phase = "idle";
635
586
  }
636
587
  },
588
+ initialize(callback) {
589
+ assertIdle();
590
+ return compute(callback);
591
+ },
637
592
  batch,
638
593
  batchContext: () => activeBatch,
639
594
  dispose() {
@@ -660,10 +615,11 @@ const createScheduler = (onError) => {
660
615
  processors.clear();
661
616
  sources.clear();
662
617
  pendingSources.clear();
663
- heap.length = 0;
618
+ dirty.clear();
664
619
  queued.clear();
665
620
  completed.clear();
666
621
  emissions.clear();
622
+ forced.clear();
667
623
  guards.clear();
668
624
  errors.length = 0;
669
625
  reportingFailures = [];
@@ -678,6 +634,26 @@ const assertScope = (active) => {
678
634
  if (!active()) throw new Error("Projection reader or writer is no longer active.");
679
635
  };
680
636
  //#endregion
637
+ //#region core/src/value/array.ts
638
+ /** Allocation-free strict equality for ordered readonly arrays. */
639
+ const sameArray = (left, right) => {
640
+ if (left === right) return true;
641
+ if (left.length !== right.length) return false;
642
+ for (let index = 0; index < left.length; index++) if (left[index] !== right[index]) return false;
643
+ return true;
644
+ };
645
+ /** Owns an immutable shallow snapshot while reusing arrays that are already frozen. */
646
+ const snapshotArray = (values) => Object.isFrozen(values) ? values : Object.freeze([...values]);
647
+ /** Compares an iterable with an ordered array without materializing the iterable. */
648
+ const iterableMatchesArray = (values, expected) => {
649
+ let index = 0;
650
+ for (const value of values) {
651
+ if (index >= expected.length || value !== expected[index]) return false;
652
+ index++;
653
+ }
654
+ return index === expected.length;
655
+ };
656
+ //#endregion
681
657
  //#region core/src/projection/keyed/relation.ts
682
658
  /** Runtime-local domain-neutral many-to-many keyed relation. */
683
659
  const createKeyRelation = () => {
@@ -906,7 +882,6 @@ const createKeyedDependencyRuntime = (compiled) => {
906
882
  for (const state of states) {
907
883
  const source = sources[state.sourceIndex];
908
884
  if (state.revision === source.revision) continue;
909
- state.entry;
910
885
  if (state.kind === "global") {
911
886
  all = true;
912
887
  continue;
@@ -948,6 +923,80 @@ const createKeyedDependencyRuntime = (compiled) => {
948
923
  };
949
924
  };
950
925
  //#endregion
926
+ //#region core/src/projection/collection/change.ts
927
+ const transitionContains = (entries, key) => {
928
+ for (const entry of entries) if (entry.key === key) return true;
929
+ return false;
930
+ };
931
+ const transitionSet = (entries) => {
932
+ if (entries.length <= 4) return void 0;
933
+ const keys = /* @__PURE__ */ new Set();
934
+ for (const entry of entries) keys.add(entry.key);
935
+ return keys;
936
+ };
937
+ const nextExcluding = (iterator, excluded, index) => {
938
+ while (true) {
939
+ const next = iterator.next();
940
+ if (next.done || !(index ? index.has(next.value) : transitionContains(excluded, next.value))) return next;
941
+ }
942
+ };
943
+ /** Compares common-member order from known transitions without materializing common arrays. */
944
+ const transitionOrderChanged = (before, after, removed, added) => {
945
+ const removedKeys = transitionSet(removed);
946
+ const addedKeys = transitionSet(added);
947
+ const beforeIterator = before[Symbol.iterator]();
948
+ const afterIterator = after[Symbol.iterator]();
949
+ while (true) {
950
+ const left = nextExcluding(beforeIterator, removed, removedKeys);
951
+ const right = nextExcluding(afterIterator, added, addedKeys);
952
+ if (left.done || right.done) return left.done !== right.done;
953
+ if (left.value !== right.value) return true;
954
+ }
955
+ };
956
+ const sealIncrementalChange = (added, updated, removed, order) => {
957
+ if (!added.length && !updated.length && !removed.length && !order) return void 0;
958
+ return Object.freeze({
959
+ kind: "incremental",
960
+ added: Object.freeze(added),
961
+ updated: Object.freeze(updated),
962
+ removed: Object.freeze(removed),
963
+ ...order ? { order: Object.freeze({
964
+ before: snapshotArray(order.before),
965
+ after: snapshotArray(order.after)
966
+ }) } : {}
967
+ });
968
+ };
969
+ /** Builds the canonical incremental CollectionChange from already-known net transitions. */
970
+ const createCollectionChange = (input) => {
971
+ let orderChanged = false;
972
+ if (input.beforeOrder !== input.afterOrder) if (!input.added.length && !input.removed.length) orderChanged = !sameArray(input.beforeOrder, input.afterOrder);
973
+ else orderChanged = transitionOrderChanged(input.beforeOrder, input.afterOrder, input.removed, input.added);
974
+ return sealIncrementalChange(input.added, input.updated, input.removed, orderChanged ? {
975
+ before: input.beforeOrder,
976
+ after: input.afterOrder
977
+ } : void 0);
978
+ };
979
+ const changedKeys = function* (change) {
980
+ if (change.kind !== "incremental") throw new TypeError("collectionChange.keys requires an incremental CollectionChange.");
981
+ for (const entry of change.added) yield entry.key;
982
+ for (const entry of change.updated) yield entry.key;
983
+ for (const entry of change.removed) yield entry.key;
984
+ };
985
+ const collectionChange = Object.freeze({ keys: changedKeys });
986
+ const collectionHasStructuralChange = (change) => change.kind === "reset" || Boolean(change.added.length || change.removed.length || change.order);
987
+ const collectionChangeTouchesKey = (change, key) => {
988
+ for (const entry of change.added) if (entry.key === key) return true;
989
+ for (const entry of change.updated) if (entry.key === key) return true;
990
+ for (const entry of change.removed) if (entry.key === key) return true;
991
+ return false;
992
+ };
993
+ const collectionChangeIntersects = (change, keys) => {
994
+ for (const entry of change.added) if (keys.has(entry.key)) return true;
995
+ for (const entry of change.updated) if (keys.has(entry.key)) return true;
996
+ for (const entry of change.removed) if (keys.has(entry.key)) return true;
997
+ return false;
998
+ };
999
+ //#endregion
951
1000
  //#region core/src/projection/keyed/transform.ts
952
1001
  const keyedAbsent = Symbol("keyed-transform-absent");
953
1002
  /** Shared driver-keyed lifecycle for pure and retained keyed transforms. */
@@ -972,7 +1021,8 @@ const createKeyedTransform = (definition) => {
972
1021
  const driver = evaluation.sources[0];
973
1022
  const output = evaluation.outputs[0];
974
1023
  if (driver.kind !== "collection" || output.kind !== "collection") throw new Error(`${definition.name} requires collection input and output.`);
975
- dependencies.prepare(evaluation.sources, evaluation.reset);
1024
+ const invalidation = collectKeyedInvalidation(driver, evaluation.sources, dependencies, evaluation.reset);
1025
+ dependencies.prepare(evaluation.sources, invalidation.rebuild);
976
1026
  const stateFor = (key, value) => {
977
1027
  if (states.has(key)) return states.get(key);
978
1028
  const state = definition.state ? definition.state(value, key) : void 0;
@@ -997,49 +1047,29 @@ const createKeyedTransform = (definition) => {
997
1047
  else output.output.set(key, next);
998
1048
  return beforePresent !== output.next.has(key);
999
1049
  };
1000
- if (evaluation.reset) {
1050
+ if (invalidation.rebuild) {
1001
1051
  dependencies.clearBindings();
1002
1052
  const ids = driver.read.ids();
1003
1053
  for (const key of states.keys()) if (!driver.read.has(key)) states.delete(key);
1054
+ if (!evaluation.reset) {
1055
+ for (const key of output.previous.ids()) if (!driver.read.has(key)) output.output.remove(key);
1056
+ }
1004
1057
  for (const key of ids) project(key);
1005
1058
  output.output.order(ids.filter((key) => output.next.has(key)));
1006
1059
  dependencies.remember(evaluation.sources);
1007
1060
  return;
1008
1061
  }
1009
- const dirty = /* @__PURE__ */ new Set();
1010
- let all = false;
1011
- let updateOrder = false;
1012
1062
  let membershipChanged = false;
1013
- if (dependencies.driverChanged(driver)) {
1014
- const change = driver.change;
1015
- if (!change || change.kind === "reset") {
1016
- all = true;
1017
- updateOrder = true;
1018
- for (const key of output.previous.ids()) {
1019
- if (driver.read.has(key)) continue;
1020
- output.output.remove(key);
1021
- dropState(key);
1022
- membershipChanged = true;
1023
- }
1024
- } else {
1025
- for (const entry of change.removed) {
1026
- membershipChanged ||= output.previous.has(entry.key);
1027
- output.output.remove(entry.key);
1028
- dropState(entry.key);
1029
- dirty.delete(entry.key);
1030
- }
1031
- for (const entry of change.added) dirty.add(entry.key);
1032
- for (const entry of change.updated) dirty.add(entry.key);
1033
- updateOrder = Boolean(change.order);
1034
- }
1063
+ for (const entry of invalidation.removed) {
1064
+ membershipChanged ||= output.previous.has(entry.key);
1065
+ output.output.remove(entry.key);
1066
+ dropState(entry.key);
1035
1067
  }
1036
- all ||= dependencies.collectInvalidated(evaluation.sources, dirty);
1037
- if (all) for (const key of driver.read.ids()) dirty.add(key);
1038
- for (const key of dirty) {
1068
+ for (const key of invalidation.keys) {
1039
1069
  const changedMembership = project(key);
1040
1070
  membershipChanged ||= changedMembership;
1041
1071
  }
1042
- if (updateOrder || membershipChanged) output.output.order(driver.read.ids().filter((key) => output.next.has(key)));
1072
+ if (invalidation.structural || membershipChanged) output.output.order(driver.read.ids().filter((key) => output.next.has(key)));
1043
1073
  dependencies.remember(evaluation.sources);
1044
1074
  },
1045
1075
  release: () => {
@@ -1083,6 +1113,12 @@ Object.defineProperty(exports, "assertSynchronous", {
1083
1113
  return assertSynchronous;
1084
1114
  }
1085
1115
  });
1116
+ Object.defineProperty(exports, "collectKeyedInvalidation", {
1117
+ enumerable: true,
1118
+ get: function() {
1119
+ return collectKeyedInvalidation;
1120
+ }
1121
+ });
1086
1122
  Object.defineProperty(exports, "collectionChange", {
1087
1123
  enumerable: true,
1088
1124
  get: function() {
@@ -1167,12 +1203,6 @@ Object.defineProperty(exports, "defineSource", {
1167
1203
  return defineSource;
1168
1204
  }
1169
1205
  });
1170
- Object.defineProperty(exports, "diffCollection", {
1171
- enumerable: true,
1172
- get: function() {
1173
- return diffCollection;
1174
- }
1175
- });
1176
1206
  Object.defineProperty(exports, "isProjection", {
1177
1207
  enumerable: true,
1178
1208
  get: function() {
@@ -1252,4 +1282,4 @@ Object.defineProperty(exports, "snapshotDependencyValue", {
1252
1282
  }
1253
1283
  });
1254
1284
 
1255
- //# sourceMappingURL=transform-CENhTaVN.cjs.map
1285
+ //# sourceMappingURL=transform-Bt1Mwgrj.cjs.map