@oimdb/core 3.10.0 → 3.11.0

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
@@ -225,6 +225,25 @@ var OIMEventQueueScheduler = class {
225
225
  flush() {
226
226
  this.emitter.emit(0 /* FLUSH */, void 0);
227
227
  }
228
+ /**
229
+ * Detach a Node timer handle from the event loop, returning it unchanged.
230
+ *
231
+ * A pending flush must never be a reason for a process to stay alive: if
232
+ * nothing else is running there is no UI left to update, and the flush is
233
+ * moot. Without this a scheduled-but-not-yet-run flush keeps Node awake —
234
+ * which is why a test suite that leaves a queue scheduled reports
235
+ * "a worker process has failed to exit gracefully".
236
+ *
237
+ * In the browser `setTimeout` returns a number, which has no `unref`, so the
238
+ * call is guarded and the handle passes straight through.
239
+ */
240
+ unref(handle) {
241
+ const maybeUnrefable = handle;
242
+ if (typeof maybeUnrefable?.unref === "function") {
243
+ maybeUnrefable.unref();
244
+ }
245
+ return handle;
246
+ }
228
247
  };
229
248
 
230
249
  // src/core/OIMKeyedBucketSetBased.ts
@@ -559,19 +578,28 @@ var OIMCarrierKeyedEmitter = class {
559
578
  // doubles as membership: a carrier is in this array iff `carrier.dirty`.
560
579
  this.dirtyCarriers = [];
561
580
  this.isFlushEnqueued = false;
581
+ // Number of live subscriptions whose single handler spans 2+ distinct
582
+ // carriers (a `subscribeOnKeys` over distinct keys). While this is > 0 a
583
+ // flush coalesces delivery so such a handler fires at most ONCE per flush,
584
+ // not once per changed carrier — the callback carries no key, so extra
585
+ // fires are pure waste. At 0 the hot single-subscriber path pays nothing:
586
+ // no per-flush Set, no membership checks.
587
+ this.multiCarrierSubscriptions = 0;
562
588
  this.onFlush = () => {
563
589
  this.isFlushEnqueued = false;
564
590
  if (this.dirtyCarriers.length === 0) return;
565
591
  const flushing = this.dirtyCarriers;
566
592
  this.dirtyCarriers = [];
593
+ const delivered = this.multiCarrierSubscriptions > 0 ? this.deliveredPool ?? (this.deliveredPool = /* @__PURE__ */ new Set()) : void 0;
567
594
  let i = 0;
568
595
  try {
569
596
  for (; i < flushing.length; i++) {
570
597
  flushing[i].dirty = false;
571
- this.notify(flushing[i]);
598
+ this.notify(flushing[i], delivered);
572
599
  }
573
600
  } finally {
574
601
  for (; i < flushing.length; i++) flushing[i].dirty = false;
602
+ delivered?.clear();
575
603
  }
576
604
  };
577
605
  this.queue = queue;
@@ -589,7 +617,13 @@ var OIMCarrierKeyedEmitter = class {
589
617
  this.addHandler(carrier, handler);
590
618
  carriers.push(carrier);
591
619
  }
620
+ const spansMultipleCarriers = carriers.length > 1;
621
+ if (spansMultipleCarriers) this.multiCarrierSubscriptions++;
622
+ let unsubscribed = false;
592
623
  return () => {
624
+ if (unsubscribed) return;
625
+ unsubscribed = true;
626
+ if (spansMultipleCarriers) this.multiCarrierSubscriptions--;
593
627
  for (let i = 0; i < carriers.length; i++) {
594
628
  this.removeHandler(carriers[i], handler);
595
629
  }
@@ -705,19 +739,31 @@ var OIMCarrierKeyedEmitter = class {
705
739
  this.isFlushEnqueued = true;
706
740
  this.queue.enqueue(this.onFlush);
707
741
  }
708
- notify(carrier) {
742
+ notify(carrier, delivered) {
709
743
  const subscribers = carrier.subscribers;
710
744
  if (!subscribers || subscribers.size === 0) return;
711
745
  if (subscribers.size === 1) {
712
746
  const only = subscribers.values().next().value;
713
- if (only) only();
747
+ if (!only) return;
748
+ if (delivered) {
749
+ const size = delivered.size;
750
+ delivered.add(only);
751
+ if (delivered.size === size) return;
752
+ }
753
+ only();
714
754
  return;
715
755
  }
716
756
  const snapshot = [];
717
757
  subscribers.forEach((h) => snapshot.push(h));
718
758
  for (let i = 0; i < snapshot.length; i++) {
719
759
  const handler = snapshot[i];
720
- if (subscribers.has(handler)) handler();
760
+ if (!subscribers.has(handler)) continue;
761
+ if (delivered) {
762
+ const size = delivered.size;
763
+ delivered.add(handler);
764
+ if (delivered.size === size) continue;
765
+ }
766
+ handler();
721
767
  }
722
768
  }
723
769
  };
@@ -1339,8 +1385,11 @@ var OIMCollection = class {
1339
1385
  this.emitter.emit(0 /* UPDATE */, { pks });
1340
1386
  }
1341
1387
  clear() {
1388
+ const pks = this.store.getAllPks();
1342
1389
  this.store.clear();
1343
- this.emitter.emit(0 /* UPDATE */, { pks: [] });
1390
+ if (pks.length > 0) {
1391
+ this.emitter.emit(0 /* UPDATE */, { pks });
1392
+ }
1344
1393
  }
1345
1394
  countAll() {
1346
1395
  return this.store.countAll();
@@ -1382,14 +1431,12 @@ var OIMReactiveCollection = class extends OIMCollection {
1382
1431
  // Only ever holds `slot.pk` (one canonical reference per logical key), so a
1383
1432
  // native `Set` dedups correctly by reference even for composite pks.
1384
1433
  this.pendingAnyUpdatePks = /* @__PURE__ */ new Set();
1385
- this.isAnyUpdateClearPending = false;
1386
1434
  this.isAnyUpdateScheduled = false;
1387
1435
  this.runAnyUpdate = () => {
1388
1436
  if (!this.isAnyUpdateScheduled) return;
1389
1437
  this.isAnyUpdateScheduled = false;
1390
- const pks = this.isAnyUpdateClearPending ? [] : Array.from(this.pendingAnyUpdatePks.values());
1438
+ const pks = Array.from(this.pendingAnyUpdatePks.values());
1391
1439
  this.pendingAnyUpdatePks.clear();
1392
- this.isAnyUpdateClearPending = false;
1393
1440
  const snapshot = Array.from(this.anyUpdateHandlers);
1394
1441
  for (let i = 0; i < snapshot.length; i++) snapshot[i](pks);
1395
1442
  };
@@ -1429,30 +1476,43 @@ var OIMReactiveCollection = class extends OIMCollection {
1429
1476
  this.anyUpdateHandlers.delete(handler);
1430
1477
  };
1431
1478
  }
1432
- // The legacy `emitter` (EOIMCollectionEventType.UPDATE) coexists with the
1433
- // keyed `updateEmitter`. Build its payload only when something is actually
1434
- // subscribed — otherwise every upsert/remove allocates a throwaway
1435
- // `{ pks }` object (and array) that nobody reads.
1436
- notifyLegacy(pk) {
1479
+ // Two notification channels coexist on a reactive collection; they serve
1480
+ // different audiences and NEITHER is obsolete:
1481
+ // • the keyed `updateEmitter` — per-key/per-carrier pings ("did MY key
1482
+ // change?", payload-less `void`), for leaf subscribers like React hooks
1483
+ // that declare the keys they care about up front;
1484
+ // • this `emitter` UPDATE event — the aggregate/batch channel that carries
1485
+ // the full list of changed pks in one SYNCHRONOUS callback, for
1486
+ // consumers that do NOT know their keys ahead of time and must re-derive
1487
+ // over the batch: derived indexes/collections, changed-fields, persist,
1488
+ // snapshot-manager. `pks` always enumerates exactly what changed — even
1489
+ // clear() lists every removed key, so an empty array never occurs.
1490
+ // It is not a per-key emitter with fewer features — it is the opposite
1491
+ // shape (enumerated batch, not "my key"), which is why it stays.
1492
+ //
1493
+ // Emit on the batch channel only when something is actually subscribed —
1494
+ // otherwise every upsert/remove allocates a throwaway `{ pks }` object (and
1495
+ // array) that nobody reads.
1496
+ notifyBatch(pk) {
1437
1497
  if (this.emitter.hasHandlers(0 /* UPDATE */)) {
1438
1498
  this.emitter.emit(0 /* UPDATE */, { pks: [pk] });
1439
1499
  }
1440
1500
  }
1441
- notifyLegacyMany(pks) {
1501
+ notifyBatchMany(pks) {
1442
1502
  if (this.emitter.hasHandlers(0 /* UPDATE */)) {
1443
1503
  this.emitter.emit(0 /* UPDATE */, { pks });
1444
1504
  }
1445
1505
  }
1446
1506
  upsertOneByPk(pk, entity) {
1447
1507
  const slot = this.upsertOneWithoutNotificationsByPk(pk, entity);
1448
- this.notifyLegacy(pk);
1508
+ this.notifyBatch(pk);
1449
1509
  this.updateEmitter.markUpdatedCarrier(slot);
1450
1510
  this.trackAnyUpdatePk(pk);
1451
1511
  return slot;
1452
1512
  }
1453
1513
  upsertOne(entity) {
1454
1514
  const slot = this.upsertOneWithoutNotifications(entity);
1455
- this.notifyLegacy(slot.pk);
1515
+ this.notifyBatch(slot.pk);
1456
1516
  this.updateEmitter.markUpdatedCarrier(slot);
1457
1517
  this.trackAnyUpdatePk(slot.pk);
1458
1518
  return slot;
@@ -1463,7 +1523,7 @@ var OIMReactiveCollection = class extends OIMCollection {
1463
1523
  (entity) => this.upsertOneWithoutNotifications(entity)
1464
1524
  );
1465
1525
  const pks = slots.map((slot) => slot.pk);
1466
- this.notifyLegacyMany(pks);
1526
+ this.notifyBatchMany(pks);
1467
1527
  for (let i = 0; i < slots.length; i++) {
1468
1528
  this.updateEmitter.markUpdatedCarrier(slots[i]);
1469
1529
  }
@@ -1473,7 +1533,7 @@ var OIMReactiveCollection = class extends OIMCollection {
1473
1533
  removeOne(entity) {
1474
1534
  const pk = this.selectPk(entity);
1475
1535
  this.store.removeOneByPk(pk);
1476
- this.notifyLegacy(pk);
1536
+ this.notifyBatch(pk);
1477
1537
  this.updateEmitter.markUpdatedKey(pk);
1478
1538
  this.trackAnyUpdatePk(pk);
1479
1539
  }
@@ -1481,30 +1541,32 @@ var OIMReactiveCollection = class extends OIMCollection {
1481
1541
  if (entities.length === 0) return;
1482
1542
  const pks = entities.map(this.selectPk);
1483
1543
  this.store.removeManyByPks(pks);
1484
- this.notifyLegacyMany(pks);
1544
+ this.notifyBatchMany(pks);
1485
1545
  this.updateEmitter.markUpdatedKeys(pks);
1486
1546
  this.trackAnyUpdatePks(pks);
1487
1547
  }
1488
1548
  removeOneByPk(pk) {
1489
1549
  this.store.removeOneByPk(pk);
1490
- this.notifyLegacy(pk);
1550
+ this.notifyBatch(pk);
1491
1551
  this.updateEmitter.markUpdatedKey(pk);
1492
1552
  this.trackAnyUpdatePk(pk);
1493
1553
  }
1494
1554
  removeManyByPks(pks) {
1495
1555
  if (pks.length === 0) return;
1496
1556
  this.store.removeManyByPks(pks);
1497
- this.notifyLegacyMany(pks);
1557
+ this.notifyBatchMany(pks);
1498
1558
  this.updateEmitter.markUpdatedKeys(pks);
1499
1559
  this.trackAnyUpdatePks(pks);
1500
1560
  }
1501
1561
  clear() {
1562
+ const needsPks = this.emitter.hasHandlers(0 /* UPDATE */) || this.anyUpdateHandlers.size > 0;
1563
+ const pks = needsPks ? this.store.getAllPks() : void 0;
1502
1564
  this.store.clear();
1503
- if (this.emitter.hasHandlers(0 /* UPDATE */)) {
1504
- this.emitter.emit(0 /* UPDATE */, { pks: [] });
1565
+ if (pks && pks.length > 0) {
1566
+ this.notifyBatchMany(pks);
1567
+ this.trackAnyUpdatePks(pks);
1505
1568
  }
1506
1569
  this.updateEmitter.markAllUpdated();
1507
- this.trackAnyUpdateClear();
1508
1570
  }
1509
1571
  trackAnyUpdatePk(pk) {
1510
1572
  if (this.anyUpdateHandlers.size === 0) return;
@@ -1513,14 +1575,9 @@ var OIMReactiveCollection = class extends OIMCollection {
1513
1575
  }
1514
1576
  trackAnyUpdatePks(pks) {
1515
1577
  if (this.anyUpdateHandlers.size === 0) return;
1516
- for (let i = 0; i < pks.length; i++)
1578
+ for (let i = 0; i < pks.length; i++) {
1517
1579
  this.pendingAnyUpdatePks.add(pks[i]);
1518
- this.ensureAnyUpdateEnqueued();
1519
- }
1520
- trackAnyUpdateClear() {
1521
- if (this.anyUpdateHandlers.size === 0) return;
1522
- this.isAnyUpdateClearPending = true;
1523
- this.pendingAnyUpdatePks.clear();
1580
+ }
1524
1581
  this.ensureAnyUpdateEnqueued();
1525
1582
  }
1526
1583
  ensureAnyUpdateEnqueued() {
@@ -1953,8 +2010,13 @@ var OIMUpdateEventEmitter = class {
1953
2010
  var OIMOrderedListCommandBuffer = class {
1954
2011
  constructor(queue) {
1955
2012
  this.commandsByKey = /* @__PURE__ */ new Map();
2013
+ // Keys whose buffered leading `reset` is stale: an edit after the reset
2014
+ // should be folded into it, but we defer the (O(list-size)) rebuild to the
2015
+ // next read instead of redoing it on every subsequent edit in the batch.
2016
+ this.staleResetKeys = /* @__PURE__ */ new Set();
1956
2017
  this.onAfterFlush = () => {
1957
2018
  this.commandsByKey.clear();
2019
+ this.staleResetKeys.clear();
1958
2020
  };
1959
2021
  this.commandsEventEmitter = new OIMUpdateEventEmitter(
1960
2022
  queue,
@@ -1969,9 +2031,11 @@ var OIMOrderedListCommandBuffer = class {
1969
2031
  return this.commandsEventEmitter.subscribeOnKey(key, handler);
1970
2032
  }
1971
2033
  getBufferedCommands(key) {
2034
+ this.materializeStaleReset(key);
1972
2035
  return this.commandsByKey.get(key) ?? [];
1973
2036
  }
1974
2037
  consumeCommands(key) {
2038
+ this.materializeStaleReset(key);
1975
2039
  const cmds = this.commandsByKey.get(key);
1976
2040
  if (!cmds || cmds.length === 0) return [];
1977
2041
  return cmds.slice();
@@ -1980,6 +2044,20 @@ var OIMOrderedListCommandBuffer = class {
1980
2044
  this.commandsEventEmitter.destroy();
1981
2045
  this.unsubscribeAfterFlush();
1982
2046
  this.commandsByKey.clear();
2047
+ this.staleResetKeys.clear();
2048
+ }
2049
+ /**
2050
+ * Rebuild a deferred leading `reset` from the current order, once, at read
2051
+ * time. All the post-reset edits that were folded in are already reflected
2052
+ * in the driver's backing order, so a single rebuild here is equivalent to
2053
+ * having recomputed the reset on each of those edits.
2054
+ */
2055
+ materializeStaleReset(key) {
2056
+ if (!this.staleResetKeys.delete(key)) return;
2057
+ const cmds = this.commandsByKey.get(key);
2058
+ if (cmds && cmds.length > 0 && cmds[0].type === "reset") {
2059
+ cmds[0] = this.createResetCommand(this.getItemsByKey(key));
2060
+ }
1983
2061
  }
1984
2062
  appendCommand(key, cmd) {
1985
2063
  let cmds = this.commandsByKey.get(key);
@@ -1990,8 +2068,9 @@ var OIMOrderedListCommandBuffer = class {
1990
2068
  if (cmd.type === "reset") {
1991
2069
  cmds.length = 0;
1992
2070
  cmds.push(cmd);
2071
+ this.staleResetKeys.delete(key);
1993
2072
  } else if (cmds.length > 0 && cmds[0].type === "reset") {
1994
- cmds[0] = this.createResetCommand(this.getItemsByKey(key));
2073
+ this.staleResetKeys.add(key);
1995
2074
  } else {
1996
2075
  cmds.push(cmd);
1997
2076
  }
@@ -3033,10 +3112,6 @@ var OIMDerivedCollectionIndexSetBased = class extends OIMReactiveCollectionIndex
3033
3112
  });
3034
3113
  this.keysByPk = /* @__PURE__ */ new Map();
3035
3114
  this.onCollectionUpdate = (pks) => {
3036
- if (pks.length === 0) {
3037
- this.rebuildFromCollection();
3038
- return;
3039
- }
3040
3115
  for (const pk of pks) {
3041
3116
  this.syncPk(pk);
3042
3117
  }
@@ -3138,10 +3213,6 @@ var OIMDerivedCollectionIndexArrayBased = class extends OIMReactiveCollectionInd
3138
3213
  // touching an unrelated field can skip re-sorting when the value is unchanged.
3139
3214
  this.orderValueByPk = /* @__PURE__ */ new Map();
3140
3215
  this.onCollectionUpdate = (pks) => {
3141
- if (pks.length === 0) {
3142
- this.rebuildFromCollection();
3143
- return;
3144
- }
3145
3216
  const ordered = !!(this.compareEntities || this.orderBy);
3146
3217
  const keysToSort = ordered ? /* @__PURE__ */ new Set() : null;
3147
3218
  for (const pk of pks) {
@@ -3558,10 +3629,6 @@ var OIMDerivedCollectionGlobalIndexArrayBased = class extends OIMReactiveCollect
3558
3629
  // an unrelated field can skip re-sorting when the value is unchanged.
3559
3630
  this.orderValueByPk = /* @__PURE__ */ new Map();
3560
3631
  this.onCollectionUpdate = (pks) => {
3561
- if (pks.length === 0) {
3562
- this.rebuildFromCollection();
3563
- return;
3564
- }
3565
3632
  const ordered = !!(this.compareEntities || this.orderBy);
3566
3633
  let needsSort = false;
3567
3634
  for (const pk of pks) {
@@ -3659,10 +3726,6 @@ var OIMDerivedCollectionGlobalIndexSetBased = class extends OIMReactiveCollectio
3659
3726
  super(queue, { collection, indexOptions: opts.indexOptions });
3660
3727
  this.presentPks = /* @__PURE__ */ new Set();
3661
3728
  this.onCollectionUpdate = (pks) => {
3662
- if (pks.length === 0) {
3663
- this.rebuildFromCollection();
3664
- return;
3665
- }
3666
3729
  for (const pk of pks) {
3667
3730
  const prev = this.presentPks.has(pk);
3668
3731
  const slot = this.collection.getSlotByPk(pk);
@@ -3838,6 +3901,55 @@ function createOIMCollectionIndexFactory(queue, collection) {
3838
3901
  return new OIMCollectionIndexFactory(queue, collection);
3839
3902
  }
3840
3903
 
3904
+ // src/core/OIMDerivedCollection.ts
3905
+ var OIMDerivedCollection = class extends OIMReactiveCollection {
3906
+ constructor(queue, source, derive, opts) {
3907
+ super(queue, {
3908
+ store: opts?.store,
3909
+ // Each sync recomputes the WHOLE derived entity → replace, not merge.
3910
+ updateEntity: (draft) => draft
3911
+ });
3912
+ this.onSourceUpdate = (pks) => {
3913
+ for (let i = 0; i < pks.length; i++) this.syncPk(pks[i]);
3914
+ };
3915
+ this.source = source;
3916
+ this.derive = derive;
3917
+ this.compareDerived = opts?.compare ?? Object.is;
3918
+ this.unsubscribeFromSource = source.emitter.on(
3919
+ 0 /* UPDATE */,
3920
+ (payload) => this.onSourceUpdate(payload.pks)
3921
+ );
3922
+ if (opts?.buildInitial !== false) {
3923
+ this.rebuildFromSource();
3924
+ }
3925
+ }
3926
+ rebuildFromSource() {
3927
+ for (const pk of this.getAllPks()) {
3928
+ if (this.source.getOneByPk(pk) === void 0) {
3929
+ this.removeOneByPk(pk);
3930
+ }
3931
+ }
3932
+ for (const slot of this.source.getAllSlots()) {
3933
+ if (slot.item !== void 0) this.syncPk(slot.pk);
3934
+ }
3935
+ }
3936
+ destroy() {
3937
+ this.unsubscribeFromSource();
3938
+ super.destroy();
3939
+ }
3940
+ syncPk(pk) {
3941
+ const sourceEntity = this.source.getOneByPk(pk);
3942
+ if (sourceEntity === void 0) {
3943
+ if (this.getOneByPk(pk) !== void 0) this.removeOneByPk(pk);
3944
+ return;
3945
+ }
3946
+ const next = this.derive(sourceEntity, pk);
3947
+ const prev = this.getOneByPk(pk);
3948
+ if (prev !== void 0 && this.compareDerived(prev, next)) return;
3949
+ this.upsertOneByPk(pk, next);
3950
+ }
3951
+ };
3952
+
3841
3953
  // src/core/OIMObjectStoreRecordDriven.ts
3842
3954
  var OIMObjectStoreRecordDriven = class extends OIMObjectStore {
3843
3955
  constructor(initial) {
@@ -3910,8 +4022,11 @@ var OIMObject = class {
3910
4022
  return this.store.getAll();
3911
4023
  }
3912
4024
  clear() {
4025
+ const keys = this.store.keys();
3913
4026
  this.store.clear();
3914
- this.onUpdatedKeys([]);
4027
+ if (keys.length > 0) {
4028
+ this.onUpdatedKeys(keys);
4029
+ }
3915
4030
  }
3916
4031
  /**
3917
4032
  * Write fan-out hooks. Split single-key vs multi-key so a reactive subclass
@@ -4214,8 +4329,9 @@ var OIMComparatorFactory = class {
4214
4329
  if (ka.length !== kb.length) return true;
4215
4330
  for (let i = 0; i < ka.length; i++) {
4216
4331
  const k = ka[i];
4217
- if (b[k] !== a[k])
4332
+ if (b[k] !== a[k]) {
4218
4333
  return true;
4334
+ }
4219
4335
  }
4220
4336
  return false;
4221
4337
  };
@@ -4265,10 +4381,14 @@ var OIMEventQueue = class {
4265
4381
  return this.isFlushing;
4266
4382
  }
4267
4383
  /**
4268
- * Execute all pending tasks.
4384
+ * Execute the tasks pending at the moment the flush starts — a single pass,
4385
+ * not a drain-to-empty.
4269
4386
  *
4270
- * This flush drains until the queue is empty. If tasks enqueue more tasks, they will be
4271
- * executed within the same flush.
4387
+ * The pending set is swapped out for an empty spare up front and the flush
4388
+ * iterates that captured set. Tasks enqueued during the flush land in the
4389
+ * fresh set and run on the NEXT flush, not this one. (Contrast
4390
+ * `OIMComputeRuntime.flush()`, which re-reads its bucket per level and so
4391
+ * does drain same-level tasks scheduled mid-flush.)
4272
4392
  */
4273
4393
  flush() {
4274
4394
  this.scheduler?.cancel();
@@ -4542,20 +4662,92 @@ var OIMPkCodecKeyPath = class {
4542
4662
  }
4543
4663
  };
4544
4664
 
4665
+ // src/modules/effect/core/OIMEffectDependencyComputed.ts
4666
+ var OIMEffectDependencyComputed = class {
4667
+ constructor(source) {
4668
+ this.source = source;
4669
+ }
4670
+ subscribe(onUpdate) {
4671
+ return this.source.emitter.on("update", onUpdate);
4672
+ }
4673
+ };
4674
+
4545
4675
  // src/modules/compute/core/OIMComputeRuntime.ts
4546
4676
  var OIMComputeRuntime = class {
4547
4677
  constructor(queue) {
4548
4678
  this.queue = queue;
4549
4679
  this.isFlushScheduled = false;
4550
4680
  this.isFlushing = false;
4681
+ // Active level range (min > max means nothing pending).
4551
4682
  this.minLevel = Infinity;
4552
4683
  this.maxLevel = -Infinity;
4553
- this.dirtyByLevel = /* @__PURE__ */ new Map();
4684
+ this.dirtyByLevel = [];
4554
4685
  this.afterFlushTasks = /* @__PURE__ */ new Set();
4555
- this.queue.emitter.on(0 /* AFTER_FLUSH */, () => {
4556
- if (!this.isFlushScheduled) return;
4557
- this.flush();
4558
- });
4686
+ // Topological depth per computed node — the engine is the ONLY owner of
4687
+ // levels; nodes never expose their own. A node's dependents look up its depth
4688
+ // here (by the computed reference their dependency wraps). Written once at
4689
+ // construction, never on the hot flush path.
4690
+ this.levelByComputed = /* @__PURE__ */ new WeakMap();
4691
+ this._isDestroyed = false;
4692
+ this.unsubscribeFromQueue = this.queue.emitter.on(
4693
+ 0 /* AFTER_FLUSH */,
4694
+ () => {
4695
+ if (!this.isFlushScheduled) return;
4696
+ this.flush();
4697
+ }
4698
+ );
4699
+ }
4700
+ /**
4701
+ * Whether {@link destroy} has run. A destroyed runtime is detached from its
4702
+ * queue and must not be reused — `getOIMComputeRuntime` reads this to hand
4703
+ * out a fresh runtime instead of a dead one.
4704
+ */
4705
+ get isDestroyed() {
4706
+ return this._isDestroyed;
4707
+ }
4708
+ /**
4709
+ * Detach from the queue and drop everything pending.
4710
+ *
4711
+ * Without this the AFTER_FLUSH subscription lives as long as the queue does,
4712
+ * even once every computed / effect on this runtime is gone — harmless (the
4713
+ * handler early-returns) but impossible to unwind. Destroying does NOT
4714
+ * destroy the nodes scheduled on it; destroy them first, then the runtime.
4715
+ * Idempotent.
4716
+ */
4717
+ destroy() {
4718
+ if (this._isDestroyed) return;
4719
+ this._isDestroyed = true;
4720
+ this.unsubscribeFromQueue();
4721
+ for (let i = 0; i < this.dirtyByLevel.length; i++) {
4722
+ this.dirtyByLevel[i]?.clear();
4723
+ }
4724
+ this.afterFlushTasks.clear();
4725
+ this.minLevel = Infinity;
4726
+ this.maxLevel = -Infinity;
4727
+ this.isFlushScheduled = false;
4728
+ }
4729
+ /**
4730
+ * Topological depth for a node with these dependencies: one above the deepest
4731
+ * computed dependency (source-only → 0). The engine owns this — callers pass
4732
+ * their dependencies and get back a level, they never compute it themselves.
4733
+ */
4734
+ computeLevel(deps) {
4735
+ let max = -1;
4736
+ for (const dep of deps) {
4737
+ if (dep instanceof OIMEffectDependencyComputed) {
4738
+ const depLevel = this.levelByComputed.get(dep.source) ?? 0;
4739
+ if (depLevel > max) max = depLevel;
4740
+ }
4741
+ }
4742
+ return max + 1;
4743
+ }
4744
+ /** Record a computed's depth so its dependents can look it up. */
4745
+ registerLevel(computed, level) {
4746
+ this.levelByComputed.set(computed, level);
4747
+ }
4748
+ /** The recorded topological depth of a computed (0 if unknown). Introspection. */
4749
+ getLevel(computed) {
4750
+ return this.levelByComputed.get(computed) ?? 0;
4559
4751
  }
4560
4752
  /**
4561
4753
  * Schedule a one-shot task to run on the next `queue.flush()` (AFTER_FLUSH),
@@ -4567,10 +4759,10 @@ var OIMComputeRuntime = class {
4567
4759
  if (!isActive) return;
4568
4760
  task();
4569
4761
  };
4570
- let bucket = this.dirtyByLevel.get(level);
4571
- if (!bucket) {
4762
+ let bucket = this.dirtyByLevel[level];
4763
+ if (bucket === void 0) {
4572
4764
  bucket = /* @__PURE__ */ new Set();
4573
- this.dirtyByLevel.set(level, bucket);
4765
+ this.dirtyByLevel[level] = bucket;
4574
4766
  }
4575
4767
  bucket.add(wrapped);
4576
4768
  if (level < this.minLevel) this.minLevel = level;
@@ -4602,7 +4794,7 @@ var OIMComputeRuntime = class {
4602
4794
  }
4603
4795
  flush() {
4604
4796
  this.isFlushScheduled = false;
4605
- if (this.dirtyByLevel.size === 0 && this.afterFlushTasks.size === 0)
4797
+ if (this.minLevel > this.maxLevel && this.afterFlushTasks.size === 0)
4606
4798
  return;
4607
4799
  this.isFlushing = true;
4608
4800
  try {
@@ -4611,15 +4803,15 @@ var OIMComputeRuntime = class {
4611
4803
  this.afterFlushTasks.clear();
4612
4804
  for (let i = 0; i < snapshot.length; i++) snapshot[i]();
4613
4805
  }
4614
- if (this.dirtyByLevel.size === 0) return;
4806
+ if (this.minLevel > this.maxLevel) return;
4615
4807
  let level = this.minLevel;
4616
4808
  let max = this.maxLevel;
4617
4809
  while (level <= max) {
4618
4810
  for (; ; ) {
4619
- const bucket = this.dirtyByLevel.get(level);
4620
- if (!bucket || bucket.size === 0) break;
4621
- this.dirtyByLevel.delete(level);
4811
+ const bucket = this.dirtyByLevel[level];
4812
+ if (bucket === void 0 || bucket.size === 0) break;
4622
4813
  const snapshot = Array.from(bucket);
4814
+ bucket.clear();
4623
4815
  for (let i = 0; i < snapshot.length; i++) snapshot[i]();
4624
4816
  }
4625
4817
  if (this.maxLevel > max) max = this.maxLevel;
@@ -4627,24 +4819,38 @@ var OIMComputeRuntime = class {
4627
4819
  }
4628
4820
  } finally {
4629
4821
  this.isFlushing = false;
4630
- if (this.dirtyByLevel.size === 0) {
4631
- this.minLevel = Infinity;
4632
- this.maxLevel = -Infinity;
4633
- } else {
4634
- let nextMin = Infinity;
4635
- let nextMax = -Infinity;
4636
- this.dirtyByLevel.forEach((_bucket, lvl) => {
4637
- if (lvl < nextMin) nextMin = lvl;
4638
- if (lvl > nextMax) nextMax = lvl;
4639
- });
4640
- this.minLevel = nextMin;
4641
- this.maxLevel = nextMax;
4642
- this.isFlushScheduled = true;
4822
+ this.recomputePendingRange();
4823
+ }
4824
+ }
4825
+ recomputePendingRange() {
4826
+ let nextMin = Infinity;
4827
+ let nextMax = -Infinity;
4828
+ const buckets = this.dirtyByLevel;
4829
+ for (let i = 0; i < buckets.length; i++) {
4830
+ const bucket = buckets[i];
4831
+ if (bucket !== void 0 && bucket.size > 0) {
4832
+ if (i < nextMin) nextMin = i;
4833
+ if (i > nextMax) nextMax = i;
4643
4834
  }
4644
4835
  }
4836
+ this.minLevel = nextMin;
4837
+ this.maxLevel = nextMax;
4838
+ if (nextMin <= nextMax) this.isFlushScheduled = true;
4645
4839
  }
4646
4840
  };
4647
4841
 
4842
+ // src/dx/oimComputeRuntimeByQueue.ts
4843
+ var oimComputeRuntimeByQueue = /* @__PURE__ */ new WeakMap();
4844
+
4845
+ // src/dx/getOIMComputeRuntime.ts
4846
+ function getOIMComputeRuntime(queue) {
4847
+ const runtime = oimComputeRuntimeByQueue.get(queue);
4848
+ if (runtime !== void 0 && !runtime.isDestroyed) return runtime;
4849
+ const nextRuntime = new OIMComputeRuntime(queue);
4850
+ oimComputeRuntimeByQueue.set(queue, nextRuntime);
4851
+ return nextRuntime;
4852
+ }
4853
+
4648
4854
  // src/modules/selector/core/OIMSelector.ts
4649
4855
  var OIMSelector = class {
4650
4856
  constructor(runtime, sourceDeps, computedDeps = []) {
@@ -4905,8 +5111,9 @@ var OIMEntitiesByIndexKeyArrayBasedSelector = class extends OIMSelector {
4905
5111
  areEqual(prev, next) {
4906
5112
  if (prev === next) return true;
4907
5113
  if (prev.length !== next.length) return false;
4908
- for (let i = 0; i < prev.length; i++)
5114
+ for (let i = 0; i < prev.length; i++) {
4909
5115
  if (prev[i] !== next[i]) return false;
5116
+ }
4910
5117
  return true;
4911
5118
  }
4912
5119
  };
@@ -4976,8 +5183,9 @@ var OIMEntitiesByIndexKeySetBasedSelector = class extends OIMSelector {
4976
5183
  areEqual(prev, next) {
4977
5184
  if (prev === next) return true;
4978
5185
  if (prev.length !== next.length) return false;
4979
- for (let i = 0; i < prev.length; i++)
5186
+ for (let i = 0; i < prev.length; i++) {
4980
5187
  if (prev[i] !== next[i]) return false;
5188
+ }
4981
5189
  return true;
4982
5190
  }
4983
5191
  };
@@ -5041,8 +5249,9 @@ var OIMEntitiesByGlobalIndexArrayBasedSelector = class extends OIMSelector {
5041
5249
  areEqual(prev, next) {
5042
5250
  if (prev === next) return true;
5043
5251
  if (prev.length !== next.length) return false;
5044
- for (let i = 0; i < prev.length; i++)
5252
+ for (let i = 0; i < prev.length; i++) {
5045
5253
  if (prev[i] !== next[i]) return false;
5254
+ }
5046
5255
  return true;
5047
5256
  }
5048
5257
  };
@@ -5107,8 +5316,9 @@ var OIMEntitiesByGlobalIndexSetBasedSelector = class extends OIMSelector {
5107
5316
  areEqual(prev, next) {
5108
5317
  if (prev === next) return true;
5109
5318
  if (prev.length !== next.length) return false;
5110
- for (let i = 0; i < prev.length; i++)
5319
+ for (let i = 0; i < prev.length; i++) {
5111
5320
  if (prev[i] !== next[i]) return false;
5321
+ }
5112
5322
  return true;
5113
5323
  }
5114
5324
  };
@@ -5117,7 +5327,7 @@ var OIMEntitiesByGlobalIndexSetBasedSelector = class extends OIMSelector {
5117
5327
  var OIMCollectionSelectors = class {
5118
5328
  constructor(queue, collection) {
5119
5329
  this.collection = collection;
5120
- this.runtime = new OIMComputeRuntime(queue);
5330
+ this.runtime = getOIMComputeRuntime(queue);
5121
5331
  }
5122
5332
  byPk(pk) {
5123
5333
  return new OIMCollectionByPkSelector(this.runtime, this.collection, pk);
@@ -5184,127 +5394,408 @@ var OIMCollectionSelectors = class {
5184
5394
  }
5185
5395
  };
5186
5396
 
5187
- // src/dx/OIMCollectionKit.ts
5188
- function createOIMCollectionKit(queue, opts) {
5189
- const collection = new OIMReactiveCollection(queue, opts);
5190
- const indexFactory = new OIMCollectionIndexFactory(queue, collection);
5191
- const select = new OIMCollectionSelectors(queue, collection);
5192
- const scope = new OIMDisposeScope();
5193
- scope.add(collection);
5194
- return {
5195
- queue,
5196
- collection,
5197
- indexFactory,
5198
- select,
5199
- scope,
5200
- destroy: () => scope.destroy()
5201
- };
5202
- }
5397
+ // src/modules/effect/core/OIMEffectDependencyKeyed.ts
5398
+ var OIMEffectDependencyKeyed = class {
5399
+ constructor(source, key) {
5400
+ this.source = source;
5401
+ this.key = key;
5402
+ }
5403
+ subscribe(onUpdate) {
5404
+ return this.source.subscribeOnKey(this.key, onUpdate);
5405
+ }
5406
+ };
5203
5407
 
5204
- // src/modules/wrapper/collection/OIMCollectionChangedFields.ts
5205
- var OIMCollectionChangedFields = class {
5206
- constructor(queue, collection, opts) {
5207
- this.isInWrite = false;
5208
- this.lastEntitySnapshotByPk = /* @__PURE__ */ new Map();
5209
- // PKs written through this wrapper during the current flush. Their fields are
5210
- // already tracked accurately via trackPatch, so the flush-time external-write
5211
- // handler skips them — no redundant diff + snapshot re-clone. Cleared per flush.
5212
- this.writtenThroughThisFlush = /* @__PURE__ */ new Set();
5213
- this.changedFieldsByPk = /* @__PURE__ */ new Map();
5214
- this.changedPksByField = /* @__PURE__ */ new Map();
5215
- this.onAfterFlush = () => {
5216
- this.clear();
5217
- this.writtenThroughThisFlush.clear();
5218
- };
5219
- this.onCollectionUpdate = (payload) => {
5220
- if (this.isInWrite) return;
5221
- if (payload.pks.length === 0) return;
5222
- for (let i = 0; i < payload.pks.length; i++) {
5223
- const pk = payload.pks[i];
5224
- if (this.writtenThroughThisFlush.has(pk)) continue;
5225
- this.trackExternalMutationByPk(pk);
5226
- }
5227
- };
5228
- this.collection = collection;
5229
- this.detectExternalMutations = opts?.detectExternalMutations !== false;
5230
- this.selectPk = opts?.selectPk ?? (() => {
5231
- throw new Error(
5232
- `[OIMCollectionChangedFields]: selectPk was not provided. Use upsertOneByPk(...) or pass opts.selectPk.`
5233
- );
5234
- });
5235
- this.changedPksEventEmitter = new OIMUpdateEventEmitter(
5236
- queue,
5237
- "after_flush"
5238
- );
5239
- this.changedFieldsEventEmitter = new OIMUpdateEventEmitter(queue, "after_flush");
5240
- this.unsubscribeAfterFlush = queue.emitter.on(
5241
- 0 /* AFTER_FLUSH */,
5242
- this.onAfterFlush
5243
- );
5244
- if (this.detectExternalMutations) {
5245
- const maybeReactive = collection;
5246
- this.unsubscribeFromCollectionEmitter = maybeReactive.subscribeOnAnyUpdate ? maybeReactive.subscribeOnAnyUpdate((pks) => {
5247
- this.onCollectionUpdate({
5248
- pks
5249
- });
5250
- }) : collection.emitter.on(
5251
- 0 /* UPDATE */,
5252
- this.onCollectionUpdate
5253
- );
5254
- }
5408
+ // src/modules/effect/core/OIMEffectDependencyKeyedCollection.ts
5409
+ var OIMEffectDependencyKeyedCollection = class {
5410
+ constructor(collection, pk) {
5411
+ this.dep = new OIMEffectDependencyKeyed(collection, pk);
5255
5412
  }
5256
- upsertOneByPk(pk, patch) {
5257
- const slot = this.withWrite(() => {
5258
- this.trackPatch(pk, patch);
5259
- return this.collection.upsertOneByPk(pk, patch);
5260
- });
5261
- this.updateEntitySnapshot(pk);
5262
- return slot;
5413
+ get source() {
5414
+ return this.dep.source;
5263
5415
  }
5264
- upsertOne(entity) {
5265
- const slot = this.withWrite(() => {
5266
- const pk = this.selectPk(entity);
5267
- this.trackPatch(pk, entity);
5268
- return this.collection.upsertOne(entity);
5269
- });
5270
- this.updateEntitySnapshot(slot.pk);
5271
- return slot;
5416
+ subscribe(onUpdate) {
5417
+ return this.dep.subscribe(onUpdate);
5272
5418
  }
5273
- upsertMany(entities) {
5274
- const pks = [];
5275
- const slots = this.withWrite(() => {
5276
- for (const entity of entities) {
5277
- const pk = this.selectPk(entity);
5278
- pks.push(pk);
5279
- this.trackPatch(pk, entity);
5280
- }
5281
- return this.collection.upsertMany(entities);
5282
- });
5283
- for (let i = 0; i < pks.length; i++) this.updateEntitySnapshot(pks[i]);
5284
- return slots;
5419
+ };
5420
+
5421
+ // src/modules/effect/core/OIMEffectDependencyKeyedIndex.ts
5422
+ var OIMEffectDependencyKeyedIndex = class {
5423
+ constructor(index, key) {
5424
+ this.dep = new OIMEffectDependencyKeyed(index, key);
5285
5425
  }
5286
- getChangedFieldsByPk(pk) {
5287
- return this.changedFieldsByPk.get(pk) ?? /* @__PURE__ */ new Set();
5426
+ get source() {
5427
+ return this.dep.source;
5288
5428
  }
5289
- consumeChangedFieldsByPk(pk) {
5290
- const existing = this.changedFieldsByPk.get(pk);
5291
- if (!existing) return /* @__PURE__ */ new Set();
5292
- return new Set(existing);
5429
+ subscribe(onUpdate) {
5430
+ return this.dep.subscribe(onUpdate);
5293
5431
  }
5294
- getChangedPksByField(field) {
5295
- return this.changedPksByField.get(field) ?? /* @__PURE__ */ new Set();
5432
+ };
5433
+
5434
+ // src/modules/effect/core/OIMEffectDependencyKeyedObject.ts
5435
+ var OIMEffectDependencyKeyedObject = class {
5436
+ constructor(object, key) {
5437
+ this.dep = new OIMEffectDependencyKeyed(object, key);
5296
5438
  }
5297
- consumeChangedPksByField(field) {
5298
- const existing = this.changedPksByField.get(field);
5299
- if (!existing) return /* @__PURE__ */ new Set();
5300
- return new Set(existing);
5439
+ get source() {
5440
+ return this.dep.source;
5301
5441
  }
5302
- clear() {
5303
- this.changedFieldsByPk.clear();
5304
- this.changedPksByField.clear();
5442
+ subscribe(onUpdate) {
5443
+ return this.dep.subscribe(onUpdate);
5305
5444
  }
5306
- destroy() {
5307
- this.changedPksEventEmitter.destroy();
5445
+ };
5446
+
5447
+ // src/dx/on.ts
5448
+ var on = {
5449
+ /** Depend on one PK of a collection. */
5450
+ collection: (collection, pk) => new OIMEffectDependencyKeyedCollection(collection, pk),
5451
+ /** Depend on one key of an index. */
5452
+ index: (index, key) => new OIMEffectDependencyKeyedIndex(index, key),
5453
+ /** Depend on one key of a reactive object. */
5454
+ object: (object, key) => new OIMEffectDependencyKeyedObject(object, key),
5455
+ /** Depend on another computed. */
5456
+ computed: (computed) => new OIMEffectDependencyComputed(computed)
5457
+ };
5458
+
5459
+ // src/modules/effect/core/OIMEffect.ts
5460
+ var OIMEffect = class {
5461
+ constructor(runtime, opts, level) {
5462
+ this.sourceDeps = [];
5463
+ this.computedDeps = [];
5464
+ this.sourceUnsubscribers = [];
5465
+ this.computedUnsubscribers = [];
5466
+ this.isResubscribeEnqueued = false;
5467
+ this.isTemporarilyUnsubscribedForFlush = false;
5468
+ this.isScheduled = false;
5469
+ this.runTask = () => {
5470
+ this.isScheduled = false;
5471
+ this.dequeueScheduled = void 0;
5472
+ this.runOnce();
5473
+ };
5474
+ this.onSourceInvalidate = () => {
5475
+ this.onUpdate?.();
5476
+ if (!this.isTemporarilyUnsubscribedForFlush) {
5477
+ this.isTemporarilyUnsubscribedForFlush = true;
5478
+ this.unsubscribeFromSourceDeps();
5479
+ this.ensureResubscribeAfterFlush();
5480
+ }
5481
+ if (this.isScheduled) return;
5482
+ this.isScheduled = true;
5483
+ this.dequeueScheduled = this.runtime.schedule(this.runTask, this.level);
5484
+ };
5485
+ this.onComputedInvalidate = () => {
5486
+ this.onUpdate?.();
5487
+ if (this.isScheduled) return;
5488
+ this.isScheduled = true;
5489
+ this.dequeueScheduled = this.runtime.schedule(this.runTask, this.level);
5490
+ };
5491
+ this.onAfterFlush = () => {
5492
+ if (!this.isResubscribeEnqueued) return;
5493
+ this.isResubscribeEnqueued = false;
5494
+ this.dequeueResubscribe = void 0;
5495
+ this.isTemporarilyUnsubscribedForFlush = false;
5496
+ if (this.sourceUnsubscribers.length === 0) {
5497
+ for (const dep of this.sourceDeps) {
5498
+ this.sourceUnsubscribers.push(
5499
+ dep.subscribe(this.onSourceInvalidate)
5500
+ );
5501
+ }
5502
+ }
5503
+ };
5504
+ this.runtime = runtime;
5505
+ this.onUpdate = opts.onUpdate;
5506
+ this.run = opts.run;
5507
+ this.deps = opts.deps ?? [];
5508
+ for (const dep of this.deps) {
5509
+ if (dep instanceof OIMEffectDependencyComputed) {
5510
+ this.computedDeps.push(dep);
5511
+ } else {
5512
+ this.sourceDeps.push(dep);
5513
+ }
5514
+ }
5515
+ this.level = level ?? this.runtime.computeLevel(this.deps);
5516
+ this.subscribeToDeps();
5517
+ }
5518
+ destroy() {
5519
+ if (this.dequeueScheduled) {
5520
+ this.dequeueScheduled();
5521
+ this.dequeueScheduled = void 0;
5522
+ }
5523
+ this.unsubscribeFromAllDeps();
5524
+ if (this.dequeueResubscribe) {
5525
+ this.dequeueResubscribe();
5526
+ this.dequeueResubscribe = void 0;
5527
+ }
5528
+ this.isResubscribeEnqueued = false;
5529
+ this.isTemporarilyUnsubscribedForFlush = false;
5530
+ }
5531
+ runOnce() {
5532
+ this.run();
5533
+ }
5534
+ subscribeToDeps() {
5535
+ if (this.sourceUnsubscribers.length > 0 || this.computedUnsubscribers.length > 0)
5536
+ return;
5537
+ for (const dep of this.sourceDeps) {
5538
+ this.sourceUnsubscribers.push(
5539
+ dep.subscribe(this.onSourceInvalidate)
5540
+ );
5541
+ }
5542
+ for (const dep of this.computedDeps) {
5543
+ this.computedUnsubscribers.push(
5544
+ dep.subscribe(this.onComputedInvalidate)
5545
+ );
5546
+ }
5547
+ }
5548
+ unsubscribeFromSourceDeps() {
5549
+ if (this.sourceUnsubscribers.length === 0) return;
5550
+ for (const unsubscribe of this.sourceUnsubscribers) unsubscribe();
5551
+ this.sourceUnsubscribers = [];
5552
+ }
5553
+ unsubscribeFromAllDeps() {
5554
+ this.unsubscribeFromSourceDeps();
5555
+ if (this.computedUnsubscribers.length === 0) return;
5556
+ for (const unsubscribe of this.computedUnsubscribers) unsubscribe();
5557
+ this.computedUnsubscribers = [];
5558
+ }
5559
+ ensureResubscribeAfterFlush() {
5560
+ if (this.isResubscribeEnqueued) return;
5561
+ this.isResubscribeEnqueued = true;
5562
+ this.dequeueResubscribe = this.runtime.scheduleAfterFlush(
5563
+ this.onAfterFlush
5564
+ );
5565
+ }
5566
+ };
5567
+
5568
+ // src/modules/computed/enums/EOIMComputedEventType.ts
5569
+ var EOIMComputedEventType = /* @__PURE__ */ ((EOIMComputedEventType2) => {
5570
+ EOIMComputedEventType2["UPDATE"] = "update";
5571
+ return EOIMComputedEventType2;
5572
+ })(EOIMComputedEventType || {});
5573
+
5574
+ // src/modules/computed/core/OIMComputed.ts
5575
+ var OIMComputed = class {
5576
+ constructor(runtime, opts) {
5577
+ this.emitter = new OIMEventEmitter();
5578
+ this.hasValue = false;
5579
+ this.isDirty = true;
5580
+ this.runtime = runtime;
5581
+ this.compute = opts.compute;
5582
+ this.compare = opts.compare ?? Object.is;
5583
+ this._deps = opts.deps ?? [];
5584
+ this.updateEventEmitter = new OIMUpdateEventEmitter(
5585
+ this.runtime.queue,
5586
+ "immediate"
5587
+ );
5588
+ const level = this.runtime.computeLevel(this._deps);
5589
+ this.runtime.registerLevel(this, level);
5590
+ this.effect = new OIMEffect(
5591
+ this.runtime,
5592
+ {
5593
+ deps: this._deps,
5594
+ onUpdate: () => {
5595
+ this.isDirty = true;
5596
+ },
5597
+ run: () => {
5598
+ this.recomputeAndEmitIfChanged();
5599
+ }
5600
+ },
5601
+ level
5602
+ );
5603
+ }
5604
+ get needsRecompute() {
5605
+ return this.isDirty;
5606
+ }
5607
+ get isReady() {
5608
+ return this.hasValue;
5609
+ }
5610
+ get deps() {
5611
+ return this._deps;
5612
+ }
5613
+ get() {
5614
+ this.recomputeAndEmitIfChanged();
5615
+ return this.value;
5616
+ }
5617
+ getIfReady() {
5618
+ return this.hasValue ? this.value : void 0;
5619
+ }
5620
+ destroy() {
5621
+ this.effect.destroy();
5622
+ this.updateEventEmitter.destroy();
5623
+ this.emitter.offAll();
5624
+ }
5625
+ recomputeAndEmitIfChanged() {
5626
+ if (!this.isDirty) return;
5627
+ const prev = this.value;
5628
+ const hadPrev = this.hasValue;
5629
+ const next = this.compute();
5630
+ this.value = next;
5631
+ this.hasValue = true;
5632
+ this.isDirty = false;
5633
+ const changed = !hadPrev || !this.compare(prev, next);
5634
+ if (!changed) return;
5635
+ this.emitter.emit("update" /* UPDATE */, {
5636
+ keys: ["value"]
5637
+ });
5638
+ this.updateEventEmitter.markUpdatedKey("value");
5639
+ }
5640
+ };
5641
+
5642
+ // src/dx/OIMCollectionKit.ts
5643
+ function createOIMCollectionKit(queue, opts) {
5644
+ const collection = new OIMReactiveCollection(queue, opts);
5645
+ const indexFactory = new OIMCollectionIndexFactory(queue, collection);
5646
+ const select = new OIMCollectionSelectors(queue, collection);
5647
+ const runtime = getOIMComputeRuntime(queue);
5648
+ const scope = new OIMDisposeScope();
5649
+ scope.add(collection);
5650
+ return {
5651
+ queue,
5652
+ collection,
5653
+ indexFactory,
5654
+ select,
5655
+ on,
5656
+ computed: (deps, compute, options) => {
5657
+ const computed = new OIMComputed(runtime, {
5658
+ deps,
5659
+ compute,
5660
+ compare: options?.compare
5661
+ });
5662
+ scope.add(computed);
5663
+ return computed;
5664
+ },
5665
+ effect: (deps, run, options) => {
5666
+ const effect = new OIMEffect(runtime, {
5667
+ deps,
5668
+ run,
5669
+ onUpdate: options?.onUpdate
5670
+ });
5671
+ scope.add(effect);
5672
+ return effect;
5673
+ },
5674
+ scope,
5675
+ destroy: () => scope.destroy()
5676
+ };
5677
+ }
5678
+
5679
+ // src/dx/peekOIMComputeRuntime.ts
5680
+ function peekOIMComputeRuntime(queue) {
5681
+ const runtime = oimComputeRuntimeByQueue.get(queue);
5682
+ return runtime === void 0 || runtime.isDestroyed ? void 0 : runtime;
5683
+ }
5684
+
5685
+ // src/dx/setOIMComputeRuntime.ts
5686
+ function setOIMComputeRuntime(queue, runtime) {
5687
+ if (runtime.queue !== queue) {
5688
+ throw new Error(
5689
+ "setOIMComputeRuntime: the runtime is attached to a different queue. A runtime drains on its own queue and would never run here."
5690
+ );
5691
+ }
5692
+ oimComputeRuntimeByQueue.set(queue, runtime);
5693
+ }
5694
+
5695
+ // src/modules/wrapper/collection/OIMCollectionChangedFields.ts
5696
+ var OIMCollectionChangedFields = class {
5697
+ constructor(queue, collection, opts) {
5698
+ this.isInWrite = false;
5699
+ this.lastEntitySnapshotByPk = /* @__PURE__ */ new Map();
5700
+ // PKs written through this wrapper during the current flush. Their fields are
5701
+ // already tracked accurately via trackPatch, so the flush-time external-write
5702
+ // handler skips them — no redundant diff + snapshot re-clone. Cleared per flush.
5703
+ this.writtenThroughThisFlush = /* @__PURE__ */ new Set();
5704
+ this.changedFieldsByPk = /* @__PURE__ */ new Map();
5705
+ this.changedPksByField = /* @__PURE__ */ new Map();
5706
+ this.onAfterFlush = () => {
5707
+ this.clear();
5708
+ this.writtenThroughThisFlush.clear();
5709
+ };
5710
+ this.onCollectionUpdate = (payload) => {
5711
+ if (this.isInWrite) return;
5712
+ if (payload.pks.length === 0) return;
5713
+ for (let i = 0; i < payload.pks.length; i++) {
5714
+ const pk = payload.pks[i];
5715
+ if (this.writtenThroughThisFlush.has(pk)) continue;
5716
+ this.trackExternalMutationByPk(pk);
5717
+ }
5718
+ };
5719
+ this.collection = collection;
5720
+ this.detectExternalMutations = opts?.detectExternalMutations !== false;
5721
+ this.selectPk = opts?.selectPk ?? (() => {
5722
+ throw new Error(
5723
+ `[OIMCollectionChangedFields]: selectPk was not provided. Use upsertOneByPk(...) or pass opts.selectPk.`
5724
+ );
5725
+ });
5726
+ this.changedPksEventEmitter = new OIMUpdateEventEmitter(
5727
+ queue,
5728
+ "after_flush"
5729
+ );
5730
+ this.changedFieldsEventEmitter = new OIMUpdateEventEmitter(queue, "after_flush");
5731
+ this.unsubscribeAfterFlush = queue.emitter.on(
5732
+ 0 /* AFTER_FLUSH */,
5733
+ this.onAfterFlush
5734
+ );
5735
+ if (this.detectExternalMutations) {
5736
+ const maybeReactive = collection;
5737
+ this.unsubscribeFromCollectionEmitter = maybeReactive.subscribeOnAnyUpdate ? maybeReactive.subscribeOnAnyUpdate((pks) => {
5738
+ this.onCollectionUpdate({
5739
+ pks
5740
+ });
5741
+ }) : collection.emitter.on(
5742
+ 0 /* UPDATE */,
5743
+ this.onCollectionUpdate
5744
+ );
5745
+ }
5746
+ }
5747
+ upsertOneByPk(pk, patch) {
5748
+ const slot = this.withWrite(() => {
5749
+ this.trackPatch(pk, patch);
5750
+ return this.collection.upsertOneByPk(pk, patch);
5751
+ });
5752
+ this.updateEntitySnapshot(pk);
5753
+ return slot;
5754
+ }
5755
+ upsertOne(entity) {
5756
+ const slot = this.withWrite(() => {
5757
+ const pk = this.selectPk(entity);
5758
+ this.trackPatch(pk, entity);
5759
+ return this.collection.upsertOne(entity);
5760
+ });
5761
+ this.updateEntitySnapshot(slot.pk);
5762
+ return slot;
5763
+ }
5764
+ upsertMany(entities) {
5765
+ const pks = [];
5766
+ const slots = this.withWrite(() => {
5767
+ for (const entity of entities) {
5768
+ const pk = this.selectPk(entity);
5769
+ pks.push(pk);
5770
+ this.trackPatch(pk, entity);
5771
+ }
5772
+ return this.collection.upsertMany(entities);
5773
+ });
5774
+ for (let i = 0; i < pks.length; i++) this.updateEntitySnapshot(pks[i]);
5775
+ return slots;
5776
+ }
5777
+ getChangedFieldsByPk(pk) {
5778
+ return this.changedFieldsByPk.get(pk) ?? /* @__PURE__ */ new Set();
5779
+ }
5780
+ consumeChangedFieldsByPk(pk) {
5781
+ const existing = this.changedFieldsByPk.get(pk);
5782
+ if (!existing) return /* @__PURE__ */ new Set();
5783
+ return new Set(existing);
5784
+ }
5785
+ getChangedPksByField(field) {
5786
+ return this.changedPksByField.get(field) ?? /* @__PURE__ */ new Set();
5787
+ }
5788
+ consumeChangedPksByField(field) {
5789
+ const existing = this.changedPksByField.get(field);
5790
+ if (!existing) return /* @__PURE__ */ new Set();
5791
+ return new Set(existing);
5792
+ }
5793
+ clear() {
5794
+ this.changedFieldsByPk.clear();
5795
+ this.changedPksByField.clear();
5796
+ }
5797
+ destroy() {
5798
+ this.changedPksEventEmitter.destroy();
5308
5799
  this.changedFieldsEventEmitter.destroy();
5309
5800
  this.unsubscribeAfterFlush();
5310
5801
  this.unsubscribeFromCollectionEmitter?.();
@@ -5343,8 +5834,9 @@ var OIMCollectionChangedFields = class {
5343
5834
  Object.keys(nextObj).forEach((k) => keys.add(k));
5344
5835
  const changed = [];
5345
5836
  keys.forEach((k) => {
5346
- if (prevObj[k] !== nextObj[k])
5837
+ if (prevObj[k] !== nextObj[k]) {
5347
5838
  changed.push(k);
5839
+ }
5348
5840
  });
5349
5841
  return changed;
5350
5842
  }
@@ -5510,10 +6002,15 @@ var OIMOrderedListMappedCommandStream = class _OIMOrderedListMappedCommandStream
5510
6002
  try {
5511
6003
  const subs = this.subscribers.get(key);
5512
6004
  if (subs && subs.size > 0) {
5513
- const snapshot = Array.from(subs);
5514
- for (let s = 0; s < snapshot.length; s++) {
5515
- const handler = snapshot[s];
5516
- if (subs.has(handler)) handler();
6005
+ if (subs.size === 1) {
6006
+ const only = subs.values().next().value;
6007
+ if (only) only();
6008
+ } else {
6009
+ const snapshot = Array.from(subs);
6010
+ for (let s = 0; s < snapshot.length; s++) {
6011
+ const handler = snapshot[s];
6012
+ if (subs.has(handler)) handler();
6013
+ }
5517
6014
  }
5518
6015
  }
5519
6016
  } finally {
@@ -5807,10 +6304,13 @@ var OIMEventQueueSchedulerAnimationFrame = class extends OIMEventQueueScheduler
5807
6304
  this.flush();
5808
6305
  });
5809
6306
  } else {
5810
- this.frameId = setTimeout(() => {
5811
- this.frameId = void 0;
5812
- this.flush();
5813
- }, 16);
6307
+ this.frameId = this.unref(
6308
+ setTimeout(() => {
6309
+ this.frameId = void 0;
6310
+ this.flush();
6311
+ }, 16)
6312
+ // ~60fps
6313
+ );
5814
6314
  }
5815
6315
  }
5816
6316
  cancel() {
@@ -5856,10 +6356,12 @@ var OIMEventQueueSchedulerTimeout = class extends OIMEventQueueScheduler {
5856
6356
  }
5857
6357
  schedule() {
5858
6358
  if (this.timeoutId !== void 0) return;
5859
- this.timeoutId = setTimeout(() => {
5860
- this.timeoutId = void 0;
5861
- this.flush();
5862
- }, this.delay);
6359
+ this.timeoutId = this.unref(
6360
+ setTimeout(() => {
6361
+ this.timeoutId = void 0;
6362
+ this.flush();
6363
+ }, this.delay)
6364
+ );
5863
6365
  }
5864
6366
  cancel() {
5865
6367
  if (this.timeoutId === void 0) return;
@@ -5893,13 +6395,13 @@ var OIMEventQueueSchedulerImmediate = class extends OIMEventQueueScheduler {
5893
6395
  this.flush();
5894
6396
  };
5895
6397
  if (this.useSetImmediate) {
5896
- this.immediateId = setImmediate(callback);
6398
+ this.immediateId = this.unref(setImmediate(callback));
5897
6399
  } else if (this.useMessageChannel && this.messageChannel) {
5898
6400
  this.immediateId = 1;
5899
6401
  this.pendingCallback = callback;
5900
6402
  this.messageChannel.port1.postMessage(null);
5901
6403
  } else {
5902
- this.immediateId = setTimeout(callback, 0);
6404
+ this.immediateId = this.unref(setTimeout(callback, 0));
5903
6405
  }
5904
6406
  }
5905
6407
  cancel() {
@@ -5992,242 +6494,6 @@ var OIMDBSettings = class {
5992
6494
  OIMDBSettings.UPDATED_KEYS_INDEX_KEY = "__updatedKeys__";
5993
6495
  OIMDBSettings.FLUSHING_KEYS_INDEX_KEY = "__flushingKeys__";
5994
6496
 
5995
- // src/modules/effect/core/OIMEffectDependencyComputed.ts
5996
- var OIMEffectDependencyComputed = class {
5997
- constructor(source) {
5998
- this.source = source;
5999
- }
6000
- subscribe(onUpdate) {
6001
- return this.source.emitter.on("update", onUpdate);
6002
- }
6003
- };
6004
-
6005
- // src/modules/effect/core/OIMEffect.ts
6006
- var OIMEffect = class {
6007
- constructor(runtime, opts) {
6008
- this.sourceDeps = [];
6009
- this.computedDeps = [];
6010
- this.sourceUnsubscribers = [];
6011
- this.computedUnsubscribers = [];
6012
- this.isResubscribeEnqueued = false;
6013
- this.isTemporarilyUnsubscribedForFlush = false;
6014
- this.isScheduled = false;
6015
- this.runTask = () => {
6016
- this.isScheduled = false;
6017
- this.dequeueScheduled = void 0;
6018
- this.runOnce();
6019
- };
6020
- this.onSourceInvalidate = () => {
6021
- this.onUpdate?.();
6022
- if (!this.isTemporarilyUnsubscribedForFlush) {
6023
- this.isTemporarilyUnsubscribedForFlush = true;
6024
- this.unsubscribeFromSourceDeps();
6025
- this.ensureResubscribeAfterFlush();
6026
- }
6027
- if (this.isScheduled) return;
6028
- this.isScheduled = true;
6029
- this.dequeueScheduled = this.runtime.schedule(this.runTask);
6030
- };
6031
- this.onComputedInvalidate = () => {
6032
- this.onUpdate?.();
6033
- if (this.isScheduled) return;
6034
- this.isScheduled = true;
6035
- this.dequeueScheduled = this.runtime.schedule(this.runTask);
6036
- };
6037
- this.onAfterFlush = () => {
6038
- if (!this.isResubscribeEnqueued) return;
6039
- this.isResubscribeEnqueued = false;
6040
- this.dequeueResubscribe = void 0;
6041
- this.isTemporarilyUnsubscribedForFlush = false;
6042
- if (this.sourceUnsubscribers.length === 0) {
6043
- for (const dep of this.sourceDeps) {
6044
- this.sourceUnsubscribers.push(
6045
- dep.subscribe(this.onSourceInvalidate)
6046
- );
6047
- }
6048
- }
6049
- };
6050
- this.runtime = runtime;
6051
- this.onUpdate = opts.onUpdate;
6052
- this.run = opts.run;
6053
- this.deps = opts.deps ?? [];
6054
- for (const dep of this.deps) {
6055
- if (dep instanceof OIMEffectDependencyComputed) {
6056
- this.computedDeps.push(dep);
6057
- } else {
6058
- this.sourceDeps.push(dep);
6059
- }
6060
- }
6061
- this.subscribeToDeps();
6062
- }
6063
- destroy() {
6064
- if (this.dequeueScheduled) {
6065
- this.dequeueScheduled();
6066
- this.dequeueScheduled = void 0;
6067
- }
6068
- this.unsubscribeFromAllDeps();
6069
- if (this.dequeueResubscribe) {
6070
- this.dequeueResubscribe();
6071
- this.dequeueResubscribe = void 0;
6072
- }
6073
- this.isResubscribeEnqueued = false;
6074
- this.isTemporarilyUnsubscribedForFlush = false;
6075
- }
6076
- runOnce() {
6077
- this.run();
6078
- }
6079
- subscribeToDeps() {
6080
- if (this.sourceUnsubscribers.length > 0 || this.computedUnsubscribers.length > 0)
6081
- return;
6082
- for (const dep of this.sourceDeps) {
6083
- this.sourceUnsubscribers.push(
6084
- dep.subscribe(this.onSourceInvalidate)
6085
- );
6086
- }
6087
- for (const dep of this.computedDeps) {
6088
- this.computedUnsubscribers.push(
6089
- dep.subscribe(this.onComputedInvalidate)
6090
- );
6091
- }
6092
- }
6093
- unsubscribeFromSourceDeps() {
6094
- if (this.sourceUnsubscribers.length === 0) return;
6095
- for (const unsubscribe of this.sourceUnsubscribers) unsubscribe();
6096
- this.sourceUnsubscribers = [];
6097
- }
6098
- unsubscribeFromAllDeps() {
6099
- this.unsubscribeFromSourceDeps();
6100
- if (this.computedUnsubscribers.length === 0) return;
6101
- for (const unsubscribe of this.computedUnsubscribers) unsubscribe();
6102
- this.computedUnsubscribers = [];
6103
- }
6104
- ensureResubscribeAfterFlush() {
6105
- if (this.isResubscribeEnqueued) return;
6106
- this.isResubscribeEnqueued = true;
6107
- this.dequeueResubscribe = this.runtime.scheduleAfterFlush(
6108
- this.onAfterFlush
6109
- );
6110
- }
6111
- };
6112
-
6113
- // src/modules/computed/enums/EOIMComputedEventType.ts
6114
- var EOIMComputedEventType = /* @__PURE__ */ ((EOIMComputedEventType2) => {
6115
- EOIMComputedEventType2["UPDATE"] = "update";
6116
- return EOIMComputedEventType2;
6117
- })(EOIMComputedEventType || {});
6118
-
6119
- // src/modules/computed/core/OIMComputed.ts
6120
- var OIMComputed = class {
6121
- constructor(runtime, opts) {
6122
- this.emitter = new OIMEventEmitter();
6123
- this.hasValue = false;
6124
- this.isDirty = true;
6125
- this.runtime = runtime;
6126
- this.compute = opts.compute;
6127
- this.compare = opts.compare ?? Object.is;
6128
- this._deps = opts.deps ?? [];
6129
- this.updateEventEmitter = new OIMUpdateEventEmitter(
6130
- this.runtime.queue,
6131
- "immediate"
6132
- );
6133
- this.effect = new OIMEffect(this.runtime, {
6134
- deps: this._deps,
6135
- onUpdate: () => {
6136
- this.isDirty = true;
6137
- },
6138
- run: () => {
6139
- this.recomputeAndEmitIfChanged();
6140
- }
6141
- });
6142
- }
6143
- get needsRecompute() {
6144
- return this.isDirty;
6145
- }
6146
- get isReady() {
6147
- return this.hasValue;
6148
- }
6149
- get deps() {
6150
- return this._deps;
6151
- }
6152
- get() {
6153
- this.recomputeAndEmitIfChanged();
6154
- return this.value;
6155
- }
6156
- getIfReady() {
6157
- return this.hasValue ? this.value : void 0;
6158
- }
6159
- destroy() {
6160
- this.effect.destroy();
6161
- this.updateEventEmitter.destroy();
6162
- this.emitter.offAll();
6163
- }
6164
- recomputeAndEmitIfChanged() {
6165
- if (!this.isDirty) return;
6166
- const prev = this.value;
6167
- const hadPrev = this.hasValue;
6168
- const next = this.compute();
6169
- this.value = next;
6170
- this.hasValue = true;
6171
- this.isDirty = false;
6172
- const changed = !hadPrev || !this.compare(prev, next);
6173
- if (!changed) return;
6174
- this.emitter.emit("update" /* UPDATE */, {
6175
- keys: ["value"]
6176
- });
6177
- this.updateEventEmitter.markUpdatedKey("value");
6178
- }
6179
- };
6180
-
6181
- // src/modules/effect/core/OIMEffectDependencyKeyed.ts
6182
- var OIMEffectDependencyKeyed = class {
6183
- constructor(source, key) {
6184
- this.source = source;
6185
- this.key = key;
6186
- }
6187
- subscribe(onUpdate) {
6188
- return this.source.subscribeOnKey(this.key, onUpdate);
6189
- }
6190
- };
6191
-
6192
- // src/modules/effect/core/OIMEffectDependencyKeyedObject.ts
6193
- var OIMEffectDependencyKeyedObject = class {
6194
- constructor(object, key) {
6195
- this.dep = new OIMEffectDependencyKeyed(object, key);
6196
- }
6197
- get source() {
6198
- return this.dep.source;
6199
- }
6200
- subscribe(onUpdate) {
6201
- return this.dep.subscribe(onUpdate);
6202
- }
6203
- };
6204
-
6205
- // src/modules/effect/core/OIMEffectDependencyKeyedCollection.ts
6206
- var OIMEffectDependencyKeyedCollection = class {
6207
- constructor(collection, pk) {
6208
- this.dep = new OIMEffectDependencyKeyed(collection, pk);
6209
- }
6210
- get source() {
6211
- return this.dep.source;
6212
- }
6213
- subscribe(onUpdate) {
6214
- return this.dep.subscribe(onUpdate);
6215
- }
6216
- };
6217
-
6218
- // src/modules/effect/core/OIMEffectDependencyKeyedIndex.ts
6219
- var OIMEffectDependencyKeyedIndex = class {
6220
- constructor(index, key) {
6221
- this.dep = new OIMEffectDependencyKeyed(index, key);
6222
- }
6223
- get source() {
6224
- return this.dep.source;
6225
- }
6226
- subscribe(onUpdate) {
6227
- return this.dep.subscribe(onUpdate);
6228
- }
6229
- };
6230
-
6231
6497
  // src/modules/selector/core/OIMObjectValueByKeySelector.ts
6232
6498
  var OIMObjectValueByKeySelector = class extends OIMSelector {
6233
6499
  constructor(runtime, obj, key) {
@@ -6305,8 +6571,9 @@ var OIMIndexPksByKeyArrayBasedSelector = class extends OIMSelector {
6305
6571
  areEqual(prev, next) {
6306
6572
  if (prev === next) return true;
6307
6573
  if (prev.length !== next.length) return false;
6308
- for (let i = 0; i < prev.length; i++)
6574
+ for (let i = 0; i < prev.length; i++) {
6309
6575
  if (prev[i] !== next[i]) return false;
6576
+ }
6310
6577
  return true;
6311
6578
  }
6312
6579
  };
@@ -6329,8 +6596,9 @@ var OIMIndexPksByKeysSetBasedSelector = class extends OIMSelector {
6329
6596
  areEqual(prev, next) {
6330
6597
  if (prev === next) return true;
6331
6598
  if (prev.length !== next.length) return false;
6332
- for (let i = 0; i < prev.length; i++)
6599
+ for (let i = 0; i < prev.length; i++) {
6333
6600
  if (prev[i] !== next[i]) return false;
6601
+ }
6334
6602
  return true;
6335
6603
  }
6336
6604
  };
@@ -6353,8 +6621,9 @@ var OIMIndexPksByKeysArrayBasedSelector = class extends OIMSelector {
6353
6621
  areEqual(prev, next) {
6354
6622
  if (prev === next) return true;
6355
6623
  if (prev.length !== next.length) return false;
6356
- for (let i = 0; i < prev.length; i++)
6624
+ for (let i = 0; i < prev.length; i++) {
6357
6625
  if (prev[i] !== next[i]) return false;
6626
+ }
6358
6627
  return true;
6359
6628
  }
6360
6629
  };
@@ -6383,6 +6652,7 @@ export {
6383
6652
  OIMComputeRuntime,
6384
6653
  OIMComputed,
6385
6654
  OIMDBSettings,
6655
+ OIMDerivedCollection,
6386
6656
  OIMDerivedCollectionGlobalIndexArrayBased,
6387
6657
  OIMDerivedCollectionGlobalIndexSetBased,
6388
6658
  OIMDerivedCollectionIndexArrayBased,
@@ -6478,5 +6748,9 @@ export {
6478
6748
  createOIMIndexSlotMap,
6479
6749
  createOIMOrderedListCommandStreamDiffDriven,
6480
6750
  createOIMOrderedListMappedCommandStream,
6481
- diffOrderedListByPk
6751
+ diffOrderedListByPk,
6752
+ getOIMComputeRuntime,
6753
+ on,
6754
+ peekOIMComputeRuntime,
6755
+ setOIMComputeRuntime
6482
6756
  };