@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.js CHANGED
@@ -422,265 +422,197 @@ var OIMIndexArrayBased = class extends OIMIndex {
422
422
  }
423
423
  };
424
424
 
425
- // src/enums/EOIMEventQueueEventType.ts
426
- var EOIMEventQueueEventType = /* @__PURE__ */ ((EOIMEventQueueEventType2) => {
427
- EOIMEventQueueEventType2[EOIMEventQueueEventType2["AFTER_FLUSH"] = 0] = "AFTER_FLUSH";
428
- EOIMEventQueueEventType2[EOIMEventQueueEventType2["BEFORE_FLUSH"] = 1] = "BEFORE_FLUSH";
429
- return EOIMEventQueueEventType2;
430
- })(EOIMEventQueueEventType || {});
431
-
432
- // src/core/OIMUpdateEventEmitter.ts
433
- var OIMUpdateEventEmitter = class {
434
- constructor(queue, deliveryMode = "queue") {
435
- this.keyHandlers = /* @__PURE__ */ new Map();
425
+ // src/core/OIMCarrierKeyedEmitter.ts
426
+ var OIMCarrierKeyedEmitter = class {
427
+ constructor(queue, resolver) {
428
+ // Carriers with at least one subscriber — for markAllUpdated / metrics.
429
+ this.subscribedCarriers = /* @__PURE__ */ new Set();
430
+ // Pending flush batch. A plain array + a `carrier.dirty` flag for dedup —
431
+ // marking is the hottest write-path op, and pushing to an array with a
432
+ // boolean check is ~4x cheaper than a `Set` identity-hash add. The flag
433
+ // doubles as membership: a carrier is in this array iff `carrier.dirty`.
434
+ this.dirtyCarriers = [];
436
435
  this.isFlushEnqueued = false;
437
- this.updatedKeys = /* @__PURE__ */ new Set();
438
- this.isAfterFlushDeliveryScheduled = false;
439
- // Stable handler reference to avoid allocating a new closure on every HAS_CHANGES.
440
436
  this.onFlush = () => {
441
437
  this.isFlushEnqueued = false;
442
- this.dequeueFlush = void 0;
443
- if (this.keyHandlers.size === 0) {
444
- this.updatedKeys.clear();
445
- return;
438
+ if (this.dirtyCarriers.length === 0) return;
439
+ const flushing = this.dirtyCarriers;
440
+ this.dirtyCarriers = [];
441
+ let i = 0;
442
+ try {
443
+ for (; i < flushing.length; i++) {
444
+ flushing[i].dirty = false;
445
+ this.notify(flushing[i]);
446
+ }
447
+ } finally {
448
+ for (; i < flushing.length; i++) flushing[i].dirty = false;
446
449
  }
447
- this.runSinglePass();
448
- };
449
- this.onAfterFlush = () => {
450
- if (!this.isAfterFlushDeliveryScheduled) return;
451
- this.isAfterFlushDeliveryScheduled = false;
452
- this.deliverImmediate();
453
450
  };
454
451
  this.queue = queue;
455
- this.deliveryMode = deliveryMode;
456
- if (this.deliveryMode === "after_flush") {
457
- this.unsubscribeAfterFlushDelivery = this.queue.emitter.on(
458
- 0 /* AFTER_FLUSH */,
459
- this.onAfterFlush
460
- );
461
- }
452
+ this.resolver = resolver;
462
453
  }
463
- /**
464
- * Mark keys as updated and schedule delivery.
465
- *
466
- * If there are no subscriptions, this is a no-op and does not allocate or retain keys.
467
- */
468
- markUpdatedKeys(keys) {
469
- if (keys.length === 0) return;
470
- if (this.deliveryMode === "queue" && this.queue.isInFlush) {
471
- throw new Error(
472
- "OIMUpdateEventEmitter: updates during queue.flush() are not allowed. Finish all writes before calling queue.flush()."
473
- );
454
+ subscribeOnKey(key, handler) {
455
+ const carrier = this.resolver.getOrReserveCarrier(key);
456
+ this.addHandler(carrier, handler);
457
+ return () => this.removeHandler(carrier, handler);
458
+ }
459
+ subscribeOnKeys(keys, handler) {
460
+ const carriers = [];
461
+ for (let i = 0; i < keys.length; i++) {
462
+ const carrier = this.resolver.getOrReserveCarrier(keys[i]);
463
+ this.addHandler(carrier, handler);
464
+ carriers.push(carrier);
474
465
  }
475
- if (this.keyHandlers.size === 0) return;
476
- let didAddAny = false;
466
+ return () => {
467
+ for (let i = 0; i < carriers.length; i++) {
468
+ this.removeHandler(carriers[i], handler);
469
+ }
470
+ };
471
+ }
472
+ unsubscribeFromKey(key, handler) {
473
+ const carrier = this.resolver.findCarrier(key);
474
+ if (carrier) this.removeHandler(carrier, handler);
475
+ }
476
+ unsubscribeFromKeys(keys, handler) {
477
477
  for (let i = 0; i < keys.length; i++) {
478
- const key = keys[i];
479
- const handlers = this.keyHandlers.get(key);
480
- if (!handlers || handlers.size === 0) continue;
481
- this.updatedKeys.add(key);
482
- didAddAny = true;
478
+ this.unsubscribeFromKey(keys[i], handler);
483
479
  }
484
- if (!didAddAny) return;
485
- if (this.deliveryMode === "immediate") {
486
- this.deliverImmediate();
487
- return;
480
+ }
481
+ addHandler(carrier, handler) {
482
+ let subscribers = carrier.subscribers;
483
+ if (!subscribers) {
484
+ subscribers = /* @__PURE__ */ new Set();
485
+ carrier.subscribers = subscribers;
488
486
  }
489
- if (this.deliveryMode === "after_flush") {
490
- this.isAfterFlushDeliveryScheduled = true;
491
- return;
487
+ subscribers.add(handler);
488
+ this.subscribedCarriers.add(carrier);
489
+ }
490
+ removeHandler(carrier, handler) {
491
+ const subscribers = carrier.subscribers;
492
+ if (!subscribers) return;
493
+ subscribers.delete(handler);
494
+ if (subscribers.size === 0) {
495
+ this.subscribedCarriers.delete(carrier);
496
+ this.resolver.onCarrierEmptied?.(carrier);
492
497
  }
493
- if (this.isFlushEnqueued) return;
494
- this.isFlushEnqueued = true;
495
- this.dequeueFlush = this.queue.enqueue(this.onFlush);
498
+ }
499
+ /** Fast path: the writer already holds the carrier. */
500
+ markUpdatedCarrier(carrier) {
501
+ this.assertNotInFlush();
502
+ const subscribers = carrier.subscribers;
503
+ if (!subscribers || subscribers.size === 0) return;
504
+ if (carrier.dirty) return;
505
+ carrier.dirty = true;
506
+ this.dirtyCarriers.push(carrier);
507
+ this.scheduleFlush();
496
508
  }
497
509
  markUpdatedKey(key) {
498
- if (this.deliveryMode === "queue" && this.queue.isInFlush) {
499
- throw new Error(
500
- "OIMUpdateEventEmitter: updates during queue.flush() are not allowed. Finish all writes before calling queue.flush()."
501
- );
502
- }
503
- if (this.keyHandlers.size === 0) return;
504
- const handlers = this.keyHandlers.get(key);
505
- if (!handlers || handlers.size === 0) return;
506
- this.updatedKeys.add(key);
507
- if (this.deliveryMode === "immediate") {
508
- this.deliverImmediate();
509
- return;
510
- }
511
- if (this.deliveryMode === "after_flush") {
512
- this.isAfterFlushDeliveryScheduled = true;
513
- return;
514
- }
515
- if (this.isFlushEnqueued) return;
516
- this.isFlushEnqueued = true;
517
- this.dequeueFlush = this.queue.enqueue(this.onFlush);
510
+ const carrier = this.resolver.findCarrier(key);
511
+ if (carrier) this.markUpdatedCarrier(carrier);
512
+ else this.assertNotInFlush();
513
+ }
514
+ markUpdatedKeys(keys) {
515
+ for (let i = 0; i < keys.length; i++) this.markUpdatedKey(keys[i]);
518
516
  }
519
517
  markAllUpdated() {
520
- if (this.deliveryMode === "queue" && this.queue.isInFlush) {
521
- throw new Error(
522
- "OIMUpdateEventEmitter: updates during queue.flush() are not allowed. Finish all writes before calling queue.flush()."
523
- );
524
- }
525
- if (this.keyHandlers.size === 0) return;
526
- this.keyHandlers.forEach((_handlers, key) => {
527
- this.updatedKeys.add(key);
518
+ this.assertNotInFlush();
519
+ if (this.subscribedCarriers.size === 0) return;
520
+ this.subscribedCarriers.forEach((carrier) => {
521
+ if (carrier.dirty) return;
522
+ carrier.dirty = true;
523
+ this.dirtyCarriers.push(carrier);
528
524
  });
529
- if (this.deliveryMode === "immediate") {
530
- this.deliverImmediate();
531
- return;
532
- }
533
- if (this.deliveryMode === "after_flush") {
534
- this.isAfterFlushDeliveryScheduled = true;
535
- return;
536
- }
537
- if (this.isFlushEnqueued) return;
538
- this.isFlushEnqueued = true;
539
- this.dequeueFlush = this.queue.enqueue(this.onFlush);
540
- }
541
- deliverImmediate() {
542
- this.runSinglePass();
543
- }
544
- /**
545
- * Notifies all handlers for a key. The overwhelmingly common case is a
546
- * single subscriber per key (one component per pk), handled without any
547
- * allocation. For multiple subscribers a snapshot is taken so a handler may
548
- * (un)subscribe during iteration without corrupting the walk.
549
- */
550
- notifyHandlers(handlers) {
551
- if (handlers.size === 1) {
552
- const only = handlers.values().next().value;
553
- if (only) only();
554
- return;
555
- }
556
- const snapshot = [];
557
- handlers.forEach((h) => snapshot.push(h));
558
- for (let i = 0; i < snapshot.length; i++) {
559
- const handler = snapshot[i];
560
- if (handlers.has(handler)) handler();
561
- }
525
+ this.scheduleFlush();
562
526
  }
563
- /**
564
- * Deliver updated keys and notify handlers once.
565
- * Re-entrant updates during delivery are forbidden and will throw in markUpdatedKeys().
566
- */
567
- runSinglePass() {
568
- if (this.updatedKeys.size === 0) return;
569
- const flushingKeys = this.updatedKeys;
570
- this.updatedKeys = /* @__PURE__ */ new Set();
571
- if (this.keyHandlers.size === 0 || flushingKeys.size === 0) return;
572
- if (flushingKeys.size * 2 < this.keyHandlers.size) {
573
- flushingKeys.forEach((key) => {
574
- const handlers = this.keyHandlers.get(key);
575
- if (!handlers || handlers.size === 0) return;
576
- this.notifyHandlers(handlers);
577
- });
578
- } else {
579
- this.keyHandlers.forEach((handlers, key) => {
580
- if (!flushingKeys.has(key)) return;
581
- if (!handlers || handlers.size === 0) return;
582
- this.notifyHandlers(handlers);
583
- });
584
- }
527
+ hasSubscriptions() {
528
+ return this.subscribedCarriers.size > 0;
585
529
  }
586
- destroy() {
587
- if (this.dequeueFlush) {
588
- this.dequeueFlush();
589
- this.dequeueFlush = void 0;
590
- }
591
- if (this.unsubscribeAfterFlushDelivery) {
592
- this.unsubscribeAfterFlushDelivery();
593
- this.unsubscribeAfterFlushDelivery = void 0;
594
- }
595
- this.keyHandlers.forEach((handlers) => handlers.clear());
596
- this.keyHandlers.clear();
597
- this.updatedKeys.clear();
530
+ getHandlerCount(key) {
531
+ return this.resolver.findCarrier(key)?.subscribers?.size ?? 0;
598
532
  }
599
- /**
600
- * Get performance metrics for monitoring and debugging
601
- */
602
533
  getMetrics() {
603
534
  let totalHandlers = 0;
604
- this.keyHandlers.forEach((handlers) => {
605
- totalHandlers += handlers.size;
535
+ this.subscribedCarriers.forEach((carrier) => {
536
+ totalHandlers += carrier.subscribers?.size ?? 0;
606
537
  });
538
+ const totalKeys = this.subscribedCarriers.size;
607
539
  return {
608
- totalKeys: this.keyHandlers.size,
540
+ totalKeys,
609
541
  totalHandlers,
610
- averageHandlersPerKey: this.keyHandlers.size > 0 ? totalHandlers / this.keyHandlers.size : 0,
611
- queueLength: this.queue.length
542
+ averageHandlersPerKey: totalKeys > 0 ? totalHandlers / totalKeys : 0,
543
+ queueLength: this.dirtyCarriers.length
612
544
  };
613
545
  }
614
- /**
615
- * Check if there are any active subscriptions
616
- */
617
- hasSubscriptions() {
618
- return this.keyHandlers.size > 0;
619
- }
620
- /**
621
- * Get the number of handlers for a specific key
622
- */
623
- getHandlerCount(key) {
624
- return this.keyHandlers.get(key)?.size ?? 0;
546
+ destroy() {
547
+ if (this.isFlushEnqueued) {
548
+ this.queue.cancelTask(this.onFlush);
549
+ this.isFlushEnqueued = false;
550
+ }
551
+ this.subscribedCarriers.forEach((carrier) => {
552
+ carrier.subscribers?.clear();
553
+ carrier.subscribers = void 0;
554
+ });
555
+ this.subscribedCarriers.clear();
556
+ for (let i = 0; i < this.dirtyCarriers.length; i++) {
557
+ this.dirtyCarriers[i].dirty = false;
558
+ }
559
+ this.dirtyCarriers = [];
560
+ this.isFlushEnqueued = false;
625
561
  }
626
- subscribeOnKey(key, handler) {
627
- let handlers = this.keyHandlers.get(key);
628
- if (!handlers) {
629
- handlers = /* @__PURE__ */ new Set();
630
- this.keyHandlers.set(key, handlers);
562
+ assertNotInFlush() {
563
+ if (this.queue.isInFlush) {
564
+ throw new Error(
565
+ "OIMCarrierKeyedEmitter: updates during queue.flush() are not allowed. Finish all writes before calling queue.flush()."
566
+ );
631
567
  }
632
- handlers.add(handler);
633
- return () => {
634
- this.unsubscribeFromKey(key, handler);
635
- };
636
568
  }
637
- unsubscribeFromKey(key, handler) {
638
- const handlers = this.keyHandlers.get(key);
639
- if (!handlers) return;
640
- handlers.delete(handler);
641
- if (handlers.size === 0) {
642
- this.keyHandlers.delete(key);
643
- this.updatedKeys.delete(key);
569
+ scheduleFlush() {
570
+ if (this.isFlushEnqueued) return;
571
+ this.isFlushEnqueued = true;
572
+ this.queue.enqueueTask(this.onFlush);
573
+ }
574
+ notify(carrier) {
575
+ const subscribers = carrier.subscribers;
576
+ if (!subscribers || subscribers.size === 0) return;
577
+ if (subscribers.size === 1) {
578
+ const only = subscribers.values().next().value;
579
+ if (only) only();
580
+ return;
644
581
  }
645
- if (this.keyHandlers.size === 0) {
646
- this.updatedKeys.clear();
647
- if (this.dequeueFlush) {
648
- this.dequeueFlush();
649
- this.dequeueFlush = void 0;
650
- }
651
- this.isFlushEnqueued = false;
582
+ const snapshot = [];
583
+ subscribers.forEach((h) => snapshot.push(h));
584
+ for (let i = 0; i < snapshot.length; i++) {
585
+ const handler = snapshot[i];
586
+ if (subscribers.has(handler)) handler();
652
587
  }
653
588
  }
654
- subscribeOnKeys(keys, handler) {
655
- const subscribedKeys = [];
656
- for (const key of keys) {
657
- let handlers = this.keyHandlers.get(key);
658
- if (!handlers) {
659
- handlers = /* @__PURE__ */ new Set();
660
- this.keyHandlers.set(key, handlers);
661
- }
662
- if (!handlers.has(handler)) {
663
- handlers.add(handler);
664
- subscribedKeys.push(key);
665
- }
666
- }
667
- return () => {
668
- for (const key of subscribedKeys) {
669
- this.unsubscribeFromKey(key, handler);
670
- }
671
- };
589
+ };
590
+
591
+ // src/core/OIMKeyedCarrierResolver.ts
592
+ var OIMKeyedCarrierResolver = class {
593
+ constructor() {
594
+ this.carriers = /* @__PURE__ */ new Map();
672
595
  }
673
- unsubscribeFromKeys(keys, handler) {
674
- for (const key of keys) {
675
- this.unsubscribeFromKey(key, handler);
596
+ getOrReserveCarrier(key) {
597
+ let carrier = this.carriers.get(key);
598
+ if (!carrier) {
599
+ carrier = { key };
600
+ this.carriers.set(key, carrier);
676
601
  }
602
+ return carrier;
603
+ }
604
+ findCarrier(key) {
605
+ return this.carriers.get(key);
606
+ }
607
+ onCarrierEmptied(carrier) {
608
+ this.carriers.delete(carrier.key);
677
609
  }
678
610
  };
679
611
 
680
612
  // src/abstract/OIMReactiveIndex.ts
681
613
  var OIMReactiveIndex = class {
682
614
  constructor(queue, createIndex) {
683
- this.updateEmitter = new OIMUpdateEventEmitter(queue);
615
+ this.updateEmitter = new OIMCarrierKeyedEmitter(queue, new OIMKeyedCarrierResolver());
684
616
  this.index = createIndex(this.updateEmitter);
685
617
  }
686
618
  getEntitiesByKey(key) {
@@ -1011,192 +943,45 @@ var OIMCollection = class {
1011
943
  removeManyByPks(pks) {
1012
944
  if (pks.length === 0) return;
1013
945
  this.store.removeManyByPks(pks);
1014
- this.emitter.emit(0 /* UPDATE */, { pks });
1015
- }
1016
- clear() {
1017
- this.store.clear();
1018
- this.emitter.emit(0 /* UPDATE */, { pks: [] });
1019
- }
1020
- countAll() {
1021
- return this.store.countAll();
1022
- }
1023
- getAll() {
1024
- return this.store.getAll();
1025
- }
1026
- getAllPks() {
1027
- return this.store.getAllPks();
1028
- }
1029
- upsertOneWithoutNotifications(entity) {
1030
- const pk = this.selectPk(entity);
1031
- return this.upsertOneWithoutNotificationsByPk(pk, entity);
1032
- }
1033
- upsertOneWithoutNotificationsByPk(pk, entity) {
1034
- if (!pk) {
1035
- throw new Error(
1036
- `[OIMCollection]: PK is required to upsert an entity ${JSON.stringify(entity)}`
1037
- );
1038
- }
1039
- const existingEntity = this.store.getOneByPk(pk);
1040
- if (existingEntity) {
1041
- return this.store.setOneByPk(
1042
- pk,
1043
- this.updateEntity(entity, existingEntity)
1044
- );
1045
- } else {
1046
- return this.store.setOneByPk(pk, entity);
1047
- }
1048
- }
1049
- destroy() {
1050
- this.store.destroy();
1051
- this.emitter.offAll();
1052
- }
1053
- };
1054
-
1055
- // src/core/OIMCarrierKeyedEmitter.ts
1056
- var OIMCarrierKeyedEmitter = class {
1057
- constructor(queue, resolver) {
1058
- // Carriers with at least one subscriber — for markAllUpdated / metrics.
1059
- this.subscribedCarriers = /* @__PURE__ */ new Set();
1060
- this.dirtyCarriers = /* @__PURE__ */ new Set();
1061
- this.isFlushEnqueued = false;
1062
- this.onFlush = () => {
1063
- this.isFlushEnqueued = false;
1064
- this.dequeueFlush = void 0;
1065
- if (this.dirtyCarriers.size === 0) return;
1066
- const flushing = this.dirtyCarriers;
1067
- this.dirtyCarriers = /* @__PURE__ */ new Set();
1068
- flushing.forEach((carrier) => this.notify(carrier));
1069
- };
1070
- this.queue = queue;
1071
- this.resolver = resolver;
1072
- }
1073
- subscribeOnKey(key, handler) {
1074
- const carrier = this.resolver.getOrReserveCarrier(key);
1075
- this.addHandler(carrier, handler);
1076
- return () => this.removeHandler(carrier, handler);
1077
- }
1078
- subscribeOnKeys(keys, handler) {
1079
- const carriers = [];
1080
- for (let i = 0; i < keys.length; i++) {
1081
- const carrier = this.resolver.getOrReserveCarrier(keys[i]);
1082
- this.addHandler(carrier, handler);
1083
- carriers.push(carrier);
1084
- }
1085
- return () => {
1086
- for (let i = 0; i < carriers.length; i++) {
1087
- this.removeHandler(carriers[i], handler);
1088
- }
1089
- };
1090
- }
1091
- unsubscribeFromKey(key, handler) {
1092
- const carrier = this.resolver.findCarrier(key);
1093
- if (carrier) this.removeHandler(carrier, handler);
1094
- }
1095
- unsubscribeFromKeys(keys, handler) {
1096
- for (let i = 0; i < keys.length; i++) {
1097
- this.unsubscribeFromKey(keys[i], handler);
1098
- }
1099
- }
1100
- addHandler(carrier, handler) {
1101
- let subscribers = carrier.subscribers;
1102
- if (!subscribers) {
1103
- subscribers = /* @__PURE__ */ new Set();
1104
- carrier.subscribers = subscribers;
1105
- }
1106
- subscribers.add(handler);
1107
- this.subscribedCarriers.add(carrier);
1108
- }
1109
- removeHandler(carrier, handler) {
1110
- const subscribers = carrier.subscribers;
1111
- if (!subscribers) return;
1112
- subscribers.delete(handler);
1113
- if (subscribers.size === 0) {
1114
- this.subscribedCarriers.delete(carrier);
1115
- }
1116
- }
1117
- /** Fast path: the writer already holds the carrier. */
1118
- markUpdatedCarrier(carrier) {
1119
- this.assertNotInFlush();
1120
- const subscribers = carrier.subscribers;
1121
- if (!subscribers || subscribers.size === 0) return;
1122
- this.dirtyCarriers.add(carrier);
1123
- this.scheduleFlush();
1124
- }
1125
- markUpdatedKey(key) {
1126
- const carrier = this.resolver.findCarrier(key);
1127
- if (carrier) this.markUpdatedCarrier(carrier);
1128
- else this.assertNotInFlush();
1129
- }
1130
- markUpdatedKeys(keys) {
1131
- for (let i = 0; i < keys.length; i++) this.markUpdatedKey(keys[i]);
1132
- }
1133
- markAllUpdated() {
1134
- this.assertNotInFlush();
1135
- if (this.subscribedCarriers.size === 0) return;
1136
- this.subscribedCarriers.forEach(
1137
- (carrier) => this.dirtyCarriers.add(carrier)
1138
- );
1139
- this.scheduleFlush();
946
+ this.emitter.emit(0 /* UPDATE */, { pks });
1140
947
  }
1141
- hasSubscriptions() {
1142
- return this.subscribedCarriers.size > 0;
948
+ clear() {
949
+ this.store.clear();
950
+ this.emitter.emit(0 /* UPDATE */, { pks: [] });
1143
951
  }
1144
- getHandlerCount(key) {
1145
- return this.resolver.findCarrier(key)?.subscribers?.size ?? 0;
952
+ countAll() {
953
+ return this.store.countAll();
1146
954
  }
1147
- getMetrics() {
1148
- let totalHandlers = 0;
1149
- this.subscribedCarriers.forEach((carrier) => {
1150
- totalHandlers += carrier.subscribers?.size ?? 0;
1151
- });
1152
- const totalKeys = this.subscribedCarriers.size;
1153
- return {
1154
- totalKeys,
1155
- totalHandlers,
1156
- averageHandlersPerKey: totalKeys > 0 ? totalHandlers / totalKeys : 0,
1157
- queueLength: this.dirtyCarriers.size
1158
- };
955
+ getAll() {
956
+ return this.store.getAll();
1159
957
  }
1160
- destroy() {
1161
- if (this.dequeueFlush) {
1162
- this.dequeueFlush();
1163
- this.dequeueFlush = void 0;
1164
- }
1165
- this.subscribedCarriers.forEach((carrier) => {
1166
- carrier.subscribers?.clear();
1167
- carrier.subscribers = void 0;
1168
- });
1169
- this.subscribedCarriers.clear();
1170
- this.dirtyCarriers.clear();
1171
- this.isFlushEnqueued = false;
958
+ getAllPks() {
959
+ return this.store.getAllPks();
1172
960
  }
1173
- assertNotInFlush() {
1174
- if (this.queue.isInFlush) {
961
+ upsertOneWithoutNotifications(entity) {
962
+ const pk = this.selectPk(entity);
963
+ return this.upsertOneWithoutNotificationsByPk(pk, entity);
964
+ }
965
+ upsertOneWithoutNotificationsByPk(pk, entity) {
966
+ if (!pk) {
1175
967
  throw new Error(
1176
- "OIMCarrierKeyedEmitter: updates during queue.flush() are not allowed. Finish all writes before calling queue.flush()."
968
+ `[OIMCollection]: PK is required to upsert an entity ${JSON.stringify(entity)}`
1177
969
  );
1178
970
  }
1179
- }
1180
- scheduleFlush() {
1181
- if (this.isFlushEnqueued) return;
1182
- this.isFlushEnqueued = true;
1183
- this.dequeueFlush = this.queue.enqueue(this.onFlush);
1184
- }
1185
- notify(carrier) {
1186
- const subscribers = carrier.subscribers;
1187
- if (!subscribers || subscribers.size === 0) return;
1188
- if (subscribers.size === 1) {
1189
- const only = subscribers.values().next().value;
1190
- if (only) only();
1191
- return;
1192
- }
1193
- const snapshot = [];
1194
- subscribers.forEach((h) => snapshot.push(h));
1195
- for (let i = 0; i < snapshot.length; i++) {
1196
- const handler = snapshot[i];
1197
- if (subscribers.has(handler)) handler();
971
+ const existingEntity = this.store.getOneByPk(pk);
972
+ if (existingEntity) {
973
+ return this.store.setOneByPk(
974
+ pk,
975
+ this.updateEntity(entity, existingEntity)
976
+ );
977
+ } else {
978
+ return this.store.setOneByPk(pk, entity);
1198
979
  }
1199
980
  }
981
+ destroy() {
982
+ this.store.destroy();
983
+ this.emitter.offAll();
984
+ }
1200
985
  };
1201
986
 
1202
987
  // src/core/OIMReactiveCollection.ts
@@ -1210,7 +995,6 @@ var OIMReactiveCollection = class extends OIMCollection {
1210
995
  this.runAnyUpdate = () => {
1211
996
  if (!this.isAnyUpdateScheduled) return;
1212
997
  this.isAnyUpdateScheduled = false;
1213
- this.dequeueAnyUpdate = void 0;
1214
998
  const pks = this.isAnyUpdateClearPending ? [] : Array.from(this.pendingAnyUpdatePks);
1215
999
  this.pendingAnyUpdatePks.clear();
1216
1000
  this.isAnyUpdateClearPending = false;
@@ -1350,13 +1134,13 @@ var OIMReactiveCollection = class extends OIMCollection {
1350
1134
  ensureAnyUpdateEnqueued() {
1351
1135
  if (this.isAnyUpdateScheduled) return;
1352
1136
  this.isAnyUpdateScheduled = true;
1353
- this.dequeueAnyUpdate = this.queue.enqueue(this.runAnyUpdate);
1137
+ this.queue.enqueueTask(this.runAnyUpdate);
1354
1138
  }
1355
1139
  destroy() {
1356
1140
  this.updateEmitter.destroy();
1357
- if (this.dequeueAnyUpdate) {
1358
- this.dequeueAnyUpdate();
1359
- this.dequeueAnyUpdate = void 0;
1141
+ if (this.isAnyUpdateScheduled) {
1142
+ this.queue.cancelTask(this.runAnyUpdate);
1143
+ this.isAnyUpdateScheduled = false;
1360
1144
  }
1361
1145
  this.anyUpdateHandlers.clear();
1362
1146
  this.pendingAnyUpdatePks.clear();
@@ -1375,109 +1159,362 @@ var OIMIndexManualOrderedArrayBased = class extends OIMIndexArrayBased {
1375
1159
  }
1376
1160
  return;
1377
1161
  }
1378
- if (this.store.getOneByKey(key) !== void 0) {
1379
- this.store.removeOneByKey(key);
1380
- this.emitUpdate([key]);
1162
+ if (this.store.getOneByKey(key) !== void 0) {
1163
+ this.store.removeOneByKey(key);
1164
+ this.emitUpdate([key]);
1165
+ }
1166
+ }
1167
+ pushSlot(key, slot) {
1168
+ const list = this.getOrCreateList(key);
1169
+ const index = list.length;
1170
+ list.push(slot);
1171
+ this.emitUpdate([key]);
1172
+ return index;
1173
+ }
1174
+ insertSlotAt(key, index, slot) {
1175
+ const list = this.getOrCreateList(key);
1176
+ const safeIndex = Math.max(0, Math.min(index, list.length));
1177
+ list.splice(safeIndex, 0, slot);
1178
+ this.emitUpdate([key]);
1179
+ return safeIndex;
1180
+ }
1181
+ removeAt(key, index) {
1182
+ const list = this.store.getOneByKey(key);
1183
+ if (!list) return void 0;
1184
+ if (index < 0 || index >= list.length) return void 0;
1185
+ const [removed] = list.splice(index, 1);
1186
+ if (list.length === 0) {
1187
+ this.store.removeOneByKey(key);
1188
+ }
1189
+ this.emitUpdate([key]);
1190
+ return removed;
1191
+ }
1192
+ move(key, fromIndex, toIndex) {
1193
+ const list = this.store.getOneByKey(key);
1194
+ if (!list) return void 0;
1195
+ if (fromIndex < 0 || fromIndex >= list.length) return void 0;
1196
+ if (toIndex < 0 || toIndex >= list.length) return void 0;
1197
+ if (fromIndex === toIndex) return list[fromIndex];
1198
+ const [slot] = list.splice(fromIndex, 1);
1199
+ if (slot === void 0) return void 0;
1200
+ list.splice(toIndex, 0, slot);
1201
+ this.emitUpdate([key]);
1202
+ return slot;
1203
+ }
1204
+ resetSlots(key, slots) {
1205
+ if (slots.length === 0) {
1206
+ if (this.store.getOneByKey(key) !== void 0) {
1207
+ this.store.removeOneByKey(key);
1208
+ this.emitUpdate([key]);
1209
+ }
1210
+ return;
1211
+ }
1212
+ this.store.setOneByKey(key, slots.slice());
1213
+ this.emitUpdate([key]);
1214
+ }
1215
+ getOrCreateList(key) {
1216
+ let list = this.store.getOneByKey(key);
1217
+ if (!list) {
1218
+ list = [];
1219
+ this.store.setOneByKey(key, list);
1220
+ }
1221
+ return list;
1222
+ }
1223
+ };
1224
+
1225
+ // src/modules/wrapper/index/OIMCollectionIndexManualOrderedArrayBased.ts
1226
+ var OIMCollectionIndexManualOrderedArrayBased = class extends OIMIndexManualOrderedArrayBased {
1227
+ constructor(opts) {
1228
+ super();
1229
+ this.resolveSlot = opts.collection !== void 0 ? (pk) => opts.collection.getSlotByPk(pk) : opts.resolveSlot;
1230
+ }
1231
+ push(key, pk) {
1232
+ return this.pushSlot(key, this.resolveRequiredSlot(pk));
1233
+ }
1234
+ insertAt(key, index, pk) {
1235
+ return this.insertSlotAt(key, index, this.resolveRequiredSlot(pk));
1236
+ }
1237
+ reset(key, pks) {
1238
+ this.resetSlots(key, this.resolveSlots(pks));
1239
+ }
1240
+ getEntitiesByKey(key) {
1241
+ return super.getEntitiesByKey(key);
1242
+ }
1243
+ getEntitiesByKeys(keys) {
1244
+ return super.getEntitiesByKeys(keys);
1245
+ }
1246
+ resolvePk(pk) {
1247
+ return this.resolveRequiredSlot(pk);
1248
+ }
1249
+ resolveSlots(pks) {
1250
+ const slots = [];
1251
+ slots.length = pks.length;
1252
+ for (let i = 0; i < pks.length; i++) {
1253
+ slots[i] = this.resolveRequiredSlot(pks[i]);
1254
+ }
1255
+ return slots;
1256
+ }
1257
+ resolveRequiredSlot(pk) {
1258
+ const slot = this.resolveSlot(pk);
1259
+ if (!slot) {
1260
+ throw new Error(
1261
+ `[OIMCollectionIndexManualOrderedArrayBased]: Unable to resolve slot for PK "${String(pk)}".`
1262
+ );
1263
+ }
1264
+ return slot;
1265
+ }
1266
+ };
1267
+
1268
+ // src/enums/EOIMEventQueueEventType.ts
1269
+ var EOIMEventQueueEventType = /* @__PURE__ */ ((EOIMEventQueueEventType2) => {
1270
+ EOIMEventQueueEventType2[EOIMEventQueueEventType2["AFTER_FLUSH"] = 0] = "AFTER_FLUSH";
1271
+ EOIMEventQueueEventType2[EOIMEventQueueEventType2["BEFORE_FLUSH"] = 1] = "BEFORE_FLUSH";
1272
+ return EOIMEventQueueEventType2;
1273
+ })(EOIMEventQueueEventType || {});
1274
+
1275
+ // src/core/OIMUpdateEventEmitter.ts
1276
+ var OIMUpdateEventEmitter = class {
1277
+ constructor(queue, deliveryMode = "queue") {
1278
+ this.keyHandlers = /* @__PURE__ */ new Map();
1279
+ this.isFlushEnqueued = false;
1280
+ this.updatedKeys = /* @__PURE__ */ new Set();
1281
+ this.isAfterFlushDeliveryScheduled = false;
1282
+ // Stable handler reference to avoid allocating a new closure on every HAS_CHANGES.
1283
+ this.onFlush = () => {
1284
+ this.isFlushEnqueued = false;
1285
+ if (this.keyHandlers.size === 0) {
1286
+ this.updatedKeys.clear();
1287
+ return;
1288
+ }
1289
+ this.runSinglePass();
1290
+ };
1291
+ this.onAfterFlush = () => {
1292
+ if (!this.isAfterFlushDeliveryScheduled) return;
1293
+ this.isAfterFlushDeliveryScheduled = false;
1294
+ this.deliverImmediate();
1295
+ };
1296
+ this.queue = queue;
1297
+ this.deliveryMode = deliveryMode;
1298
+ if (this.deliveryMode === "after_flush") {
1299
+ this.unsubscribeAfterFlushDelivery = this.queue.emitter.on(
1300
+ 0 /* AFTER_FLUSH */,
1301
+ this.onAfterFlush
1302
+ );
1303
+ }
1304
+ }
1305
+ /**
1306
+ * Mark keys as updated and schedule delivery.
1307
+ *
1308
+ * If there are no subscriptions, this is a no-op and does not allocate or retain keys.
1309
+ */
1310
+ markUpdatedKeys(keys) {
1311
+ if (keys.length === 0) return;
1312
+ if (this.deliveryMode === "queue" && this.queue.isInFlush) {
1313
+ throw new Error(
1314
+ "OIMUpdateEventEmitter: updates during queue.flush() are not allowed. Finish all writes before calling queue.flush()."
1315
+ );
1316
+ }
1317
+ if (this.keyHandlers.size === 0) return;
1318
+ let didAddAny = false;
1319
+ for (let i = 0; i < keys.length; i++) {
1320
+ const key = keys[i];
1321
+ const handlers = this.keyHandlers.get(key);
1322
+ if (!handlers || handlers.size === 0) continue;
1323
+ this.updatedKeys.add(key);
1324
+ didAddAny = true;
1325
+ }
1326
+ if (!didAddAny) return;
1327
+ if (this.deliveryMode === "immediate") {
1328
+ this.deliverImmediate();
1329
+ return;
1330
+ }
1331
+ if (this.deliveryMode === "after_flush") {
1332
+ this.isAfterFlushDeliveryScheduled = true;
1333
+ return;
1334
+ }
1335
+ if (this.isFlushEnqueued) return;
1336
+ this.isFlushEnqueued = true;
1337
+ this.queue.enqueueTask(this.onFlush);
1338
+ }
1339
+ markUpdatedKey(key) {
1340
+ if (this.deliveryMode === "queue" && this.queue.isInFlush) {
1341
+ throw new Error(
1342
+ "OIMUpdateEventEmitter: updates during queue.flush() are not allowed. Finish all writes before calling queue.flush()."
1343
+ );
1344
+ }
1345
+ if (this.keyHandlers.size === 0) return;
1346
+ const handlers = this.keyHandlers.get(key);
1347
+ if (!handlers || handlers.size === 0) return;
1348
+ this.updatedKeys.add(key);
1349
+ if (this.deliveryMode === "immediate") {
1350
+ this.deliverImmediate();
1351
+ return;
1352
+ }
1353
+ if (this.deliveryMode === "after_flush") {
1354
+ this.isAfterFlushDeliveryScheduled = true;
1355
+ return;
1381
1356
  }
1357
+ if (this.isFlushEnqueued) return;
1358
+ this.isFlushEnqueued = true;
1359
+ this.queue.enqueueTask(this.onFlush);
1382
1360
  }
1383
- pushSlot(key, slot) {
1384
- const list = this.getOrCreateList(key);
1385
- const index = list.length;
1386
- list.push(slot);
1387
- this.emitUpdate([key]);
1388
- return index;
1389
- }
1390
- insertSlotAt(key, index, slot) {
1391
- const list = this.getOrCreateList(key);
1392
- const safeIndex = Math.max(0, Math.min(index, list.length));
1393
- list.splice(safeIndex, 0, slot);
1394
- this.emitUpdate([key]);
1395
- return safeIndex;
1396
- }
1397
- removeAt(key, index) {
1398
- const list = this.store.getOneByKey(key);
1399
- if (!list) return void 0;
1400
- if (index < 0 || index >= list.length) return void 0;
1401
- const [removed] = list.splice(index, 1);
1402
- if (list.length === 0) {
1403
- this.store.removeOneByKey(key);
1361
+ markAllUpdated() {
1362
+ if (this.deliveryMode === "queue" && this.queue.isInFlush) {
1363
+ throw new Error(
1364
+ "OIMUpdateEventEmitter: updates during queue.flush() are not allowed. Finish all writes before calling queue.flush()."
1365
+ );
1404
1366
  }
1405
- this.emitUpdate([key]);
1406
- return removed;
1367
+ if (this.keyHandlers.size === 0) return;
1368
+ this.keyHandlers.forEach((_handlers, key) => {
1369
+ this.updatedKeys.add(key);
1370
+ });
1371
+ if (this.deliveryMode === "immediate") {
1372
+ this.deliverImmediate();
1373
+ return;
1374
+ }
1375
+ if (this.deliveryMode === "after_flush") {
1376
+ this.isAfterFlushDeliveryScheduled = true;
1377
+ return;
1378
+ }
1379
+ if (this.isFlushEnqueued) return;
1380
+ this.isFlushEnqueued = true;
1381
+ this.queue.enqueueTask(this.onFlush);
1407
1382
  }
1408
- move(key, fromIndex, toIndex) {
1409
- const list = this.store.getOneByKey(key);
1410
- if (!list) return void 0;
1411
- if (fromIndex < 0 || fromIndex >= list.length) return void 0;
1412
- if (toIndex < 0 || toIndex >= list.length) return void 0;
1413
- if (fromIndex === toIndex) return list[fromIndex];
1414
- const [slot] = list.splice(fromIndex, 1);
1415
- if (slot === void 0) return void 0;
1416
- list.splice(toIndex, 0, slot);
1417
- this.emitUpdate([key]);
1418
- return slot;
1383
+ deliverImmediate() {
1384
+ this.runSinglePass();
1419
1385
  }
1420
- resetSlots(key, slots) {
1421
- if (slots.length === 0) {
1422
- if (this.store.getOneByKey(key) !== void 0) {
1423
- this.store.removeOneByKey(key);
1424
- this.emitUpdate([key]);
1425
- }
1386
+ /**
1387
+ * Notifies all handlers for a key. The overwhelmingly common case is a
1388
+ * single subscriber per key (one component per pk), handled without any
1389
+ * allocation. For multiple subscribers a snapshot is taken so a handler may
1390
+ * (un)subscribe during iteration without corrupting the walk.
1391
+ */
1392
+ notifyHandlers(handlers) {
1393
+ if (handlers.size === 1) {
1394
+ const only = handlers.values().next().value;
1395
+ if (only) only();
1426
1396
  return;
1427
1397
  }
1428
- this.store.setOneByKey(key, slots.slice());
1429
- this.emitUpdate([key]);
1430
- }
1431
- getOrCreateList(key) {
1432
- let list = this.store.getOneByKey(key);
1433
- if (!list) {
1434
- list = [];
1435
- this.store.setOneByKey(key, list);
1398
+ const snapshot = [];
1399
+ handlers.forEach((h) => snapshot.push(h));
1400
+ for (let i = 0; i < snapshot.length; i++) {
1401
+ const handler = snapshot[i];
1402
+ if (handlers.has(handler)) handler();
1436
1403
  }
1437
- return list;
1438
1404
  }
1439
- };
1440
-
1441
- // src/modules/wrapper/index/OIMCollectionIndexManualOrderedArrayBased.ts
1442
- var OIMCollectionIndexManualOrderedArrayBased = class extends OIMIndexManualOrderedArrayBased {
1443
- constructor(opts) {
1444
- super();
1445
- this.resolveSlot = opts.collection !== void 0 ? (pk) => opts.collection.getSlotByPk(pk) : opts.resolveSlot;
1405
+ /**
1406
+ * Deliver updated keys and notify handlers once.
1407
+ * Re-entrant updates during delivery are forbidden and will throw in markUpdatedKeys().
1408
+ */
1409
+ runSinglePass() {
1410
+ if (this.updatedKeys.size === 0) return;
1411
+ const flushingKeys = this.updatedKeys;
1412
+ this.updatedKeys = /* @__PURE__ */ new Set();
1413
+ if (this.keyHandlers.size === 0 || flushingKeys.size === 0) return;
1414
+ if (flushingKeys.size * 2 < this.keyHandlers.size) {
1415
+ flushingKeys.forEach((key) => {
1416
+ const handlers = this.keyHandlers.get(key);
1417
+ if (!handlers || handlers.size === 0) return;
1418
+ this.notifyHandlers(handlers);
1419
+ });
1420
+ } else {
1421
+ this.keyHandlers.forEach((handlers, key) => {
1422
+ if (!flushingKeys.has(key)) return;
1423
+ if (!handlers || handlers.size === 0) return;
1424
+ this.notifyHandlers(handlers);
1425
+ });
1426
+ }
1446
1427
  }
1447
- push(key, pk) {
1448
- return this.pushSlot(key, this.resolveRequiredSlot(pk));
1428
+ destroy() {
1429
+ if (this.isFlushEnqueued) {
1430
+ this.queue.cancelTask(this.onFlush);
1431
+ this.isFlushEnqueued = false;
1432
+ }
1433
+ if (this.unsubscribeAfterFlushDelivery) {
1434
+ this.unsubscribeAfterFlushDelivery();
1435
+ this.unsubscribeAfterFlushDelivery = void 0;
1436
+ }
1437
+ this.keyHandlers.forEach((handlers) => handlers.clear());
1438
+ this.keyHandlers.clear();
1439
+ this.updatedKeys.clear();
1449
1440
  }
1450
- insertAt(key, index, pk) {
1451
- return this.insertSlotAt(key, index, this.resolveRequiredSlot(pk));
1441
+ /**
1442
+ * Get performance metrics for monitoring and debugging
1443
+ */
1444
+ getMetrics() {
1445
+ let totalHandlers = 0;
1446
+ this.keyHandlers.forEach((handlers) => {
1447
+ totalHandlers += handlers.size;
1448
+ });
1449
+ return {
1450
+ totalKeys: this.keyHandlers.size,
1451
+ totalHandlers,
1452
+ averageHandlersPerKey: this.keyHandlers.size > 0 ? totalHandlers / this.keyHandlers.size : 0,
1453
+ queueLength: this.queue.length
1454
+ };
1452
1455
  }
1453
- reset(key, pks) {
1454
- this.resetSlots(key, this.resolveSlots(pks));
1456
+ /**
1457
+ * Check if there are any active subscriptions
1458
+ */
1459
+ hasSubscriptions() {
1460
+ return this.keyHandlers.size > 0;
1455
1461
  }
1456
- getEntitiesByKey(key) {
1457
- return super.getEntitiesByKey(key);
1462
+ /**
1463
+ * Get the number of handlers for a specific key
1464
+ */
1465
+ getHandlerCount(key) {
1466
+ return this.keyHandlers.get(key)?.size ?? 0;
1458
1467
  }
1459
- getEntitiesByKeys(keys) {
1460
- return super.getEntitiesByKeys(keys);
1468
+ subscribeOnKey(key, handler) {
1469
+ let handlers = this.keyHandlers.get(key);
1470
+ if (!handlers) {
1471
+ handlers = /* @__PURE__ */ new Set();
1472
+ this.keyHandlers.set(key, handlers);
1473
+ }
1474
+ handlers.add(handler);
1475
+ return () => {
1476
+ this.unsubscribeFromKey(key, handler);
1477
+ };
1461
1478
  }
1462
- resolvePk(pk) {
1463
- return this.resolveRequiredSlot(pk);
1479
+ unsubscribeFromKey(key, handler) {
1480
+ const handlers = this.keyHandlers.get(key);
1481
+ if (!handlers) return;
1482
+ handlers.delete(handler);
1483
+ if (handlers.size === 0) {
1484
+ this.keyHandlers.delete(key);
1485
+ this.updatedKeys.delete(key);
1486
+ }
1487
+ if (this.keyHandlers.size === 0) {
1488
+ this.updatedKeys.clear();
1489
+ if (this.isFlushEnqueued) {
1490
+ this.queue.cancelTask(this.onFlush);
1491
+ }
1492
+ this.isFlushEnqueued = false;
1493
+ }
1464
1494
  }
1465
- resolveSlots(pks) {
1466
- const slots = [];
1467
- slots.length = pks.length;
1468
- for (let i = 0; i < pks.length; i++) {
1469
- slots[i] = this.resolveRequiredSlot(pks[i]);
1495
+ subscribeOnKeys(keys, handler) {
1496
+ const subscribedKeys = [];
1497
+ for (const key of keys) {
1498
+ let handlers = this.keyHandlers.get(key);
1499
+ if (!handlers) {
1500
+ handlers = /* @__PURE__ */ new Set();
1501
+ this.keyHandlers.set(key, handlers);
1502
+ }
1503
+ if (!handlers.has(handler)) {
1504
+ handlers.add(handler);
1505
+ subscribedKeys.push(key);
1506
+ }
1470
1507
  }
1471
- return slots;
1508
+ return () => {
1509
+ for (const key of subscribedKeys) {
1510
+ this.unsubscribeFromKey(key, handler);
1511
+ }
1512
+ };
1472
1513
  }
1473
- resolveRequiredSlot(pk) {
1474
- const slot = this.resolveSlot(pk);
1475
- if (!slot) {
1476
- throw new Error(
1477
- `[OIMCollectionIndexManualOrderedArrayBased]: Unable to resolve slot for PK "${String(pk)}".`
1478
- );
1514
+ unsubscribeFromKeys(keys, handler) {
1515
+ for (const key of keys) {
1516
+ this.unsubscribeFromKey(key, handler);
1479
1517
  }
1480
- return slot;
1481
1518
  }
1482
1519
  };
1483
1520
 
@@ -2408,17 +2445,15 @@ var OIMObject = class {
2408
2445
  }
2409
2446
  setProperty(key, value) {
2410
2447
  this.store.setProperty(key, value);
2411
- this.emitter.emit(0 /* UPDATE */, { keys: [key] });
2448
+ this.onUpdatedKey(key);
2412
2449
  }
2413
2450
  merge(draft) {
2414
2451
  this.store.merge(draft);
2415
- this.emitter.emit(0 /* UPDATE */, {
2416
- keys: Object.keys(draft)
2417
- });
2452
+ this.onUpdatedKeys(Object.keys(draft));
2418
2453
  }
2419
2454
  delete(key) {
2420
2455
  this.store.delete(key);
2421
- this.emitter.emit(0 /* UPDATE */, { keys: [key] });
2456
+ this.onUpdatedKey(key);
2422
2457
  }
2423
2458
  get(key) {
2424
2459
  return this.store.get(key);
@@ -2428,7 +2463,24 @@ var OIMObject = class {
2428
2463
  }
2429
2464
  clear() {
2430
2465
  this.store.clear();
2431
- this.emitter.emit(0 /* UPDATE */, { keys: [] });
2466
+ this.onUpdatedKeys([]);
2467
+ }
2468
+ /**
2469
+ * Write fan-out hooks. Split single-key vs multi-key so a reactive subclass
2470
+ * can mark its keyed emitter on the hot single-property path without
2471
+ * allocating a `{ keys: [key] }` payload. The base only fires the general
2472
+ * UPDATE event, and only when something is actually subscribed to it — so an
2473
+ * unobserved write skips the payload allocation entirely.
2474
+ */
2475
+ onUpdatedKey(key) {
2476
+ if (this.emitter.hasHandlers(0 /* UPDATE */)) {
2477
+ this.emitter.emit(0 /* UPDATE */, { keys: [key] });
2478
+ }
2479
+ }
2480
+ onUpdatedKeys(keys) {
2481
+ if (this.emitter.hasHandlers(0 /* UPDATE */)) {
2482
+ this.emitter.emit(0 /* UPDATE */, { keys });
2483
+ }
2432
2484
  }
2433
2485
  count() {
2434
2486
  return this.store.count();
@@ -2452,10 +2504,20 @@ var OIMObject = class {
2452
2504
  var OIMReactiveObject = class extends OIMObject {
2453
2505
  constructor(queue, opts) {
2454
2506
  super(opts);
2455
- this.updateEmitter = new OIMUpdateEventEmitter(queue, "queue");
2456
- this.emitter.on(0 /* UPDATE */, (payload) => {
2457
- this.updateEmitter.markUpdatedKeys(payload.keys);
2458
- });
2507
+ this.updateEmitter = new OIMCarrierKeyedEmitter(queue, new OIMKeyedCarrierResolver());
2508
+ }
2509
+ /**
2510
+ * Mark the written key(s) on the keyed emitter directly — no general-event
2511
+ * bridge, no `{ keys }` payload on the single-key path. `super` still fires
2512
+ * the general UPDATE event for any low-level `emitter` subscribers.
2513
+ */
2514
+ onUpdatedKey(key) {
2515
+ this.updateEmitter.markUpdatedKey(key);
2516
+ super.onUpdatedKey(key);
2517
+ }
2518
+ onUpdatedKeys(keys) {
2519
+ this.updateEmitter.markUpdatedKeys(keys);
2520
+ super.onUpdatedKeys(keys);
2459
2521
  }
2460
2522
  subscribeOnKey(key, handler) {
2461
2523
  return this.updateEmitter.subscribeOnKey(key, handler);
@@ -2559,6 +2621,9 @@ var OIMEventQueue = class {
2559
2621
  constructor(options = {}) {
2560
2622
  this.emitter = new OIMEventEmitter();
2561
2623
  this.tasks = /* @__PURE__ */ new Set();
2624
+ // Reused buffer swapped in during flush so the pending set is never copied
2625
+ // (no per-flush array snapshot) and new enqueues land in a fresh set.
2626
+ this.tasksSpare = /* @__PURE__ */ new Set();
2562
2627
  this.isFlushing = false;
2563
2628
  this.scheduler = options.scheduler;
2564
2629
  if (this.scheduler) {
@@ -2587,6 +2652,22 @@ var OIMEventQueue = class {
2587
2652
  this.tasks.delete(task);
2588
2653
  };
2589
2654
  }
2655
+ /**
2656
+ * Allocation-free enqueue for a **stable** function reference (e.g. a bound
2657
+ * `onFlush` method). The function is stored directly — no wrapper, no
2658
+ * returned closure — and cancelled via {@link cancelTask}. Enqueuing the
2659
+ * same reference twice is idempotent (Set-deduped); callers that may run a
2660
+ * task more than once per flush should use {@link enqueue} instead.
2661
+ */
2662
+ enqueueTask(fn) {
2663
+ this.tasks.add(fn);
2664
+ this.ensureScheduled();
2665
+ }
2666
+ /** Cancel a task scheduled via {@link enqueueTask}; safe to call mid-flush. */
2667
+ cancelTask(fn) {
2668
+ this.tasks.delete(fn);
2669
+ this.flushing?.delete(fn);
2670
+ }
2590
2671
  get isInFlush() {
2591
2672
  return this.isFlushing;
2592
2673
  }
@@ -2606,11 +2687,13 @@ var OIMEventQueue = class {
2606
2687
  this.scheduler?.cancel();
2607
2688
  this.isFlushing = true;
2608
2689
  this.emitter.emit(1 /* BEFORE_FLUSH */, void 0);
2609
- const snapshot = Array.from(this.tasks);
2610
- this.tasks.clear();
2611
- for (let i = 0; i < snapshot.length; i++) {
2612
- snapshot[i]();
2613
- }
2690
+ const flushing = this.tasks;
2691
+ this.tasks = this.tasksSpare;
2692
+ this.flushing = flushing;
2693
+ for (const task of flushing) task();
2694
+ flushing.clear();
2695
+ this.tasksSpare = flushing;
2696
+ this.flushing = void 0;
2614
2697
  this.isFlushing = false;
2615
2698
  this.emitter.emit(0 /* AFTER_FLUSH */, void 0);
2616
2699
  }