@oimdb/core 3.0.1 → 3.2.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.cjs CHANGED
@@ -524,265 +524,197 @@ var OIMIndexArrayBased = class extends OIMIndex {
524
524
  }
525
525
  };
526
526
 
527
- // src/enums/EOIMEventQueueEventType.ts
528
- var EOIMEventQueueEventType = /* @__PURE__ */ ((EOIMEventQueueEventType2) => {
529
- EOIMEventQueueEventType2[EOIMEventQueueEventType2["AFTER_FLUSH"] = 0] = "AFTER_FLUSH";
530
- EOIMEventQueueEventType2[EOIMEventQueueEventType2["BEFORE_FLUSH"] = 1] = "BEFORE_FLUSH";
531
- return EOIMEventQueueEventType2;
532
- })(EOIMEventQueueEventType || {});
533
-
534
- // src/core/OIMUpdateEventEmitter.ts
535
- var OIMUpdateEventEmitter = class {
536
- constructor(queue, deliveryMode = "queue") {
537
- this.keyHandlers = /* @__PURE__ */ new Map();
527
+ // src/core/OIMCarrierKeyedEmitter.ts
528
+ var OIMCarrierKeyedEmitter = class {
529
+ constructor(queue, resolver) {
530
+ // Carriers with at least one subscriber — for markAllUpdated / metrics.
531
+ this.subscribedCarriers = /* @__PURE__ */ new Set();
532
+ // Pending flush batch. A plain array + a `carrier.dirty` flag for dedup —
533
+ // marking is the hottest write-path op, and pushing to an array with a
534
+ // boolean check is ~4x cheaper than a `Set` identity-hash add. The flag
535
+ // doubles as membership: a carrier is in this array iff `carrier.dirty`.
536
+ this.dirtyCarriers = [];
538
537
  this.isFlushEnqueued = false;
539
- this.updatedKeys = /* @__PURE__ */ new Set();
540
- this.isAfterFlushDeliveryScheduled = false;
541
- // Stable handler reference to avoid allocating a new closure on every HAS_CHANGES.
542
538
  this.onFlush = () => {
543
539
  this.isFlushEnqueued = false;
544
- this.dequeueFlush = void 0;
545
- if (this.keyHandlers.size === 0) {
546
- this.updatedKeys.clear();
547
- return;
540
+ if (this.dirtyCarriers.length === 0) return;
541
+ const flushing = this.dirtyCarriers;
542
+ this.dirtyCarriers = [];
543
+ let i = 0;
544
+ try {
545
+ for (; i < flushing.length; i++) {
546
+ flushing[i].dirty = false;
547
+ this.notify(flushing[i]);
548
+ }
549
+ } finally {
550
+ for (; i < flushing.length; i++) flushing[i].dirty = false;
548
551
  }
549
- this.runSinglePass();
550
- };
551
- this.onAfterFlush = () => {
552
- if (!this.isAfterFlushDeliveryScheduled) return;
553
- this.isAfterFlushDeliveryScheduled = false;
554
- this.deliverImmediate();
555
552
  };
556
553
  this.queue = queue;
557
- this.deliveryMode = deliveryMode;
558
- if (this.deliveryMode === "after_flush") {
559
- this.unsubscribeAfterFlushDelivery = this.queue.emitter.on(
560
- 0 /* AFTER_FLUSH */,
561
- this.onAfterFlush
562
- );
563
- }
554
+ this.resolver = resolver;
564
555
  }
565
- /**
566
- * Mark keys as updated and schedule delivery.
567
- *
568
- * If there are no subscriptions, this is a no-op and does not allocate or retain keys.
569
- */
570
- markUpdatedKeys(keys) {
571
- if (keys.length === 0) return;
572
- if (this.deliveryMode === "queue" && this.queue.isInFlush) {
573
- throw new Error(
574
- "OIMUpdateEventEmitter: updates during queue.flush() are not allowed. Finish all writes before calling queue.flush()."
575
- );
556
+ subscribeOnKey(key, handler) {
557
+ const carrier = this.resolver.getOrReserveCarrier(key);
558
+ this.addHandler(carrier, handler);
559
+ return () => this.removeHandler(carrier, handler);
560
+ }
561
+ subscribeOnKeys(keys, handler) {
562
+ const carriers = [];
563
+ for (let i = 0; i < keys.length; i++) {
564
+ const carrier = this.resolver.getOrReserveCarrier(keys[i]);
565
+ this.addHandler(carrier, handler);
566
+ carriers.push(carrier);
576
567
  }
577
- if (this.keyHandlers.size === 0) return;
578
- let didAddAny = false;
568
+ return () => {
569
+ for (let i = 0; i < carriers.length; i++) {
570
+ this.removeHandler(carriers[i], handler);
571
+ }
572
+ };
573
+ }
574
+ unsubscribeFromKey(key, handler) {
575
+ const carrier = this.resolver.findCarrier(key);
576
+ if (carrier) this.removeHandler(carrier, handler);
577
+ }
578
+ unsubscribeFromKeys(keys, handler) {
579
579
  for (let i = 0; i < keys.length; i++) {
580
- const key = keys[i];
581
- const handlers = this.keyHandlers.get(key);
582
- if (!handlers || handlers.size === 0) continue;
583
- this.updatedKeys.add(key);
584
- didAddAny = true;
580
+ this.unsubscribeFromKey(keys[i], handler);
585
581
  }
586
- if (!didAddAny) return;
587
- if (this.deliveryMode === "immediate") {
588
- this.deliverImmediate();
589
- return;
582
+ }
583
+ addHandler(carrier, handler) {
584
+ let subscribers = carrier.subscribers;
585
+ if (!subscribers) {
586
+ subscribers = /* @__PURE__ */ new Set();
587
+ carrier.subscribers = subscribers;
590
588
  }
591
- if (this.deliveryMode === "after_flush") {
592
- this.isAfterFlushDeliveryScheduled = true;
593
- return;
589
+ subscribers.add(handler);
590
+ this.subscribedCarriers.add(carrier);
591
+ }
592
+ removeHandler(carrier, handler) {
593
+ const subscribers = carrier.subscribers;
594
+ if (!subscribers) return;
595
+ subscribers.delete(handler);
596
+ if (subscribers.size === 0) {
597
+ this.subscribedCarriers.delete(carrier);
598
+ this.resolver.onCarrierEmptied?.(carrier);
594
599
  }
595
- if (this.isFlushEnqueued) return;
596
- this.isFlushEnqueued = true;
597
- this.dequeueFlush = this.queue.enqueue(this.onFlush);
600
+ }
601
+ /** Fast path: the writer already holds the carrier. */
602
+ markUpdatedCarrier(carrier) {
603
+ this.assertNotInFlush();
604
+ const subscribers = carrier.subscribers;
605
+ if (!subscribers || subscribers.size === 0) return;
606
+ if (carrier.dirty) return;
607
+ carrier.dirty = true;
608
+ this.dirtyCarriers.push(carrier);
609
+ this.scheduleFlush();
598
610
  }
599
611
  markUpdatedKey(key) {
600
- if (this.deliveryMode === "queue" && this.queue.isInFlush) {
601
- throw new Error(
602
- "OIMUpdateEventEmitter: updates during queue.flush() are not allowed. Finish all writes before calling queue.flush()."
603
- );
604
- }
605
- if (this.keyHandlers.size === 0) return;
606
- const handlers = this.keyHandlers.get(key);
607
- if (!handlers || handlers.size === 0) return;
608
- this.updatedKeys.add(key);
609
- if (this.deliveryMode === "immediate") {
610
- this.deliverImmediate();
611
- return;
612
- }
613
- if (this.deliveryMode === "after_flush") {
614
- this.isAfterFlushDeliveryScheduled = true;
615
- return;
616
- }
617
- if (this.isFlushEnqueued) return;
618
- this.isFlushEnqueued = true;
619
- this.dequeueFlush = this.queue.enqueue(this.onFlush);
612
+ const carrier = this.resolver.findCarrier(key);
613
+ if (carrier) this.markUpdatedCarrier(carrier);
614
+ else this.assertNotInFlush();
615
+ }
616
+ markUpdatedKeys(keys) {
617
+ for (let i = 0; i < keys.length; i++) this.markUpdatedKey(keys[i]);
620
618
  }
621
619
  markAllUpdated() {
622
- if (this.deliveryMode === "queue" && this.queue.isInFlush) {
623
- throw new Error(
624
- "OIMUpdateEventEmitter: updates during queue.flush() are not allowed. Finish all writes before calling queue.flush()."
625
- );
626
- }
627
- if (this.keyHandlers.size === 0) return;
628
- this.keyHandlers.forEach((_handlers, key) => {
629
- this.updatedKeys.add(key);
620
+ this.assertNotInFlush();
621
+ if (this.subscribedCarriers.size === 0) return;
622
+ this.subscribedCarriers.forEach((carrier) => {
623
+ if (carrier.dirty) return;
624
+ carrier.dirty = true;
625
+ this.dirtyCarriers.push(carrier);
630
626
  });
631
- if (this.deliveryMode === "immediate") {
632
- this.deliverImmediate();
633
- return;
634
- }
635
- if (this.deliveryMode === "after_flush") {
636
- this.isAfterFlushDeliveryScheduled = true;
637
- return;
638
- }
639
- if (this.isFlushEnqueued) return;
640
- this.isFlushEnqueued = true;
641
- this.dequeueFlush = this.queue.enqueue(this.onFlush);
642
- }
643
- deliverImmediate() {
644
- this.runSinglePass();
645
- }
646
- /**
647
- * Notifies all handlers for a key. The overwhelmingly common case is a
648
- * single subscriber per key (one component per pk), handled without any
649
- * allocation. For multiple subscribers a snapshot is taken so a handler may
650
- * (un)subscribe during iteration without corrupting the walk.
651
- */
652
- notifyHandlers(handlers) {
653
- if (handlers.size === 1) {
654
- const only = handlers.values().next().value;
655
- if (only) only();
656
- return;
657
- }
658
- const snapshot = [];
659
- handlers.forEach((h) => snapshot.push(h));
660
- for (let i = 0; i < snapshot.length; i++) {
661
- const handler = snapshot[i];
662
- if (handlers.has(handler)) handler();
663
- }
627
+ this.scheduleFlush();
664
628
  }
665
- /**
666
- * Deliver updated keys and notify handlers once.
667
- * Re-entrant updates during delivery are forbidden and will throw in markUpdatedKeys().
668
- */
669
- runSinglePass() {
670
- if (this.updatedKeys.size === 0) return;
671
- const flushingKeys = this.updatedKeys;
672
- this.updatedKeys = /* @__PURE__ */ new Set();
673
- if (this.keyHandlers.size === 0 || flushingKeys.size === 0) return;
674
- if (flushingKeys.size * 2 < this.keyHandlers.size) {
675
- flushingKeys.forEach((key) => {
676
- const handlers = this.keyHandlers.get(key);
677
- if (!handlers || handlers.size === 0) return;
678
- this.notifyHandlers(handlers);
679
- });
680
- } else {
681
- this.keyHandlers.forEach((handlers, key) => {
682
- if (!flushingKeys.has(key)) return;
683
- if (!handlers || handlers.size === 0) return;
684
- this.notifyHandlers(handlers);
685
- });
686
- }
629
+ hasSubscriptions() {
630
+ return this.subscribedCarriers.size > 0;
687
631
  }
688
- destroy() {
689
- if (this.dequeueFlush) {
690
- this.dequeueFlush();
691
- this.dequeueFlush = void 0;
692
- }
693
- if (this.unsubscribeAfterFlushDelivery) {
694
- this.unsubscribeAfterFlushDelivery();
695
- this.unsubscribeAfterFlushDelivery = void 0;
696
- }
697
- this.keyHandlers.forEach((handlers) => handlers.clear());
698
- this.keyHandlers.clear();
699
- this.updatedKeys.clear();
632
+ getHandlerCount(key) {
633
+ return this.resolver.findCarrier(key)?.subscribers?.size ?? 0;
700
634
  }
701
- /**
702
- * Get performance metrics for monitoring and debugging
703
- */
704
635
  getMetrics() {
705
636
  let totalHandlers = 0;
706
- this.keyHandlers.forEach((handlers) => {
707
- totalHandlers += handlers.size;
637
+ this.subscribedCarriers.forEach((carrier) => {
638
+ totalHandlers += carrier.subscribers?.size ?? 0;
708
639
  });
640
+ const totalKeys = this.subscribedCarriers.size;
709
641
  return {
710
- totalKeys: this.keyHandlers.size,
642
+ totalKeys,
711
643
  totalHandlers,
712
- averageHandlersPerKey: this.keyHandlers.size > 0 ? totalHandlers / this.keyHandlers.size : 0,
713
- queueLength: this.queue.length
644
+ averageHandlersPerKey: totalKeys > 0 ? totalHandlers / totalKeys : 0,
645
+ queueLength: this.dirtyCarriers.length
714
646
  };
715
647
  }
716
- /**
717
- * Check if there are any active subscriptions
718
- */
719
- hasSubscriptions() {
720
- return this.keyHandlers.size > 0;
721
- }
722
- /**
723
- * Get the number of handlers for a specific key
724
- */
725
- getHandlerCount(key) {
726
- return this.keyHandlers.get(key)?.size ?? 0;
648
+ destroy() {
649
+ if (this.isFlushEnqueued) {
650
+ this.queue.cancelTask(this.onFlush);
651
+ this.isFlushEnqueued = false;
652
+ }
653
+ this.subscribedCarriers.forEach((carrier) => {
654
+ carrier.subscribers?.clear();
655
+ carrier.subscribers = void 0;
656
+ });
657
+ this.subscribedCarriers.clear();
658
+ for (let i = 0; i < this.dirtyCarriers.length; i++) {
659
+ this.dirtyCarriers[i].dirty = false;
660
+ }
661
+ this.dirtyCarriers = [];
662
+ this.isFlushEnqueued = false;
727
663
  }
728
- subscribeOnKey(key, handler) {
729
- let handlers = this.keyHandlers.get(key);
730
- if (!handlers) {
731
- handlers = /* @__PURE__ */ new Set();
732
- this.keyHandlers.set(key, handlers);
664
+ assertNotInFlush() {
665
+ if (this.queue.isInFlush) {
666
+ throw new Error(
667
+ "OIMCarrierKeyedEmitter: updates during queue.flush() are not allowed. Finish all writes before calling queue.flush()."
668
+ );
733
669
  }
734
- handlers.add(handler);
735
- return () => {
736
- this.unsubscribeFromKey(key, handler);
737
- };
738
670
  }
739
- unsubscribeFromKey(key, handler) {
740
- const handlers = this.keyHandlers.get(key);
741
- if (!handlers) return;
742
- handlers.delete(handler);
743
- if (handlers.size === 0) {
744
- this.keyHandlers.delete(key);
745
- this.updatedKeys.delete(key);
671
+ scheduleFlush() {
672
+ if (this.isFlushEnqueued) return;
673
+ this.isFlushEnqueued = true;
674
+ this.queue.enqueueTask(this.onFlush);
675
+ }
676
+ notify(carrier) {
677
+ const subscribers = carrier.subscribers;
678
+ if (!subscribers || subscribers.size === 0) return;
679
+ if (subscribers.size === 1) {
680
+ const only = subscribers.values().next().value;
681
+ if (only) only();
682
+ return;
746
683
  }
747
- if (this.keyHandlers.size === 0) {
748
- this.updatedKeys.clear();
749
- if (this.dequeueFlush) {
750
- this.dequeueFlush();
751
- this.dequeueFlush = void 0;
752
- }
753
- this.isFlushEnqueued = false;
684
+ const snapshot = [];
685
+ subscribers.forEach((h) => snapshot.push(h));
686
+ for (let i = 0; i < snapshot.length; i++) {
687
+ const handler = snapshot[i];
688
+ if (subscribers.has(handler)) handler();
754
689
  }
755
690
  }
756
- subscribeOnKeys(keys, handler) {
757
- const subscribedKeys = [];
758
- for (const key of keys) {
759
- let handlers = this.keyHandlers.get(key);
760
- if (!handlers) {
761
- handlers = /* @__PURE__ */ new Set();
762
- this.keyHandlers.set(key, handlers);
763
- }
764
- if (!handlers.has(handler)) {
765
- handlers.add(handler);
766
- subscribedKeys.push(key);
767
- }
768
- }
769
- return () => {
770
- for (const key of subscribedKeys) {
771
- this.unsubscribeFromKey(key, handler);
772
- }
773
- };
691
+ };
692
+
693
+ // src/core/OIMKeyedCarrierResolver.ts
694
+ var OIMKeyedCarrierResolver = class {
695
+ constructor() {
696
+ this.carriers = /* @__PURE__ */ new Map();
774
697
  }
775
- unsubscribeFromKeys(keys, handler) {
776
- for (const key of keys) {
777
- this.unsubscribeFromKey(key, handler);
698
+ getOrReserveCarrier(key) {
699
+ let carrier = this.carriers.get(key);
700
+ if (!carrier) {
701
+ carrier = { key };
702
+ this.carriers.set(key, carrier);
778
703
  }
704
+ return carrier;
705
+ }
706
+ findCarrier(key) {
707
+ return this.carriers.get(key);
708
+ }
709
+ onCarrierEmptied(carrier) {
710
+ this.carriers.delete(carrier.key);
779
711
  }
780
712
  };
781
713
 
782
714
  // src/abstract/OIMReactiveIndex.ts
783
715
  var OIMReactiveIndex = class {
784
716
  constructor(queue, createIndex) {
785
- this.updateEmitter = new OIMUpdateEventEmitter(queue);
717
+ this.updateEmitter = new OIMCarrierKeyedEmitter(queue, new OIMKeyedCarrierResolver());
786
718
  this.index = createIndex(this.updateEmitter);
787
719
  }
788
720
  getEntitiesByKey(key) {
@@ -1113,192 +1045,45 @@ var OIMCollection = class {
1113
1045
  removeManyByPks(pks) {
1114
1046
  if (pks.length === 0) return;
1115
1047
  this.store.removeManyByPks(pks);
1116
- this.emitter.emit(0 /* UPDATE */, { pks });
1117
- }
1118
- clear() {
1119
- this.store.clear();
1120
- this.emitter.emit(0 /* UPDATE */, { pks: [] });
1121
- }
1122
- countAll() {
1123
- return this.store.countAll();
1124
- }
1125
- getAll() {
1126
- return this.store.getAll();
1127
- }
1128
- getAllPks() {
1129
- return this.store.getAllPks();
1130
- }
1131
- upsertOneWithoutNotifications(entity) {
1132
- const pk = this.selectPk(entity);
1133
- return this.upsertOneWithoutNotificationsByPk(pk, entity);
1134
- }
1135
- upsertOneWithoutNotificationsByPk(pk, entity) {
1136
- if (!pk) {
1137
- throw new Error(
1138
- `[OIMCollection]: PK is required to upsert an entity ${JSON.stringify(entity)}`
1139
- );
1140
- }
1141
- const existingEntity = this.store.getOneByPk(pk);
1142
- if (existingEntity) {
1143
- return this.store.setOneByPk(
1144
- pk,
1145
- this.updateEntity(entity, existingEntity)
1146
- );
1147
- } else {
1148
- return this.store.setOneByPk(pk, entity);
1149
- }
1150
- }
1151
- destroy() {
1152
- this.store.destroy();
1153
- this.emitter.offAll();
1154
- }
1155
- };
1156
-
1157
- // src/core/OIMCarrierKeyedEmitter.ts
1158
- var OIMCarrierKeyedEmitter = class {
1159
- constructor(queue, resolver) {
1160
- // Carriers with at least one subscriber — for markAllUpdated / metrics.
1161
- this.subscribedCarriers = /* @__PURE__ */ new Set();
1162
- this.dirtyCarriers = /* @__PURE__ */ new Set();
1163
- this.isFlushEnqueued = false;
1164
- this.onFlush = () => {
1165
- this.isFlushEnqueued = false;
1166
- this.dequeueFlush = void 0;
1167
- if (this.dirtyCarriers.size === 0) return;
1168
- const flushing = this.dirtyCarriers;
1169
- this.dirtyCarriers = /* @__PURE__ */ new Set();
1170
- flushing.forEach((carrier) => this.notify(carrier));
1171
- };
1172
- this.queue = queue;
1173
- this.resolver = resolver;
1174
- }
1175
- subscribeOnKey(key, handler) {
1176
- const carrier = this.resolver.getOrReserveCarrier(key);
1177
- this.addHandler(carrier, handler);
1178
- return () => this.removeHandler(carrier, handler);
1179
- }
1180
- subscribeOnKeys(keys, handler) {
1181
- const carriers = [];
1182
- for (let i = 0; i < keys.length; i++) {
1183
- const carrier = this.resolver.getOrReserveCarrier(keys[i]);
1184
- this.addHandler(carrier, handler);
1185
- carriers.push(carrier);
1186
- }
1187
- return () => {
1188
- for (let i = 0; i < carriers.length; i++) {
1189
- this.removeHandler(carriers[i], handler);
1190
- }
1191
- };
1192
- }
1193
- unsubscribeFromKey(key, handler) {
1194
- const carrier = this.resolver.findCarrier(key);
1195
- if (carrier) this.removeHandler(carrier, handler);
1196
- }
1197
- unsubscribeFromKeys(keys, handler) {
1198
- for (let i = 0; i < keys.length; i++) {
1199
- this.unsubscribeFromKey(keys[i], handler);
1200
- }
1201
- }
1202
- addHandler(carrier, handler) {
1203
- let subscribers = carrier.subscribers;
1204
- if (!subscribers) {
1205
- subscribers = /* @__PURE__ */ new Set();
1206
- carrier.subscribers = subscribers;
1207
- }
1208
- subscribers.add(handler);
1209
- this.subscribedCarriers.add(carrier);
1210
- }
1211
- removeHandler(carrier, handler) {
1212
- const subscribers = carrier.subscribers;
1213
- if (!subscribers) return;
1214
- subscribers.delete(handler);
1215
- if (subscribers.size === 0) {
1216
- this.subscribedCarriers.delete(carrier);
1217
- }
1218
- }
1219
- /** Fast path: the writer already holds the carrier. */
1220
- markUpdatedCarrier(carrier) {
1221
- this.assertNotInFlush();
1222
- const subscribers = carrier.subscribers;
1223
- if (!subscribers || subscribers.size === 0) return;
1224
- this.dirtyCarriers.add(carrier);
1225
- this.scheduleFlush();
1226
- }
1227
- markUpdatedKey(key) {
1228
- const carrier = this.resolver.findCarrier(key);
1229
- if (carrier) this.markUpdatedCarrier(carrier);
1230
- else this.assertNotInFlush();
1231
- }
1232
- markUpdatedKeys(keys) {
1233
- for (let i = 0; i < keys.length; i++) this.markUpdatedKey(keys[i]);
1234
- }
1235
- markAllUpdated() {
1236
- this.assertNotInFlush();
1237
- if (this.subscribedCarriers.size === 0) return;
1238
- this.subscribedCarriers.forEach(
1239
- (carrier) => this.dirtyCarriers.add(carrier)
1240
- );
1241
- this.scheduleFlush();
1048
+ this.emitter.emit(0 /* UPDATE */, { pks });
1242
1049
  }
1243
- hasSubscriptions() {
1244
- return this.subscribedCarriers.size > 0;
1050
+ clear() {
1051
+ this.store.clear();
1052
+ this.emitter.emit(0 /* UPDATE */, { pks: [] });
1245
1053
  }
1246
- getHandlerCount(key) {
1247
- return this.resolver.findCarrier(key)?.subscribers?.size ?? 0;
1054
+ countAll() {
1055
+ return this.store.countAll();
1248
1056
  }
1249
- getMetrics() {
1250
- let totalHandlers = 0;
1251
- this.subscribedCarriers.forEach((carrier) => {
1252
- totalHandlers += carrier.subscribers?.size ?? 0;
1253
- });
1254
- const totalKeys = this.subscribedCarriers.size;
1255
- return {
1256
- totalKeys,
1257
- totalHandlers,
1258
- averageHandlersPerKey: totalKeys > 0 ? totalHandlers / totalKeys : 0,
1259
- queueLength: this.dirtyCarriers.size
1260
- };
1057
+ getAll() {
1058
+ return this.store.getAll();
1261
1059
  }
1262
- destroy() {
1263
- if (this.dequeueFlush) {
1264
- this.dequeueFlush();
1265
- this.dequeueFlush = void 0;
1266
- }
1267
- this.subscribedCarriers.forEach((carrier) => {
1268
- carrier.subscribers?.clear();
1269
- carrier.subscribers = void 0;
1270
- });
1271
- this.subscribedCarriers.clear();
1272
- this.dirtyCarriers.clear();
1273
- this.isFlushEnqueued = false;
1060
+ getAllPks() {
1061
+ return this.store.getAllPks();
1274
1062
  }
1275
- assertNotInFlush() {
1276
- if (this.queue.isInFlush) {
1063
+ upsertOneWithoutNotifications(entity) {
1064
+ const pk = this.selectPk(entity);
1065
+ return this.upsertOneWithoutNotificationsByPk(pk, entity);
1066
+ }
1067
+ upsertOneWithoutNotificationsByPk(pk, entity) {
1068
+ if (!pk) {
1277
1069
  throw new Error(
1278
- "OIMCarrierKeyedEmitter: updates during queue.flush() are not allowed. Finish all writes before calling queue.flush()."
1070
+ `[OIMCollection]: PK is required to upsert an entity ${JSON.stringify(entity)}`
1279
1071
  );
1280
1072
  }
1281
- }
1282
- scheduleFlush() {
1283
- if (this.isFlushEnqueued) return;
1284
- this.isFlushEnqueued = true;
1285
- this.dequeueFlush = this.queue.enqueue(this.onFlush);
1286
- }
1287
- notify(carrier) {
1288
- const subscribers = carrier.subscribers;
1289
- if (!subscribers || subscribers.size === 0) return;
1290
- if (subscribers.size === 1) {
1291
- const only = subscribers.values().next().value;
1292
- if (only) only();
1293
- return;
1294
- }
1295
- const snapshot = [];
1296
- subscribers.forEach((h) => snapshot.push(h));
1297
- for (let i = 0; i < snapshot.length; i++) {
1298
- const handler = snapshot[i];
1299
- if (subscribers.has(handler)) handler();
1073
+ const existingEntity = this.store.getOneByPk(pk);
1074
+ if (existingEntity) {
1075
+ return this.store.setOneByPk(
1076
+ pk,
1077
+ this.updateEntity(entity, existingEntity)
1078
+ );
1079
+ } else {
1080
+ return this.store.setOneByPk(pk, entity);
1300
1081
  }
1301
1082
  }
1083
+ destroy() {
1084
+ this.store.destroy();
1085
+ this.emitter.offAll();
1086
+ }
1302
1087
  };
1303
1088
 
1304
1089
  // src/core/OIMReactiveCollection.ts
@@ -1312,7 +1097,6 @@ var OIMReactiveCollection = class extends OIMCollection {
1312
1097
  this.runAnyUpdate = () => {
1313
1098
  if (!this.isAnyUpdateScheduled) return;
1314
1099
  this.isAnyUpdateScheduled = false;
1315
- this.dequeueAnyUpdate = void 0;
1316
1100
  const pks = this.isAnyUpdateClearPending ? [] : Array.from(this.pendingAnyUpdatePks);
1317
1101
  this.pendingAnyUpdatePks.clear();
1318
1102
  this.isAnyUpdateClearPending = false;
@@ -1452,13 +1236,13 @@ var OIMReactiveCollection = class extends OIMCollection {
1452
1236
  ensureAnyUpdateEnqueued() {
1453
1237
  if (this.isAnyUpdateScheduled) return;
1454
1238
  this.isAnyUpdateScheduled = true;
1455
- this.dequeueAnyUpdate = this.queue.enqueue(this.runAnyUpdate);
1239
+ this.queue.enqueueTask(this.runAnyUpdate);
1456
1240
  }
1457
1241
  destroy() {
1458
1242
  this.updateEmitter.destroy();
1459
- if (this.dequeueAnyUpdate) {
1460
- this.dequeueAnyUpdate();
1461
- this.dequeueAnyUpdate = void 0;
1243
+ if (this.isAnyUpdateScheduled) {
1244
+ this.queue.cancelTask(this.runAnyUpdate);
1245
+ this.isAnyUpdateScheduled = false;
1462
1246
  }
1463
1247
  this.anyUpdateHandlers.clear();
1464
1248
  this.pendingAnyUpdatePks.clear();
@@ -1477,109 +1261,362 @@ var OIMIndexManualOrderedArrayBased = class extends OIMIndexArrayBased {
1477
1261
  }
1478
1262
  return;
1479
1263
  }
1480
- if (this.store.getOneByKey(key) !== void 0) {
1481
- this.store.removeOneByKey(key);
1482
- this.emitUpdate([key]);
1264
+ if (this.store.getOneByKey(key) !== void 0) {
1265
+ this.store.removeOneByKey(key);
1266
+ this.emitUpdate([key]);
1267
+ }
1268
+ }
1269
+ pushSlot(key, slot) {
1270
+ const list = this.getOrCreateList(key);
1271
+ const index = list.length;
1272
+ list.push(slot);
1273
+ this.emitUpdate([key]);
1274
+ return index;
1275
+ }
1276
+ insertSlotAt(key, index, slot) {
1277
+ const list = this.getOrCreateList(key);
1278
+ const safeIndex = Math.max(0, Math.min(index, list.length));
1279
+ list.splice(safeIndex, 0, slot);
1280
+ this.emitUpdate([key]);
1281
+ return safeIndex;
1282
+ }
1283
+ removeAt(key, index) {
1284
+ const list = this.store.getOneByKey(key);
1285
+ if (!list) return void 0;
1286
+ if (index < 0 || index >= list.length) return void 0;
1287
+ const [removed] = list.splice(index, 1);
1288
+ if (list.length === 0) {
1289
+ this.store.removeOneByKey(key);
1290
+ }
1291
+ this.emitUpdate([key]);
1292
+ return removed;
1293
+ }
1294
+ move(key, fromIndex, toIndex) {
1295
+ const list = this.store.getOneByKey(key);
1296
+ if (!list) return void 0;
1297
+ if (fromIndex < 0 || fromIndex >= list.length) return void 0;
1298
+ if (toIndex < 0 || toIndex >= list.length) return void 0;
1299
+ if (fromIndex === toIndex) return list[fromIndex];
1300
+ const [slot] = list.splice(fromIndex, 1);
1301
+ if (slot === void 0) return void 0;
1302
+ list.splice(toIndex, 0, slot);
1303
+ this.emitUpdate([key]);
1304
+ return slot;
1305
+ }
1306
+ resetSlots(key, slots) {
1307
+ if (slots.length === 0) {
1308
+ if (this.store.getOneByKey(key) !== void 0) {
1309
+ this.store.removeOneByKey(key);
1310
+ this.emitUpdate([key]);
1311
+ }
1312
+ return;
1313
+ }
1314
+ this.store.setOneByKey(key, slots.slice());
1315
+ this.emitUpdate([key]);
1316
+ }
1317
+ getOrCreateList(key) {
1318
+ let list = this.store.getOneByKey(key);
1319
+ if (!list) {
1320
+ list = [];
1321
+ this.store.setOneByKey(key, list);
1322
+ }
1323
+ return list;
1324
+ }
1325
+ };
1326
+
1327
+ // src/modules/wrapper/index/OIMCollectionIndexManualOrderedArrayBased.ts
1328
+ var OIMCollectionIndexManualOrderedArrayBased = class extends OIMIndexManualOrderedArrayBased {
1329
+ constructor(opts) {
1330
+ super();
1331
+ this.resolveSlot = opts.collection !== void 0 ? (pk) => opts.collection.getSlotByPk(pk) : opts.resolveSlot;
1332
+ }
1333
+ push(key, pk) {
1334
+ return this.pushSlot(key, this.resolveRequiredSlot(pk));
1335
+ }
1336
+ insertAt(key, index, pk) {
1337
+ return this.insertSlotAt(key, index, this.resolveRequiredSlot(pk));
1338
+ }
1339
+ reset(key, pks) {
1340
+ this.resetSlots(key, this.resolveSlots(pks));
1341
+ }
1342
+ getEntitiesByKey(key) {
1343
+ return super.getEntitiesByKey(key);
1344
+ }
1345
+ getEntitiesByKeys(keys) {
1346
+ return super.getEntitiesByKeys(keys);
1347
+ }
1348
+ resolvePk(pk) {
1349
+ return this.resolveRequiredSlot(pk);
1350
+ }
1351
+ resolveSlots(pks) {
1352
+ const slots = [];
1353
+ slots.length = pks.length;
1354
+ for (let i = 0; i < pks.length; i++) {
1355
+ slots[i] = this.resolveRequiredSlot(pks[i]);
1356
+ }
1357
+ return slots;
1358
+ }
1359
+ resolveRequiredSlot(pk) {
1360
+ const slot = this.resolveSlot(pk);
1361
+ if (!slot) {
1362
+ throw new Error(
1363
+ `[OIMCollectionIndexManualOrderedArrayBased]: Unable to resolve slot for PK "${String(pk)}".`
1364
+ );
1365
+ }
1366
+ return slot;
1367
+ }
1368
+ };
1369
+
1370
+ // src/enums/EOIMEventQueueEventType.ts
1371
+ var EOIMEventQueueEventType = /* @__PURE__ */ ((EOIMEventQueueEventType2) => {
1372
+ EOIMEventQueueEventType2[EOIMEventQueueEventType2["AFTER_FLUSH"] = 0] = "AFTER_FLUSH";
1373
+ EOIMEventQueueEventType2[EOIMEventQueueEventType2["BEFORE_FLUSH"] = 1] = "BEFORE_FLUSH";
1374
+ return EOIMEventQueueEventType2;
1375
+ })(EOIMEventQueueEventType || {});
1376
+
1377
+ // src/core/OIMUpdateEventEmitter.ts
1378
+ var OIMUpdateEventEmitter = class {
1379
+ constructor(queue, deliveryMode = "queue") {
1380
+ this.keyHandlers = /* @__PURE__ */ new Map();
1381
+ this.isFlushEnqueued = false;
1382
+ this.updatedKeys = /* @__PURE__ */ new Set();
1383
+ this.isAfterFlushDeliveryScheduled = false;
1384
+ // Stable handler reference to avoid allocating a new closure on every HAS_CHANGES.
1385
+ this.onFlush = () => {
1386
+ this.isFlushEnqueued = false;
1387
+ if (this.keyHandlers.size === 0) {
1388
+ this.updatedKeys.clear();
1389
+ return;
1390
+ }
1391
+ this.runSinglePass();
1392
+ };
1393
+ this.onAfterFlush = () => {
1394
+ if (!this.isAfterFlushDeliveryScheduled) return;
1395
+ this.isAfterFlushDeliveryScheduled = false;
1396
+ this.deliverImmediate();
1397
+ };
1398
+ this.queue = queue;
1399
+ this.deliveryMode = deliveryMode;
1400
+ if (this.deliveryMode === "after_flush") {
1401
+ this.unsubscribeAfterFlushDelivery = this.queue.emitter.on(
1402
+ 0 /* AFTER_FLUSH */,
1403
+ this.onAfterFlush
1404
+ );
1405
+ }
1406
+ }
1407
+ /**
1408
+ * Mark keys as updated and schedule delivery.
1409
+ *
1410
+ * If there are no subscriptions, this is a no-op and does not allocate or retain keys.
1411
+ */
1412
+ markUpdatedKeys(keys) {
1413
+ if (keys.length === 0) return;
1414
+ if (this.deliveryMode === "queue" && this.queue.isInFlush) {
1415
+ throw new Error(
1416
+ "OIMUpdateEventEmitter: updates during queue.flush() are not allowed. Finish all writes before calling queue.flush()."
1417
+ );
1418
+ }
1419
+ if (this.keyHandlers.size === 0) return;
1420
+ let didAddAny = false;
1421
+ for (let i = 0; i < keys.length; i++) {
1422
+ const key = keys[i];
1423
+ const handlers = this.keyHandlers.get(key);
1424
+ if (!handlers || handlers.size === 0) continue;
1425
+ this.updatedKeys.add(key);
1426
+ didAddAny = true;
1427
+ }
1428
+ if (!didAddAny) return;
1429
+ if (this.deliveryMode === "immediate") {
1430
+ this.deliverImmediate();
1431
+ return;
1432
+ }
1433
+ if (this.deliveryMode === "after_flush") {
1434
+ this.isAfterFlushDeliveryScheduled = true;
1435
+ return;
1436
+ }
1437
+ if (this.isFlushEnqueued) return;
1438
+ this.isFlushEnqueued = true;
1439
+ this.queue.enqueueTask(this.onFlush);
1440
+ }
1441
+ markUpdatedKey(key) {
1442
+ if (this.deliveryMode === "queue" && this.queue.isInFlush) {
1443
+ throw new Error(
1444
+ "OIMUpdateEventEmitter: updates during queue.flush() are not allowed. Finish all writes before calling queue.flush()."
1445
+ );
1446
+ }
1447
+ if (this.keyHandlers.size === 0) return;
1448
+ const handlers = this.keyHandlers.get(key);
1449
+ if (!handlers || handlers.size === 0) return;
1450
+ this.updatedKeys.add(key);
1451
+ if (this.deliveryMode === "immediate") {
1452
+ this.deliverImmediate();
1453
+ return;
1454
+ }
1455
+ if (this.deliveryMode === "after_flush") {
1456
+ this.isAfterFlushDeliveryScheduled = true;
1457
+ return;
1483
1458
  }
1459
+ if (this.isFlushEnqueued) return;
1460
+ this.isFlushEnqueued = true;
1461
+ this.queue.enqueueTask(this.onFlush);
1484
1462
  }
1485
- pushSlot(key, slot) {
1486
- const list = this.getOrCreateList(key);
1487
- const index = list.length;
1488
- list.push(slot);
1489
- this.emitUpdate([key]);
1490
- return index;
1491
- }
1492
- insertSlotAt(key, index, slot) {
1493
- const list = this.getOrCreateList(key);
1494
- const safeIndex = Math.max(0, Math.min(index, list.length));
1495
- list.splice(safeIndex, 0, slot);
1496
- this.emitUpdate([key]);
1497
- return safeIndex;
1498
- }
1499
- removeAt(key, index) {
1500
- const list = this.store.getOneByKey(key);
1501
- if (!list) return void 0;
1502
- if (index < 0 || index >= list.length) return void 0;
1503
- const [removed] = list.splice(index, 1);
1504
- if (list.length === 0) {
1505
- this.store.removeOneByKey(key);
1463
+ markAllUpdated() {
1464
+ if (this.deliveryMode === "queue" && this.queue.isInFlush) {
1465
+ throw new Error(
1466
+ "OIMUpdateEventEmitter: updates during queue.flush() are not allowed. Finish all writes before calling queue.flush()."
1467
+ );
1506
1468
  }
1507
- this.emitUpdate([key]);
1508
- return removed;
1469
+ if (this.keyHandlers.size === 0) return;
1470
+ this.keyHandlers.forEach((_handlers, key) => {
1471
+ this.updatedKeys.add(key);
1472
+ });
1473
+ if (this.deliveryMode === "immediate") {
1474
+ this.deliverImmediate();
1475
+ return;
1476
+ }
1477
+ if (this.deliveryMode === "after_flush") {
1478
+ this.isAfterFlushDeliveryScheduled = true;
1479
+ return;
1480
+ }
1481
+ if (this.isFlushEnqueued) return;
1482
+ this.isFlushEnqueued = true;
1483
+ this.queue.enqueueTask(this.onFlush);
1509
1484
  }
1510
- move(key, fromIndex, toIndex) {
1511
- const list = this.store.getOneByKey(key);
1512
- if (!list) return void 0;
1513
- if (fromIndex < 0 || fromIndex >= list.length) return void 0;
1514
- if (toIndex < 0 || toIndex >= list.length) return void 0;
1515
- if (fromIndex === toIndex) return list[fromIndex];
1516
- const [slot] = list.splice(fromIndex, 1);
1517
- if (slot === void 0) return void 0;
1518
- list.splice(toIndex, 0, slot);
1519
- this.emitUpdate([key]);
1520
- return slot;
1485
+ deliverImmediate() {
1486
+ this.runSinglePass();
1521
1487
  }
1522
- resetSlots(key, slots) {
1523
- if (slots.length === 0) {
1524
- if (this.store.getOneByKey(key) !== void 0) {
1525
- this.store.removeOneByKey(key);
1526
- this.emitUpdate([key]);
1527
- }
1488
+ /**
1489
+ * Notifies all handlers for a key. The overwhelmingly common case is a
1490
+ * single subscriber per key (one component per pk), handled without any
1491
+ * allocation. For multiple subscribers a snapshot is taken so a handler may
1492
+ * (un)subscribe during iteration without corrupting the walk.
1493
+ */
1494
+ notifyHandlers(handlers) {
1495
+ if (handlers.size === 1) {
1496
+ const only = handlers.values().next().value;
1497
+ if (only) only();
1528
1498
  return;
1529
1499
  }
1530
- this.store.setOneByKey(key, slots.slice());
1531
- this.emitUpdate([key]);
1532
- }
1533
- getOrCreateList(key) {
1534
- let list = this.store.getOneByKey(key);
1535
- if (!list) {
1536
- list = [];
1537
- this.store.setOneByKey(key, list);
1500
+ const snapshot = [];
1501
+ handlers.forEach((h) => snapshot.push(h));
1502
+ for (let i = 0; i < snapshot.length; i++) {
1503
+ const handler = snapshot[i];
1504
+ if (handlers.has(handler)) handler();
1538
1505
  }
1539
- return list;
1540
1506
  }
1541
- };
1542
-
1543
- // src/modules/wrapper/index/OIMCollectionIndexManualOrderedArrayBased.ts
1544
- var OIMCollectionIndexManualOrderedArrayBased = class extends OIMIndexManualOrderedArrayBased {
1545
- constructor(opts) {
1546
- super();
1547
- this.resolveSlot = opts.collection !== void 0 ? (pk) => opts.collection.getSlotByPk(pk) : opts.resolveSlot;
1507
+ /**
1508
+ * Deliver updated keys and notify handlers once.
1509
+ * Re-entrant updates during delivery are forbidden and will throw in markUpdatedKeys().
1510
+ */
1511
+ runSinglePass() {
1512
+ if (this.updatedKeys.size === 0) return;
1513
+ const flushingKeys = this.updatedKeys;
1514
+ this.updatedKeys = /* @__PURE__ */ new Set();
1515
+ if (this.keyHandlers.size === 0 || flushingKeys.size === 0) return;
1516
+ if (flushingKeys.size * 2 < this.keyHandlers.size) {
1517
+ flushingKeys.forEach((key) => {
1518
+ const handlers = this.keyHandlers.get(key);
1519
+ if (!handlers || handlers.size === 0) return;
1520
+ this.notifyHandlers(handlers);
1521
+ });
1522
+ } else {
1523
+ this.keyHandlers.forEach((handlers, key) => {
1524
+ if (!flushingKeys.has(key)) return;
1525
+ if (!handlers || handlers.size === 0) return;
1526
+ this.notifyHandlers(handlers);
1527
+ });
1528
+ }
1548
1529
  }
1549
- push(key, pk) {
1550
- return this.pushSlot(key, this.resolveRequiredSlot(pk));
1530
+ destroy() {
1531
+ if (this.isFlushEnqueued) {
1532
+ this.queue.cancelTask(this.onFlush);
1533
+ this.isFlushEnqueued = false;
1534
+ }
1535
+ if (this.unsubscribeAfterFlushDelivery) {
1536
+ this.unsubscribeAfterFlushDelivery();
1537
+ this.unsubscribeAfterFlushDelivery = void 0;
1538
+ }
1539
+ this.keyHandlers.forEach((handlers) => handlers.clear());
1540
+ this.keyHandlers.clear();
1541
+ this.updatedKeys.clear();
1551
1542
  }
1552
- insertAt(key, index, pk) {
1553
- return this.insertSlotAt(key, index, this.resolveRequiredSlot(pk));
1543
+ /**
1544
+ * Get performance metrics for monitoring and debugging
1545
+ */
1546
+ getMetrics() {
1547
+ let totalHandlers = 0;
1548
+ this.keyHandlers.forEach((handlers) => {
1549
+ totalHandlers += handlers.size;
1550
+ });
1551
+ return {
1552
+ totalKeys: this.keyHandlers.size,
1553
+ totalHandlers,
1554
+ averageHandlersPerKey: this.keyHandlers.size > 0 ? totalHandlers / this.keyHandlers.size : 0,
1555
+ queueLength: this.queue.length
1556
+ };
1554
1557
  }
1555
- reset(key, pks) {
1556
- this.resetSlots(key, this.resolveSlots(pks));
1558
+ /**
1559
+ * Check if there are any active subscriptions
1560
+ */
1561
+ hasSubscriptions() {
1562
+ return this.keyHandlers.size > 0;
1557
1563
  }
1558
- getEntitiesByKey(key) {
1559
- return super.getEntitiesByKey(key);
1564
+ /**
1565
+ * Get the number of handlers for a specific key
1566
+ */
1567
+ getHandlerCount(key) {
1568
+ return this.keyHandlers.get(key)?.size ?? 0;
1560
1569
  }
1561
- getEntitiesByKeys(keys) {
1562
- return super.getEntitiesByKeys(keys);
1570
+ subscribeOnKey(key, handler) {
1571
+ let handlers = this.keyHandlers.get(key);
1572
+ if (!handlers) {
1573
+ handlers = /* @__PURE__ */ new Set();
1574
+ this.keyHandlers.set(key, handlers);
1575
+ }
1576
+ handlers.add(handler);
1577
+ return () => {
1578
+ this.unsubscribeFromKey(key, handler);
1579
+ };
1563
1580
  }
1564
- resolvePk(pk) {
1565
- return this.resolveRequiredSlot(pk);
1581
+ unsubscribeFromKey(key, handler) {
1582
+ const handlers = this.keyHandlers.get(key);
1583
+ if (!handlers) return;
1584
+ handlers.delete(handler);
1585
+ if (handlers.size === 0) {
1586
+ this.keyHandlers.delete(key);
1587
+ this.updatedKeys.delete(key);
1588
+ }
1589
+ if (this.keyHandlers.size === 0) {
1590
+ this.updatedKeys.clear();
1591
+ if (this.isFlushEnqueued) {
1592
+ this.queue.cancelTask(this.onFlush);
1593
+ }
1594
+ this.isFlushEnqueued = false;
1595
+ }
1566
1596
  }
1567
- resolveSlots(pks) {
1568
- const slots = [];
1569
- slots.length = pks.length;
1570
- for (let i = 0; i < pks.length; i++) {
1571
- slots[i] = this.resolveRequiredSlot(pks[i]);
1597
+ subscribeOnKeys(keys, handler) {
1598
+ const subscribedKeys = [];
1599
+ for (const key of keys) {
1600
+ let handlers = this.keyHandlers.get(key);
1601
+ if (!handlers) {
1602
+ handlers = /* @__PURE__ */ new Set();
1603
+ this.keyHandlers.set(key, handlers);
1604
+ }
1605
+ if (!handlers.has(handler)) {
1606
+ handlers.add(handler);
1607
+ subscribedKeys.push(key);
1608
+ }
1572
1609
  }
1573
- return slots;
1610
+ return () => {
1611
+ for (const key of subscribedKeys) {
1612
+ this.unsubscribeFromKey(key, handler);
1613
+ }
1614
+ };
1574
1615
  }
1575
- resolveRequiredSlot(pk) {
1576
- const slot = this.resolveSlot(pk);
1577
- if (!slot) {
1578
- throw new Error(
1579
- `[OIMCollectionIndexManualOrderedArrayBased]: Unable to resolve slot for PK "${String(pk)}".`
1580
- );
1616
+ unsubscribeFromKeys(keys, handler) {
1617
+ for (const key of keys) {
1618
+ this.unsubscribeFromKey(key, handler);
1581
1619
  }
1582
- return slot;
1583
1620
  }
1584
1621
  };
1585
1622
 
@@ -2510,17 +2547,15 @@ var OIMObject = class {
2510
2547
  }
2511
2548
  setProperty(key, value) {
2512
2549
  this.store.setProperty(key, value);
2513
- this.emitter.emit(0 /* UPDATE */, { keys: [key] });
2550
+ this.onUpdatedKey(key);
2514
2551
  }
2515
2552
  merge(draft) {
2516
2553
  this.store.merge(draft);
2517
- this.emitter.emit(0 /* UPDATE */, {
2518
- keys: Object.keys(draft)
2519
- });
2554
+ this.onUpdatedKeys(Object.keys(draft));
2520
2555
  }
2521
2556
  delete(key) {
2522
2557
  this.store.delete(key);
2523
- this.emitter.emit(0 /* UPDATE */, { keys: [key] });
2558
+ this.onUpdatedKey(key);
2524
2559
  }
2525
2560
  get(key) {
2526
2561
  return this.store.get(key);
@@ -2530,7 +2565,24 @@ var OIMObject = class {
2530
2565
  }
2531
2566
  clear() {
2532
2567
  this.store.clear();
2533
- this.emitter.emit(0 /* UPDATE */, { keys: [] });
2568
+ this.onUpdatedKeys([]);
2569
+ }
2570
+ /**
2571
+ * Write fan-out hooks. Split single-key vs multi-key so a reactive subclass
2572
+ * can mark its keyed emitter on the hot single-property path without
2573
+ * allocating a `{ keys: [key] }` payload. The base only fires the general
2574
+ * UPDATE event, and only when something is actually subscribed to it — so an
2575
+ * unobserved write skips the payload allocation entirely.
2576
+ */
2577
+ onUpdatedKey(key) {
2578
+ if (this.emitter.hasHandlers(0 /* UPDATE */)) {
2579
+ this.emitter.emit(0 /* UPDATE */, { keys: [key] });
2580
+ }
2581
+ }
2582
+ onUpdatedKeys(keys) {
2583
+ if (this.emitter.hasHandlers(0 /* UPDATE */)) {
2584
+ this.emitter.emit(0 /* UPDATE */, { keys });
2585
+ }
2534
2586
  }
2535
2587
  count() {
2536
2588
  return this.store.count();
@@ -2554,10 +2606,20 @@ var OIMObject = class {
2554
2606
  var OIMReactiveObject = class extends OIMObject {
2555
2607
  constructor(queue, opts) {
2556
2608
  super(opts);
2557
- this.updateEmitter = new OIMUpdateEventEmitter(queue, "queue");
2558
- this.emitter.on(0 /* UPDATE */, (payload) => {
2559
- this.updateEmitter.markUpdatedKeys(payload.keys);
2560
- });
2609
+ this.updateEmitter = new OIMCarrierKeyedEmitter(queue, new OIMKeyedCarrierResolver());
2610
+ }
2611
+ /**
2612
+ * Mark the written key(s) on the keyed emitter directly — no general-event
2613
+ * bridge, no `{ keys }` payload on the single-key path. `super` still fires
2614
+ * the general UPDATE event for any low-level `emitter` subscribers.
2615
+ */
2616
+ onUpdatedKey(key) {
2617
+ this.updateEmitter.markUpdatedKey(key);
2618
+ super.onUpdatedKey(key);
2619
+ }
2620
+ onUpdatedKeys(keys) {
2621
+ this.updateEmitter.markUpdatedKeys(keys);
2622
+ super.onUpdatedKeys(keys);
2561
2623
  }
2562
2624
  subscribeOnKey(key, handler) {
2563
2625
  return this.updateEmitter.subscribeOnKey(key, handler);
@@ -2661,6 +2723,9 @@ var OIMEventQueue = class {
2661
2723
  constructor(options = {}) {
2662
2724
  this.emitter = new OIMEventEmitter();
2663
2725
  this.tasks = /* @__PURE__ */ new Set();
2726
+ // Reused buffer swapped in during flush so the pending set is never copied
2727
+ // (no per-flush array snapshot) and new enqueues land in a fresh set.
2728
+ this.tasksSpare = /* @__PURE__ */ new Set();
2664
2729
  this.isFlushing = false;
2665
2730
  this.scheduler = options.scheduler;
2666
2731
  if (this.scheduler) {
@@ -2689,6 +2754,22 @@ var OIMEventQueue = class {
2689
2754
  this.tasks.delete(task);
2690
2755
  };
2691
2756
  }
2757
+ /**
2758
+ * Allocation-free enqueue for a **stable** function reference (e.g. a bound
2759
+ * `onFlush` method). The function is stored directly — no wrapper, no
2760
+ * returned closure — and cancelled via {@link cancelTask}. Enqueuing the
2761
+ * same reference twice is idempotent (Set-deduped); callers that may run a
2762
+ * task more than once per flush should use {@link enqueue} instead.
2763
+ */
2764
+ enqueueTask(fn) {
2765
+ this.tasks.add(fn);
2766
+ this.ensureScheduled();
2767
+ }
2768
+ /** Cancel a task scheduled via {@link enqueueTask}; safe to call mid-flush. */
2769
+ cancelTask(fn) {
2770
+ this.tasks.delete(fn);
2771
+ this.flushing?.delete(fn);
2772
+ }
2692
2773
  get isInFlush() {
2693
2774
  return this.isFlushing;
2694
2775
  }
@@ -2708,11 +2789,13 @@ var OIMEventQueue = class {
2708
2789
  this.scheduler?.cancel();
2709
2790
  this.isFlushing = true;
2710
2791
  this.emitter.emit(1 /* BEFORE_FLUSH */, void 0);
2711
- const snapshot = Array.from(this.tasks);
2712
- this.tasks.clear();
2713
- for (let i = 0; i < snapshot.length; i++) {
2714
- snapshot[i]();
2715
- }
2792
+ const flushing = this.tasks;
2793
+ this.tasks = this.tasksSpare;
2794
+ this.flushing = flushing;
2795
+ for (const task of flushing) task();
2796
+ flushing.clear();
2797
+ this.tasksSpare = flushing;
2798
+ this.flushing = void 0;
2716
2799
  this.isFlushing = false;
2717
2800
  this.emitter.emit(0 /* AFTER_FLUSH */, void 0);
2718
2801
  }