@oimdb/core 3.1.0 → 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
@@ -537,7 +537,6 @@ var OIMCarrierKeyedEmitter = class {
537
537
  this.isFlushEnqueued = false;
538
538
  this.onFlush = () => {
539
539
  this.isFlushEnqueued = false;
540
- this.dequeueFlush = void 0;
541
540
  if (this.dirtyCarriers.length === 0) return;
542
541
  const flushing = this.dirtyCarriers;
543
542
  this.dirtyCarriers = [];
@@ -647,9 +646,9 @@ var OIMCarrierKeyedEmitter = class {
647
646
  };
648
647
  }
649
648
  destroy() {
650
- if (this.dequeueFlush) {
651
- this.dequeueFlush();
652
- this.dequeueFlush = void 0;
649
+ if (this.isFlushEnqueued) {
650
+ this.queue.cancelTask(this.onFlush);
651
+ this.isFlushEnqueued = false;
653
652
  }
654
653
  this.subscribedCarriers.forEach((carrier) => {
655
654
  carrier.subscribers?.clear();
@@ -672,7 +671,7 @@ var OIMCarrierKeyedEmitter = class {
672
671
  scheduleFlush() {
673
672
  if (this.isFlushEnqueued) return;
674
673
  this.isFlushEnqueued = true;
675
- this.dequeueFlush = this.queue.enqueue(this.onFlush);
674
+ this.queue.enqueueTask(this.onFlush);
676
675
  }
677
676
  notify(carrier) {
678
677
  const subscribers = carrier.subscribers;
@@ -1098,7 +1097,6 @@ var OIMReactiveCollection = class extends OIMCollection {
1098
1097
  this.runAnyUpdate = () => {
1099
1098
  if (!this.isAnyUpdateScheduled) return;
1100
1099
  this.isAnyUpdateScheduled = false;
1101
- this.dequeueAnyUpdate = void 0;
1102
1100
  const pks = this.isAnyUpdateClearPending ? [] : Array.from(this.pendingAnyUpdatePks);
1103
1101
  this.pendingAnyUpdatePks.clear();
1104
1102
  this.isAnyUpdateClearPending = false;
@@ -1238,13 +1236,13 @@ var OIMReactiveCollection = class extends OIMCollection {
1238
1236
  ensureAnyUpdateEnqueued() {
1239
1237
  if (this.isAnyUpdateScheduled) return;
1240
1238
  this.isAnyUpdateScheduled = true;
1241
- this.dequeueAnyUpdate = this.queue.enqueue(this.runAnyUpdate);
1239
+ this.queue.enqueueTask(this.runAnyUpdate);
1242
1240
  }
1243
1241
  destroy() {
1244
1242
  this.updateEmitter.destroy();
1245
- if (this.dequeueAnyUpdate) {
1246
- this.dequeueAnyUpdate();
1247
- this.dequeueAnyUpdate = void 0;
1243
+ if (this.isAnyUpdateScheduled) {
1244
+ this.queue.cancelTask(this.runAnyUpdate);
1245
+ this.isAnyUpdateScheduled = false;
1248
1246
  }
1249
1247
  this.anyUpdateHandlers.clear();
1250
1248
  this.pendingAnyUpdatePks.clear();
@@ -1386,7 +1384,6 @@ var OIMUpdateEventEmitter = class {
1386
1384
  // Stable handler reference to avoid allocating a new closure on every HAS_CHANGES.
1387
1385
  this.onFlush = () => {
1388
1386
  this.isFlushEnqueued = false;
1389
- this.dequeueFlush = void 0;
1390
1387
  if (this.keyHandlers.size === 0) {
1391
1388
  this.updatedKeys.clear();
1392
1389
  return;
@@ -1439,7 +1436,7 @@ var OIMUpdateEventEmitter = class {
1439
1436
  }
1440
1437
  if (this.isFlushEnqueued) return;
1441
1438
  this.isFlushEnqueued = true;
1442
- this.dequeueFlush = this.queue.enqueue(this.onFlush);
1439
+ this.queue.enqueueTask(this.onFlush);
1443
1440
  }
1444
1441
  markUpdatedKey(key) {
1445
1442
  if (this.deliveryMode === "queue" && this.queue.isInFlush) {
@@ -1461,7 +1458,7 @@ var OIMUpdateEventEmitter = class {
1461
1458
  }
1462
1459
  if (this.isFlushEnqueued) return;
1463
1460
  this.isFlushEnqueued = true;
1464
- this.dequeueFlush = this.queue.enqueue(this.onFlush);
1461
+ this.queue.enqueueTask(this.onFlush);
1465
1462
  }
1466
1463
  markAllUpdated() {
1467
1464
  if (this.deliveryMode === "queue" && this.queue.isInFlush) {
@@ -1483,7 +1480,7 @@ var OIMUpdateEventEmitter = class {
1483
1480
  }
1484
1481
  if (this.isFlushEnqueued) return;
1485
1482
  this.isFlushEnqueued = true;
1486
- this.dequeueFlush = this.queue.enqueue(this.onFlush);
1483
+ this.queue.enqueueTask(this.onFlush);
1487
1484
  }
1488
1485
  deliverImmediate() {
1489
1486
  this.runSinglePass();
@@ -1531,9 +1528,9 @@ var OIMUpdateEventEmitter = class {
1531
1528
  }
1532
1529
  }
1533
1530
  destroy() {
1534
- if (this.dequeueFlush) {
1535
- this.dequeueFlush();
1536
- this.dequeueFlush = void 0;
1531
+ if (this.isFlushEnqueued) {
1532
+ this.queue.cancelTask(this.onFlush);
1533
+ this.isFlushEnqueued = false;
1537
1534
  }
1538
1535
  if (this.unsubscribeAfterFlushDelivery) {
1539
1536
  this.unsubscribeAfterFlushDelivery();
@@ -1591,9 +1588,8 @@ var OIMUpdateEventEmitter = class {
1591
1588
  }
1592
1589
  if (this.keyHandlers.size === 0) {
1593
1590
  this.updatedKeys.clear();
1594
- if (this.dequeueFlush) {
1595
- this.dequeueFlush();
1596
- this.dequeueFlush = void 0;
1591
+ if (this.isFlushEnqueued) {
1592
+ this.queue.cancelTask(this.onFlush);
1597
1593
  }
1598
1594
  this.isFlushEnqueued = false;
1599
1595
  }
@@ -2551,17 +2547,15 @@ var OIMObject = class {
2551
2547
  }
2552
2548
  setProperty(key, value) {
2553
2549
  this.store.setProperty(key, value);
2554
- this.emitter.emit(0 /* UPDATE */, { keys: [key] });
2550
+ this.onUpdatedKey(key);
2555
2551
  }
2556
2552
  merge(draft) {
2557
2553
  this.store.merge(draft);
2558
- this.emitter.emit(0 /* UPDATE */, {
2559
- keys: Object.keys(draft)
2560
- });
2554
+ this.onUpdatedKeys(Object.keys(draft));
2561
2555
  }
2562
2556
  delete(key) {
2563
2557
  this.store.delete(key);
2564
- this.emitter.emit(0 /* UPDATE */, { keys: [key] });
2558
+ this.onUpdatedKey(key);
2565
2559
  }
2566
2560
  get(key) {
2567
2561
  return this.store.get(key);
@@ -2571,7 +2565,24 @@ var OIMObject = class {
2571
2565
  }
2572
2566
  clear() {
2573
2567
  this.store.clear();
2574
- 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
+ }
2575
2586
  }
2576
2587
  count() {
2577
2588
  return this.store.count();
@@ -2595,10 +2606,20 @@ var OIMObject = class {
2595
2606
  var OIMReactiveObject = class extends OIMObject {
2596
2607
  constructor(queue, opts) {
2597
2608
  super(opts);
2598
- this.updateEmitter = new OIMUpdateEventEmitter(queue, "queue");
2599
- this.emitter.on(0 /* UPDATE */, (payload) => {
2600
- this.updateEmitter.markUpdatedKeys(payload.keys);
2601
- });
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);
2602
2623
  }
2603
2624
  subscribeOnKey(key, handler) {
2604
2625
  return this.updateEmitter.subscribeOnKey(key, handler);
@@ -2702,6 +2723,9 @@ var OIMEventQueue = class {
2702
2723
  constructor(options = {}) {
2703
2724
  this.emitter = new OIMEventEmitter();
2704
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();
2705
2729
  this.isFlushing = false;
2706
2730
  this.scheduler = options.scheduler;
2707
2731
  if (this.scheduler) {
@@ -2730,6 +2754,22 @@ var OIMEventQueue = class {
2730
2754
  this.tasks.delete(task);
2731
2755
  };
2732
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
+ }
2733
2773
  get isInFlush() {
2734
2774
  return this.isFlushing;
2735
2775
  }
@@ -2749,11 +2789,13 @@ var OIMEventQueue = class {
2749
2789
  this.scheduler?.cancel();
2750
2790
  this.isFlushing = true;
2751
2791
  this.emitter.emit(1 /* BEFORE_FLUSH */, void 0);
2752
- const snapshot = Array.from(this.tasks);
2753
- this.tasks.clear();
2754
- for (let i = 0; i < snapshot.length; i++) {
2755
- snapshot[i]();
2756
- }
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;
2757
2799
  this.isFlushing = false;
2758
2800
  this.emitter.emit(0 /* AFTER_FLUSH */, void 0);
2759
2801
  }
package/dist/index.d.cts CHANGED
@@ -247,6 +247,8 @@ declare enum EOIMEventQueueEventType {
247
247
  declare class OIMEventQueue {
248
248
  readonly emitter: OIMEventEmitter<Record<EOIMEventQueueEventType, void>>;
249
249
  protected tasks: Set<() => void>;
250
+ protected tasksSpare: Set<() => void>;
251
+ protected flushing?: Set<() => void>;
250
252
  protected readonly scheduler?: OIMEventQueueScheduler;
251
253
  protected flushBound?: () => void;
252
254
  protected isFlushing: boolean;
@@ -256,6 +258,16 @@ declare class OIMEventQueue {
256
258
  * Returns a dequeue function that can be called to cancel the task before it runs.
257
259
  */
258
260
  enqueue(fn: () => void): () => void;
261
+ /**
262
+ * Allocation-free enqueue for a **stable** function reference (e.g. a bound
263
+ * `onFlush` method). The function is stored directly — no wrapper, no
264
+ * returned closure — and cancelled via {@link cancelTask}. Enqueuing the
265
+ * same reference twice is idempotent (Set-deduped); callers that may run a
266
+ * task more than once per flush should use {@link enqueue} instead.
267
+ */
268
+ enqueueTask(fn: () => void): void;
269
+ /** Cancel a task scheduled via {@link enqueueTask}; safe to call mid-flush. */
270
+ cancelTask(fn: () => void): void;
259
271
  get isInFlush(): boolean;
260
272
  /**
261
273
  * Dequeue (cancel) a previously enqueued task by its dequeue function.
@@ -454,7 +466,6 @@ declare class OIMCarrierKeyedEmitter<TKey extends TOIMPk, TCarrier extends IOIMS
454
466
  private readonly subscribedCarriers;
455
467
  private dirtyCarriers;
456
468
  private isFlushEnqueued;
457
- private dequeueFlush?;
458
469
  constructor(queue: OIMEventQueue, resolver: IOIMCarrierResolver<TKey, TCarrier>);
459
470
  subscribeOnKey(key: TKey, handler: TOIMEventHandler<void>): () => void;
460
471
  subscribeOnKeys(keys: readonly TKey[], handler: TOIMEventHandler<void>): () => void;
@@ -489,7 +500,6 @@ declare class OIMReactiveCollection<TEntity extends object, TPk extends TOIMPk>
489
500
  private pendingAnyUpdatePks;
490
501
  private isAnyUpdateClearPending;
491
502
  private isAnyUpdateScheduled;
492
- private dequeueAnyUpdate?;
493
503
  constructor(queue: OIMEventQueue, opts?: TOIMCollectionOptions<TEntity, TPk>);
494
504
  subscribeOnKey(pk: TPk, handler: TOIMEventHandler<void>): () => void;
495
505
  subscribeOnKeys(pks: readonly TPk[], handler: TOIMEventHandler<void>): () => void;
@@ -661,7 +671,6 @@ declare class OIMUpdateEventEmitter<TKey extends TOIMPk> {
661
671
  protected queue: OIMEventQueue;
662
672
  private readonly deliveryMode;
663
673
  private isFlushEnqueued;
664
- private dequeueFlush?;
665
674
  private updatedKeys;
666
675
  private isAfterFlushDeliveryScheduled;
667
676
  private unsubscribeAfterFlushDelivery?;
@@ -999,6 +1008,15 @@ declare class OIMObject<TKey extends string, TValue> {
999
1008
  get(key: TKey): TValue | undefined;
1000
1009
  getAll(): Record<TKey, TValue>;
1001
1010
  clear(): void;
1011
+ /**
1012
+ * Write fan-out hooks. Split single-key vs multi-key so a reactive subclass
1013
+ * can mark its keyed emitter on the hot single-property path without
1014
+ * allocating a `{ keys: [key] }` payload. The base only fires the general
1015
+ * UPDATE event, and only when something is actually subscribed to it — so an
1016
+ * unobserved write skips the payload allocation entirely.
1017
+ */
1018
+ protected onUpdatedKey(key: TKey): void;
1019
+ protected onUpdatedKeys(keys: TKey[]): void;
1002
1020
  count(): number;
1003
1021
  keys(): TKey[];
1004
1022
  values(): TValue[];
@@ -1007,8 +1025,15 @@ declare class OIMObject<TKey extends string, TValue> {
1007
1025
  }
1008
1026
 
1009
1027
  declare class OIMReactiveObject<TKey extends string, TValue> extends OIMObject<TKey, TValue> implements IOIMKeyedSubscription<TKey> {
1010
- protected readonly updateEmitter: OIMUpdateEventEmitter<TKey>;
1028
+ protected readonly updateEmitter: IOIMKeyedUpdateEmitter<TKey>;
1011
1029
  constructor(queue: OIMEventQueue, opts?: TOIMObjectOptions<TKey, TValue>);
1030
+ /**
1031
+ * Mark the written key(s) on the keyed emitter directly — no general-event
1032
+ * bridge, no `{ keys }` payload on the single-key path. `super` still fires
1033
+ * the general UPDATE event for any low-level `emitter` subscribers.
1034
+ */
1035
+ protected onUpdatedKey(key: TKey): void;
1036
+ protected onUpdatedKeys(keys: TKey[]): void;
1012
1037
  subscribeOnKey(key: TKey, handler: TOIMEventHandler<void>): () => void;
1013
1038
  subscribeOnKeys(keys: readonly TKey[], handler: TOIMEventHandler<void>): () => void;
1014
1039
  unsubscribeFromKey(key: TKey, handler: TOIMEventHandler<void>): void;
package/dist/index.d.ts CHANGED
@@ -247,6 +247,8 @@ declare enum EOIMEventQueueEventType {
247
247
  declare class OIMEventQueue {
248
248
  readonly emitter: OIMEventEmitter<Record<EOIMEventQueueEventType, void>>;
249
249
  protected tasks: Set<() => void>;
250
+ protected tasksSpare: Set<() => void>;
251
+ protected flushing?: Set<() => void>;
250
252
  protected readonly scheduler?: OIMEventQueueScheduler;
251
253
  protected flushBound?: () => void;
252
254
  protected isFlushing: boolean;
@@ -256,6 +258,16 @@ declare class OIMEventQueue {
256
258
  * Returns a dequeue function that can be called to cancel the task before it runs.
257
259
  */
258
260
  enqueue(fn: () => void): () => void;
261
+ /**
262
+ * Allocation-free enqueue for a **stable** function reference (e.g. a bound
263
+ * `onFlush` method). The function is stored directly — no wrapper, no
264
+ * returned closure — and cancelled via {@link cancelTask}. Enqueuing the
265
+ * same reference twice is idempotent (Set-deduped); callers that may run a
266
+ * task more than once per flush should use {@link enqueue} instead.
267
+ */
268
+ enqueueTask(fn: () => void): void;
269
+ /** Cancel a task scheduled via {@link enqueueTask}; safe to call mid-flush. */
270
+ cancelTask(fn: () => void): void;
259
271
  get isInFlush(): boolean;
260
272
  /**
261
273
  * Dequeue (cancel) a previously enqueued task by its dequeue function.
@@ -454,7 +466,6 @@ declare class OIMCarrierKeyedEmitter<TKey extends TOIMPk, TCarrier extends IOIMS
454
466
  private readonly subscribedCarriers;
455
467
  private dirtyCarriers;
456
468
  private isFlushEnqueued;
457
- private dequeueFlush?;
458
469
  constructor(queue: OIMEventQueue, resolver: IOIMCarrierResolver<TKey, TCarrier>);
459
470
  subscribeOnKey(key: TKey, handler: TOIMEventHandler<void>): () => void;
460
471
  subscribeOnKeys(keys: readonly TKey[], handler: TOIMEventHandler<void>): () => void;
@@ -489,7 +500,6 @@ declare class OIMReactiveCollection<TEntity extends object, TPk extends TOIMPk>
489
500
  private pendingAnyUpdatePks;
490
501
  private isAnyUpdateClearPending;
491
502
  private isAnyUpdateScheduled;
492
- private dequeueAnyUpdate?;
493
503
  constructor(queue: OIMEventQueue, opts?: TOIMCollectionOptions<TEntity, TPk>);
494
504
  subscribeOnKey(pk: TPk, handler: TOIMEventHandler<void>): () => void;
495
505
  subscribeOnKeys(pks: readonly TPk[], handler: TOIMEventHandler<void>): () => void;
@@ -661,7 +671,6 @@ declare class OIMUpdateEventEmitter<TKey extends TOIMPk> {
661
671
  protected queue: OIMEventQueue;
662
672
  private readonly deliveryMode;
663
673
  private isFlushEnqueued;
664
- private dequeueFlush?;
665
674
  private updatedKeys;
666
675
  private isAfterFlushDeliveryScheduled;
667
676
  private unsubscribeAfterFlushDelivery?;
@@ -999,6 +1008,15 @@ declare class OIMObject<TKey extends string, TValue> {
999
1008
  get(key: TKey): TValue | undefined;
1000
1009
  getAll(): Record<TKey, TValue>;
1001
1010
  clear(): void;
1011
+ /**
1012
+ * Write fan-out hooks. Split single-key vs multi-key so a reactive subclass
1013
+ * can mark its keyed emitter on the hot single-property path without
1014
+ * allocating a `{ keys: [key] }` payload. The base only fires the general
1015
+ * UPDATE event, and only when something is actually subscribed to it — so an
1016
+ * unobserved write skips the payload allocation entirely.
1017
+ */
1018
+ protected onUpdatedKey(key: TKey): void;
1019
+ protected onUpdatedKeys(keys: TKey[]): void;
1002
1020
  count(): number;
1003
1021
  keys(): TKey[];
1004
1022
  values(): TValue[];
@@ -1007,8 +1025,15 @@ declare class OIMObject<TKey extends string, TValue> {
1007
1025
  }
1008
1026
 
1009
1027
  declare class OIMReactiveObject<TKey extends string, TValue> extends OIMObject<TKey, TValue> implements IOIMKeyedSubscription<TKey> {
1010
- protected readonly updateEmitter: OIMUpdateEventEmitter<TKey>;
1028
+ protected readonly updateEmitter: IOIMKeyedUpdateEmitter<TKey>;
1011
1029
  constructor(queue: OIMEventQueue, opts?: TOIMObjectOptions<TKey, TValue>);
1030
+ /**
1031
+ * Mark the written key(s) on the keyed emitter directly — no general-event
1032
+ * bridge, no `{ keys }` payload on the single-key path. `super` still fires
1033
+ * the general UPDATE event for any low-level `emitter` subscribers.
1034
+ */
1035
+ protected onUpdatedKey(key: TKey): void;
1036
+ protected onUpdatedKeys(keys: TKey[]): void;
1012
1037
  subscribeOnKey(key: TKey, handler: TOIMEventHandler<void>): () => void;
1013
1038
  subscribeOnKeys(keys: readonly TKey[], handler: TOIMEventHandler<void>): () => void;
1014
1039
  unsubscribeFromKey(key: TKey, handler: TOIMEventHandler<void>): void;
package/dist/index.js CHANGED
@@ -435,7 +435,6 @@ var OIMCarrierKeyedEmitter = class {
435
435
  this.isFlushEnqueued = false;
436
436
  this.onFlush = () => {
437
437
  this.isFlushEnqueued = false;
438
- this.dequeueFlush = void 0;
439
438
  if (this.dirtyCarriers.length === 0) return;
440
439
  const flushing = this.dirtyCarriers;
441
440
  this.dirtyCarriers = [];
@@ -545,9 +544,9 @@ var OIMCarrierKeyedEmitter = class {
545
544
  };
546
545
  }
547
546
  destroy() {
548
- if (this.dequeueFlush) {
549
- this.dequeueFlush();
550
- this.dequeueFlush = void 0;
547
+ if (this.isFlushEnqueued) {
548
+ this.queue.cancelTask(this.onFlush);
549
+ this.isFlushEnqueued = false;
551
550
  }
552
551
  this.subscribedCarriers.forEach((carrier) => {
553
552
  carrier.subscribers?.clear();
@@ -570,7 +569,7 @@ var OIMCarrierKeyedEmitter = class {
570
569
  scheduleFlush() {
571
570
  if (this.isFlushEnqueued) return;
572
571
  this.isFlushEnqueued = true;
573
- this.dequeueFlush = this.queue.enqueue(this.onFlush);
572
+ this.queue.enqueueTask(this.onFlush);
574
573
  }
575
574
  notify(carrier) {
576
575
  const subscribers = carrier.subscribers;
@@ -996,7 +995,6 @@ var OIMReactiveCollection = class extends OIMCollection {
996
995
  this.runAnyUpdate = () => {
997
996
  if (!this.isAnyUpdateScheduled) return;
998
997
  this.isAnyUpdateScheduled = false;
999
- this.dequeueAnyUpdate = void 0;
1000
998
  const pks = this.isAnyUpdateClearPending ? [] : Array.from(this.pendingAnyUpdatePks);
1001
999
  this.pendingAnyUpdatePks.clear();
1002
1000
  this.isAnyUpdateClearPending = false;
@@ -1136,13 +1134,13 @@ var OIMReactiveCollection = class extends OIMCollection {
1136
1134
  ensureAnyUpdateEnqueued() {
1137
1135
  if (this.isAnyUpdateScheduled) return;
1138
1136
  this.isAnyUpdateScheduled = true;
1139
- this.dequeueAnyUpdate = this.queue.enqueue(this.runAnyUpdate);
1137
+ this.queue.enqueueTask(this.runAnyUpdate);
1140
1138
  }
1141
1139
  destroy() {
1142
1140
  this.updateEmitter.destroy();
1143
- if (this.dequeueAnyUpdate) {
1144
- this.dequeueAnyUpdate();
1145
- this.dequeueAnyUpdate = void 0;
1141
+ if (this.isAnyUpdateScheduled) {
1142
+ this.queue.cancelTask(this.runAnyUpdate);
1143
+ this.isAnyUpdateScheduled = false;
1146
1144
  }
1147
1145
  this.anyUpdateHandlers.clear();
1148
1146
  this.pendingAnyUpdatePks.clear();
@@ -1284,7 +1282,6 @@ var OIMUpdateEventEmitter = class {
1284
1282
  // Stable handler reference to avoid allocating a new closure on every HAS_CHANGES.
1285
1283
  this.onFlush = () => {
1286
1284
  this.isFlushEnqueued = false;
1287
- this.dequeueFlush = void 0;
1288
1285
  if (this.keyHandlers.size === 0) {
1289
1286
  this.updatedKeys.clear();
1290
1287
  return;
@@ -1337,7 +1334,7 @@ var OIMUpdateEventEmitter = class {
1337
1334
  }
1338
1335
  if (this.isFlushEnqueued) return;
1339
1336
  this.isFlushEnqueued = true;
1340
- this.dequeueFlush = this.queue.enqueue(this.onFlush);
1337
+ this.queue.enqueueTask(this.onFlush);
1341
1338
  }
1342
1339
  markUpdatedKey(key) {
1343
1340
  if (this.deliveryMode === "queue" && this.queue.isInFlush) {
@@ -1359,7 +1356,7 @@ var OIMUpdateEventEmitter = class {
1359
1356
  }
1360
1357
  if (this.isFlushEnqueued) return;
1361
1358
  this.isFlushEnqueued = true;
1362
- this.dequeueFlush = this.queue.enqueue(this.onFlush);
1359
+ this.queue.enqueueTask(this.onFlush);
1363
1360
  }
1364
1361
  markAllUpdated() {
1365
1362
  if (this.deliveryMode === "queue" && this.queue.isInFlush) {
@@ -1381,7 +1378,7 @@ var OIMUpdateEventEmitter = class {
1381
1378
  }
1382
1379
  if (this.isFlushEnqueued) return;
1383
1380
  this.isFlushEnqueued = true;
1384
- this.dequeueFlush = this.queue.enqueue(this.onFlush);
1381
+ this.queue.enqueueTask(this.onFlush);
1385
1382
  }
1386
1383
  deliverImmediate() {
1387
1384
  this.runSinglePass();
@@ -1429,9 +1426,9 @@ var OIMUpdateEventEmitter = class {
1429
1426
  }
1430
1427
  }
1431
1428
  destroy() {
1432
- if (this.dequeueFlush) {
1433
- this.dequeueFlush();
1434
- this.dequeueFlush = void 0;
1429
+ if (this.isFlushEnqueued) {
1430
+ this.queue.cancelTask(this.onFlush);
1431
+ this.isFlushEnqueued = false;
1435
1432
  }
1436
1433
  if (this.unsubscribeAfterFlushDelivery) {
1437
1434
  this.unsubscribeAfterFlushDelivery();
@@ -1489,9 +1486,8 @@ var OIMUpdateEventEmitter = class {
1489
1486
  }
1490
1487
  if (this.keyHandlers.size === 0) {
1491
1488
  this.updatedKeys.clear();
1492
- if (this.dequeueFlush) {
1493
- this.dequeueFlush();
1494
- this.dequeueFlush = void 0;
1489
+ if (this.isFlushEnqueued) {
1490
+ this.queue.cancelTask(this.onFlush);
1495
1491
  }
1496
1492
  this.isFlushEnqueued = false;
1497
1493
  }
@@ -2449,17 +2445,15 @@ var OIMObject = class {
2449
2445
  }
2450
2446
  setProperty(key, value) {
2451
2447
  this.store.setProperty(key, value);
2452
- this.emitter.emit(0 /* UPDATE */, { keys: [key] });
2448
+ this.onUpdatedKey(key);
2453
2449
  }
2454
2450
  merge(draft) {
2455
2451
  this.store.merge(draft);
2456
- this.emitter.emit(0 /* UPDATE */, {
2457
- keys: Object.keys(draft)
2458
- });
2452
+ this.onUpdatedKeys(Object.keys(draft));
2459
2453
  }
2460
2454
  delete(key) {
2461
2455
  this.store.delete(key);
2462
- this.emitter.emit(0 /* UPDATE */, { keys: [key] });
2456
+ this.onUpdatedKey(key);
2463
2457
  }
2464
2458
  get(key) {
2465
2459
  return this.store.get(key);
@@ -2469,7 +2463,24 @@ var OIMObject = class {
2469
2463
  }
2470
2464
  clear() {
2471
2465
  this.store.clear();
2472
- 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
+ }
2473
2484
  }
2474
2485
  count() {
2475
2486
  return this.store.count();
@@ -2493,10 +2504,20 @@ var OIMObject = class {
2493
2504
  var OIMReactiveObject = class extends OIMObject {
2494
2505
  constructor(queue, opts) {
2495
2506
  super(opts);
2496
- this.updateEmitter = new OIMUpdateEventEmitter(queue, "queue");
2497
- this.emitter.on(0 /* UPDATE */, (payload) => {
2498
- this.updateEmitter.markUpdatedKeys(payload.keys);
2499
- });
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);
2500
2521
  }
2501
2522
  subscribeOnKey(key, handler) {
2502
2523
  return this.updateEmitter.subscribeOnKey(key, handler);
@@ -2600,6 +2621,9 @@ var OIMEventQueue = class {
2600
2621
  constructor(options = {}) {
2601
2622
  this.emitter = new OIMEventEmitter();
2602
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();
2603
2627
  this.isFlushing = false;
2604
2628
  this.scheduler = options.scheduler;
2605
2629
  if (this.scheduler) {
@@ -2628,6 +2652,22 @@ var OIMEventQueue = class {
2628
2652
  this.tasks.delete(task);
2629
2653
  };
2630
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
+ }
2631
2671
  get isInFlush() {
2632
2672
  return this.isFlushing;
2633
2673
  }
@@ -2647,11 +2687,13 @@ var OIMEventQueue = class {
2647
2687
  this.scheduler?.cancel();
2648
2688
  this.isFlushing = true;
2649
2689
  this.emitter.emit(1 /* BEFORE_FLUSH */, void 0);
2650
- const snapshot = Array.from(this.tasks);
2651
- this.tasks.clear();
2652
- for (let i = 0; i < snapshot.length; i++) {
2653
- snapshot[i]();
2654
- }
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;
2655
2697
  this.isFlushing = false;
2656
2698
  this.emitter.emit(0 /* AFTER_FLUSH */, void 0);
2657
2699
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oimdb/core",
3
- "version": "3.1.0",
3
+ "version": "3.2.0",
4
4
  "description": "Core in-memory data library with type-safe indices, reactive subscriptions, and event processing",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",