@oimdb/core 3.11.0 → 3.11.1

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
@@ -728,19 +728,13 @@ var OIMCarrierKeyedEmitter = class {
728
728
  // doubles as membership: a carrier is in this array iff `carrier.dirty`.
729
729
  this.dirtyCarriers = [];
730
730
  this.isFlushEnqueued = false;
731
- // Number of live subscriptions whose single handler spans 2+ distinct
732
- // carriers (a `subscribeOnKeys` over distinct keys). While this is > 0 a
733
- // flush coalesces delivery so such a handler fires at most ONCE per flush,
734
- // not once per changed carrier — the callback carries no key, so extra
735
- // fires are pure waste. At 0 the hot single-subscriber path pays nothing:
736
- // no per-flush Set, no membership checks.
737
- this.multiCarrierSubscriptions = 0;
738
731
  this.onFlush = () => {
739
732
  this.isFlushEnqueued = false;
740
733
  if (this.dirtyCarriers.length === 0) return;
741
734
  const flushing = this.dirtyCarriers;
742
735
  this.dirtyCarriers = [];
743
- const delivered = this.multiCarrierSubscriptions > 0 ? this.deliveredPool ?? (this.deliveredPool = /* @__PURE__ */ new Set()) : void 0;
736
+ const multiCarrierHandlers = this.multiCarrierHandlers;
737
+ const delivered = multiCarrierHandlers !== void 0 && multiCarrierHandlers.size > 0 ? this.deliveredPool ?? (this.deliveredPool = /* @__PURE__ */ new Set()) : void 0;
744
738
  let i = 0;
745
739
  try {
746
740
  for (; i < flushing.length; i++) {
@@ -768,12 +762,14 @@ var OIMCarrierKeyedEmitter = class {
768
762
  carriers.push(carrier);
769
763
  }
770
764
  const spansMultipleCarriers = carriers.length > 1;
771
- if (spansMultipleCarriers) this.multiCarrierSubscriptions++;
765
+ if (spansMultipleCarriers) this.retainMultiCarrierHandler(handler);
772
766
  let unsubscribed = false;
773
767
  return () => {
774
768
  if (unsubscribed) return;
775
769
  unsubscribed = true;
776
- if (spansMultipleCarriers) this.multiCarrierSubscriptions--;
770
+ if (spansMultipleCarriers) {
771
+ this.releaseMultiCarrierHandler(handler);
772
+ }
777
773
  for (let i = 0; i < carriers.length; i++) {
778
774
  this.removeHandler(carriers[i], handler);
779
775
  }
@@ -784,10 +780,25 @@ var OIMCarrierKeyedEmitter = class {
784
780
  if (carrier) this.removeHandler(carrier, handler);
785
781
  }
786
782
  unsubscribeFromKeys(keys, handler) {
783
+ if (keys.length > 1) this.releaseMultiCarrierHandler(handler);
787
784
  for (let i = 0; i < keys.length; i++) {
788
785
  this.unsubscribeFromKey(keys[i], handler);
789
786
  }
790
787
  }
788
+ /** Take a reference for a handler whose subscription spans several carriers. */
789
+ retainMultiCarrierHandler(handler) {
790
+ const handlers = this.multiCarrierHandlers ?? (this.multiCarrierHandlers = /* @__PURE__ */ new Map());
791
+ handlers.set(handler, (handlers.get(handler) ?? 0) + 1);
792
+ }
793
+ /** Drop one reference; the handler leaves the set when none are left. */
794
+ releaseMultiCarrierHandler(handler) {
795
+ const handlers = this.multiCarrierHandlers;
796
+ if (handlers === void 0) return;
797
+ const count = handlers.get(handler);
798
+ if (count === void 0) return;
799
+ if (count > 1) handlers.set(handler, count - 1);
800
+ else handlers.delete(handler);
801
+ }
791
802
  addHandler(carrier, handler) {
792
803
  let subscribers = carrier.subscribers;
793
804
  if (!subscribers) {
@@ -876,6 +887,8 @@ var OIMCarrierKeyedEmitter = class {
876
887
  }
877
888
  this.dirtyCarriers = [];
878
889
  this.isFlushEnqueued = false;
890
+ this.multiCarrierHandlers?.clear();
891
+ this.deliveredPool?.clear();
879
892
  }
880
893
  assertNotInFlush() {
881
894
  if (this.queue.isInFlush) {
@@ -892,13 +905,12 @@ var OIMCarrierKeyedEmitter = class {
892
905
  notify(carrier, delivered) {
893
906
  const subscribers = carrier.subscribers;
894
907
  if (!subscribers || subscribers.size === 0) return;
908
+ const multiCarrierHandlers = delivered ? this.multiCarrierHandlers : void 0;
895
909
  if (subscribers.size === 1) {
896
910
  const only = subscribers.values().next().value;
897
911
  if (!only) return;
898
- if (delivered) {
899
- const size = delivered.size;
900
- delivered.add(only);
901
- if (delivered.size === size) return;
912
+ if (this.isAlreadyDelivered(only, delivered, multiCarrierHandlers)) {
913
+ return;
902
914
  }
903
915
  only();
904
916
  return;
@@ -908,14 +920,25 @@ var OIMCarrierKeyedEmitter = class {
908
920
  for (let i = 0; i < snapshot.length; i++) {
909
921
  const handler = snapshot[i];
910
922
  if (!subscribers.has(handler)) continue;
911
- if (delivered) {
912
- const size = delivered.size;
913
- delivered.add(handler);
914
- if (delivered.size === size) continue;
923
+ if (this.isAlreadyDelivered(handler, delivered, multiCarrierHandlers)) {
924
+ continue;
915
925
  }
916
926
  handler();
917
927
  }
918
928
  }
929
+ /**
930
+ * Whether this handler already fired in the current flush and must be
931
+ * skipped. Only multi-carrier handlers are tracked; everything else always
932
+ * delivers. Membership is recorded via the size delta of a single `add` —
933
+ * one hash probe instead of `has` + `add`.
934
+ */
935
+ isAlreadyDelivered(handler, delivered, multiCarrierHandlers) {
936
+ if (delivered === void 0) return false;
937
+ if (multiCarrierHandlers?.has(handler) !== true) return false;
938
+ const size = delivered.size;
939
+ delivered.add(handler);
940
+ return delivered.size === size;
941
+ }
919
942
  };
920
943
 
921
944
  // src/core/OIMKeyedCarrierProvider.ts
package/dist/index.d.cts CHANGED
@@ -509,13 +509,17 @@ declare class OIMCarrierKeyedEmitter<TKey extends TOIMKey, TCarrier extends IOIM
509
509
  private readonly subscribedCarriers;
510
510
  private dirtyCarriers;
511
511
  private isFlushEnqueued;
512
- private multiCarrierSubscriptions;
512
+ private multiCarrierHandlers?;
513
513
  private deliveredPool?;
514
514
  constructor(queue: OIMEventQueue, carrierProvider: IOIMCarrierProvider<TKey, TCarrier>);
515
515
  subscribeOnKey(key: TKey, handler: TOIMEventHandler<void>): () => void;
516
516
  subscribeOnKeys(keys: readonly TKey[], handler: TOIMEventHandler<void>): () => void;
517
517
  unsubscribeFromKey(key: TKey, handler: TOIMEventHandler<void>): void;
518
518
  unsubscribeFromKeys(keys: readonly TKey[], handler: TOIMEventHandler<void>): void;
519
+ /** Take a reference for a handler whose subscription spans several carriers. */
520
+ private retainMultiCarrierHandler;
521
+ /** Drop one reference; the handler leaves the set when none are left. */
522
+ private releaseMultiCarrierHandler;
519
523
  private addHandler;
520
524
  private removeHandler;
521
525
  /** Fast path: the writer already holds the carrier. */
@@ -536,6 +540,13 @@ declare class OIMCarrierKeyedEmitter<TKey extends TOIMKey, TCarrier extends IOIM
536
540
  private scheduleFlush;
537
541
  private readonly onFlush;
538
542
  private notify;
543
+ /**
544
+ * Whether this handler already fired in the current flush and must be
545
+ * skipped. Only multi-carrier handlers are tracked; everything else always
546
+ * delivers. Membership is recorded via the size delta of a single `add` —
547
+ * one hash probe instead of `has` + `add`.
548
+ */
549
+ private isAlreadyDelivered;
539
550
  }
540
551
 
541
552
  /**
package/dist/index.d.ts CHANGED
@@ -509,13 +509,17 @@ declare class OIMCarrierKeyedEmitter<TKey extends TOIMKey, TCarrier extends IOIM
509
509
  private readonly subscribedCarriers;
510
510
  private dirtyCarriers;
511
511
  private isFlushEnqueued;
512
- private multiCarrierSubscriptions;
512
+ private multiCarrierHandlers?;
513
513
  private deliveredPool?;
514
514
  constructor(queue: OIMEventQueue, carrierProvider: IOIMCarrierProvider<TKey, TCarrier>);
515
515
  subscribeOnKey(key: TKey, handler: TOIMEventHandler<void>): () => void;
516
516
  subscribeOnKeys(keys: readonly TKey[], handler: TOIMEventHandler<void>): () => void;
517
517
  unsubscribeFromKey(key: TKey, handler: TOIMEventHandler<void>): void;
518
518
  unsubscribeFromKeys(keys: readonly TKey[], handler: TOIMEventHandler<void>): void;
519
+ /** Take a reference for a handler whose subscription spans several carriers. */
520
+ private retainMultiCarrierHandler;
521
+ /** Drop one reference; the handler leaves the set when none are left. */
522
+ private releaseMultiCarrierHandler;
519
523
  private addHandler;
520
524
  private removeHandler;
521
525
  /** Fast path: the writer already holds the carrier. */
@@ -536,6 +540,13 @@ declare class OIMCarrierKeyedEmitter<TKey extends TOIMKey, TCarrier extends IOIM
536
540
  private scheduleFlush;
537
541
  private readonly onFlush;
538
542
  private notify;
543
+ /**
544
+ * Whether this handler already fired in the current flush and must be
545
+ * skipped. Only multi-carrier handlers are tracked; everything else always
546
+ * delivers. Membership is recorded via the size delta of a single `add` —
547
+ * one hash probe instead of `has` + `add`.
548
+ */
549
+ private isAlreadyDelivered;
539
550
  }
540
551
 
541
552
  /**
package/dist/index.js CHANGED
@@ -578,19 +578,13 @@ var OIMCarrierKeyedEmitter = class {
578
578
  // doubles as membership: a carrier is in this array iff `carrier.dirty`.
579
579
  this.dirtyCarriers = [];
580
580
  this.isFlushEnqueued = false;
581
- // Number of live subscriptions whose single handler spans 2+ distinct
582
- // carriers (a `subscribeOnKeys` over distinct keys). While this is > 0 a
583
- // flush coalesces delivery so such a handler fires at most ONCE per flush,
584
- // not once per changed carrier — the callback carries no key, so extra
585
- // fires are pure waste. At 0 the hot single-subscriber path pays nothing:
586
- // no per-flush Set, no membership checks.
587
- this.multiCarrierSubscriptions = 0;
588
581
  this.onFlush = () => {
589
582
  this.isFlushEnqueued = false;
590
583
  if (this.dirtyCarriers.length === 0) return;
591
584
  const flushing = this.dirtyCarriers;
592
585
  this.dirtyCarriers = [];
593
- const delivered = this.multiCarrierSubscriptions > 0 ? this.deliveredPool ?? (this.deliveredPool = /* @__PURE__ */ new Set()) : void 0;
586
+ const multiCarrierHandlers = this.multiCarrierHandlers;
587
+ const delivered = multiCarrierHandlers !== void 0 && multiCarrierHandlers.size > 0 ? this.deliveredPool ?? (this.deliveredPool = /* @__PURE__ */ new Set()) : void 0;
594
588
  let i = 0;
595
589
  try {
596
590
  for (; i < flushing.length; i++) {
@@ -618,12 +612,14 @@ var OIMCarrierKeyedEmitter = class {
618
612
  carriers.push(carrier);
619
613
  }
620
614
  const spansMultipleCarriers = carriers.length > 1;
621
- if (spansMultipleCarriers) this.multiCarrierSubscriptions++;
615
+ if (spansMultipleCarriers) this.retainMultiCarrierHandler(handler);
622
616
  let unsubscribed = false;
623
617
  return () => {
624
618
  if (unsubscribed) return;
625
619
  unsubscribed = true;
626
- if (spansMultipleCarriers) this.multiCarrierSubscriptions--;
620
+ if (spansMultipleCarriers) {
621
+ this.releaseMultiCarrierHandler(handler);
622
+ }
627
623
  for (let i = 0; i < carriers.length; i++) {
628
624
  this.removeHandler(carriers[i], handler);
629
625
  }
@@ -634,10 +630,25 @@ var OIMCarrierKeyedEmitter = class {
634
630
  if (carrier) this.removeHandler(carrier, handler);
635
631
  }
636
632
  unsubscribeFromKeys(keys, handler) {
633
+ if (keys.length > 1) this.releaseMultiCarrierHandler(handler);
637
634
  for (let i = 0; i < keys.length; i++) {
638
635
  this.unsubscribeFromKey(keys[i], handler);
639
636
  }
640
637
  }
638
+ /** Take a reference for a handler whose subscription spans several carriers. */
639
+ retainMultiCarrierHandler(handler) {
640
+ const handlers = this.multiCarrierHandlers ?? (this.multiCarrierHandlers = /* @__PURE__ */ new Map());
641
+ handlers.set(handler, (handlers.get(handler) ?? 0) + 1);
642
+ }
643
+ /** Drop one reference; the handler leaves the set when none are left. */
644
+ releaseMultiCarrierHandler(handler) {
645
+ const handlers = this.multiCarrierHandlers;
646
+ if (handlers === void 0) return;
647
+ const count = handlers.get(handler);
648
+ if (count === void 0) return;
649
+ if (count > 1) handlers.set(handler, count - 1);
650
+ else handlers.delete(handler);
651
+ }
641
652
  addHandler(carrier, handler) {
642
653
  let subscribers = carrier.subscribers;
643
654
  if (!subscribers) {
@@ -726,6 +737,8 @@ var OIMCarrierKeyedEmitter = class {
726
737
  }
727
738
  this.dirtyCarriers = [];
728
739
  this.isFlushEnqueued = false;
740
+ this.multiCarrierHandlers?.clear();
741
+ this.deliveredPool?.clear();
729
742
  }
730
743
  assertNotInFlush() {
731
744
  if (this.queue.isInFlush) {
@@ -742,13 +755,12 @@ var OIMCarrierKeyedEmitter = class {
742
755
  notify(carrier, delivered) {
743
756
  const subscribers = carrier.subscribers;
744
757
  if (!subscribers || subscribers.size === 0) return;
758
+ const multiCarrierHandlers = delivered ? this.multiCarrierHandlers : void 0;
745
759
  if (subscribers.size === 1) {
746
760
  const only = subscribers.values().next().value;
747
761
  if (!only) return;
748
- if (delivered) {
749
- const size = delivered.size;
750
- delivered.add(only);
751
- if (delivered.size === size) return;
762
+ if (this.isAlreadyDelivered(only, delivered, multiCarrierHandlers)) {
763
+ return;
752
764
  }
753
765
  only();
754
766
  return;
@@ -758,14 +770,25 @@ var OIMCarrierKeyedEmitter = class {
758
770
  for (let i = 0; i < snapshot.length; i++) {
759
771
  const handler = snapshot[i];
760
772
  if (!subscribers.has(handler)) continue;
761
- if (delivered) {
762
- const size = delivered.size;
763
- delivered.add(handler);
764
- if (delivered.size === size) continue;
773
+ if (this.isAlreadyDelivered(handler, delivered, multiCarrierHandlers)) {
774
+ continue;
765
775
  }
766
776
  handler();
767
777
  }
768
778
  }
779
+ /**
780
+ * Whether this handler already fired in the current flush and must be
781
+ * skipped. Only multi-carrier handlers are tracked; everything else always
782
+ * delivers. Membership is recorded via the size delta of a single `add` —
783
+ * one hash probe instead of `has` + `add`.
784
+ */
785
+ isAlreadyDelivered(handler, delivered, multiCarrierHandlers) {
786
+ if (delivered === void 0) return false;
787
+ if (multiCarrierHandlers?.has(handler) !== true) return false;
788
+ const size = delivered.size;
789
+ delivered.add(handler);
790
+ return delivered.size === size;
791
+ }
769
792
  };
770
793
 
771
794
  // src/core/OIMKeyedCarrierProvider.ts
package/llms.txt CHANGED
@@ -86,7 +86,9 @@ byRole.subscribeOnKey('admin', () => {
86
86
  queue.flush(); // delivers subscription
87
87
  ```
88
88
 
89
- **`subscribeOnKeys(keys, handler)` delivery is coalesced per flush:** the handler fires at most ONCE per flush no matter how many of the subscribed keys changed in it (the callback gets no key, so per-key fan-out would be pure waste). This holds across all reactive primitives — sync (collection / index / object, via `OIMCarrierKeyedEmitter`) and the `@oimdb/async` nodes (via `OIMUpdateEventEmitterAsync`) alike. `subscribeOnKey` (single key) is unchanged. Handler identity is preserved, so `unsubscribeFromKeys(keys, handler)` still removes a `subscribeOnKeys` subscription.
89
+ **`subscribeOnKeys(keys, handler)` delivery is coalesced per flush:** the handler fires at most ONCE per flush no matter how many of the subscribed keys changed in it (the callback gets no key, so per-key fan-out would be pure waste). This holds across all reactive primitives — sync (collection / index / object, via `OIMCarrierKeyedEmitter`) and the `@oimdb/async` nodes (via `OIMUpdateEventEmitterAsync`) alike. `subscribeOnKey` (single key) is unchanged. Handler identity is preserved, so `unsubscribeFromKeys(keys, handler)` still removes a `subscribeOnKeys` subscription — and releases the coalescing exactly like the returned unsubscribe closure does.
90
+
91
+ **Coalescing is scoped to the subscription, not to the handler.** It applies only to handlers registered through a multi-key `subscribeOnKeys`. If the same function holds several *independent* `subscribeOnKey` subscriptions, it still gets one delivery per changed key — an unrelated multi-key subscription elsewhere on the same collection/index never changes that.
90
92
 
91
93
  Membership writes are incremental (touch only the changed pks):
92
94
  - `addPks(key, pks)` / `removePks(key, pks)` — **set-based**: O(1) per pk for both. **array-based**: `addPks` O(1), `removePks` O(bucket) (ordered, must scan).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oimdb/core",
3
- "version": "3.11.0",
3
+ "version": "3.11.1",
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",