@oimdb/core 3.7.0 → 3.8.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
@@ -26,6 +26,7 @@ __export(index_exports, {
26
26
  EOIMEventQueueSchedulerEventType: () => EOIMEventQueueSchedulerEventType,
27
27
  EOIMIndexEventType: () => EOIMIndexEventType,
28
28
  EOIMObjectEventType: () => EOIMObjectEventType,
29
+ OIMBucketCarrierResolver: () => OIMBucketCarrierResolver,
29
30
  OIMCarrierKeyedEmitter: () => OIMCarrierKeyedEmitter,
30
31
  OIMCarrierSingleEmitter: () => OIMCarrierSingleEmitter,
31
32
  OIMCollection: () => OIMCollection,
@@ -38,6 +39,7 @@ __export(index_exports, {
38
39
  OIMCollectionSelectors: () => OIMCollectionSelectors,
39
40
  OIMCollectionStore: () => OIMCollectionStore,
40
41
  OIMCollectionStoreMapDriven: () => OIMCollectionStoreMapDriven,
42
+ OIMCollectionStoreTrieDriven: () => OIMCollectionStoreTrieDriven,
41
43
  OIMComparatorFactory: () => OIMComparatorFactory,
42
44
  OIMComputeRuntime: () => OIMComputeRuntime,
43
45
  OIMComputed: () => OIMComputed,
@@ -46,6 +48,7 @@ __export(index_exports, {
46
48
  OIMDerivedCollectionGlobalIndexSetBased: () => OIMDerivedCollectionGlobalIndexSetBased,
47
49
  OIMDerivedCollectionIndexArrayBased: () => OIMDerivedCollectionIndexArrayBased,
48
50
  OIMDerivedCollectionIndexSetBased: () => OIMDerivedCollectionIndexSetBased,
51
+ OIMDisposeScope: () => OIMDisposeScope,
49
52
  OIMEffect: () => OIMEffect,
50
53
  OIMEffectDependencyComputed: () => OIMEffectDependencyComputed,
51
54
  OIMEffectDependencyKeyed: () => OIMEffectDependencyKeyed,
@@ -88,6 +91,13 @@ __export(index_exports, {
88
91
  OIMIndexStoreMapDrivenArrayBased: () => OIMIndexStoreMapDrivenArrayBased,
89
92
  OIMIndexStoreMapDrivenSetBased: () => OIMIndexStoreMapDrivenSetBased,
90
93
  OIMIndexStoreSetBased: () => OIMIndexStoreSetBased,
94
+ OIMIndexStoreTrieDrivenArrayBased: () => OIMIndexStoreTrieDrivenArrayBased,
95
+ OIMIndexStoreTrieDrivenSetBased: () => OIMIndexStoreTrieDrivenSetBased,
96
+ OIMKeyDomainNative: () => OIMKeyDomainNative,
97
+ OIMKeyDomainTrie: () => OIMKeyDomainTrie,
98
+ OIMKeyPathCarrierResolver: () => OIMKeyPathCarrierResolver,
99
+ OIMKeyedBucketArrayBased: () => OIMKeyedBucketArrayBased,
100
+ OIMKeyedBucketSetBased: () => OIMKeyedBucketSetBased,
91
101
  OIMMap2Keys: () => OIMMap2Keys,
92
102
  OIMObject: () => OIMObject,
93
103
  OIMObjectStore: () => OIMObjectStore,
@@ -99,10 +109,13 @@ __export(index_exports, {
99
109
  OIMOrderedListCommandStream: () => OIMOrderedListCommandStream,
100
110
  OIMOrderedListCommandStreamDiffDriven: () => OIMOrderedListCommandStreamDiffDriven,
101
111
  OIMOrderedListMappedCommandStream: () => OIMOrderedListMappedCommandStream,
112
+ OIMPkCodecKeyPath: () => OIMPkCodecKeyPath,
102
113
  OIMPkSelectorFactory: () => OIMPkSelectorFactory,
103
114
  OIMReactiveCollection: () => OIMReactiveCollection,
104
115
  OIMReactiveCollectionGlobalIndexManualArrayBased: () => OIMReactiveCollectionGlobalIndexManualArrayBased,
105
116
  OIMReactiveCollectionGlobalIndexManualSetBased: () => OIMReactiveCollectionGlobalIndexManualSetBased,
117
+ OIMReactiveCollectionIndexCompositeTrieArrayBased: () => OIMReactiveCollectionIndexCompositeTrieArrayBased,
118
+ OIMReactiveCollectionIndexCompositeTrieSetBased: () => OIMReactiveCollectionIndexCompositeTrieSetBased,
106
119
  OIMReactiveCollectionIndexManualArrayBased: () => OIMReactiveCollectionIndexManualArrayBased,
107
120
  OIMReactiveCollectionIndexManualSetBased: () => OIMReactiveCollectionIndexManualSetBased,
108
121
  OIMReactiveGlobalIndex: () => OIMReactiveGlobalIndex,
@@ -117,10 +130,13 @@ __export(index_exports, {
117
130
  OIMReactiveIndexSetBased: () => OIMReactiveIndexSetBased,
118
131
  OIMReactiveObject: () => OIMReactiveObject,
119
132
  OIMSelector: () => OIMSelector,
133
+ OIMTrieMap: () => OIMTrieMap,
134
+ OIMTrieSet: () => OIMTrieSet,
120
135
  createInPlaceEntityUpdater: () => createInPlaceEntityUpdater,
121
136
  createMergeEntityUpdater: () => createMergeEntityUpdater,
122
137
  createOIMCollectionIndexFactory: () => createOIMCollectionIndexFactory,
123
138
  createOIMCollectionKit: () => createOIMCollectionKit,
139
+ createOIMDisposeScope: () => createOIMDisposeScope,
124
140
  createOIMGlobalIndexSlotMap: () => createOIMGlobalIndexSlotMap,
125
141
  createOIMIndexSlotMap: () => createOIMIndexSlotMap,
126
142
  createOIMOrderedListCommandStreamDiffDriven: () => createOIMOrderedListCommandStreamDiffDriven,
@@ -358,21 +374,82 @@ var OIMEventQueueScheduler = class {
358
374
  }
359
375
  };
360
376
 
377
+ // src/core/OIMKeyedBucketSetBased.ts
378
+ var OIMKeyedBucketSetBased = class extends Set {
379
+ constructor(key) {
380
+ super();
381
+ this.key = key;
382
+ }
383
+ hasSubscribers() {
384
+ return this.subscribers !== void 0 && this.subscribers.size > 0;
385
+ }
386
+ /** Clearing the slot set also clears the pk membership — they stay in sync. */
387
+ clear() {
388
+ super.clear();
389
+ this.membership?.clear();
390
+ }
391
+ };
392
+
361
393
  // src/core/OIMIndexStoreMapDrivenSetBased.ts
362
394
  var OIMIndexStoreMapDrivenSetBased = class extends OIMIndexStoreSetBased {
363
395
  constructor() {
364
396
  super(...arguments);
397
+ // Live buckets (non-empty, or freshly promoted). Enumeration reads these only.
365
398
  this.slots = /* @__PURE__ */ new Map();
399
+ // Empty buckets kept alive solely by their subscribers, so a subscription
400
+ // survives a key going empty → re-filled. Invisible to reads/enumeration.
401
+ this.reservedBuckets = /* @__PURE__ */ new Map();
402
+ }
403
+ getOrReserveBucket(key) {
404
+ const live = this.slots.get(key);
405
+ if (live) return live;
406
+ let reserved = this.reservedBuckets.get(key);
407
+ if (!reserved) {
408
+ reserved = new OIMKeyedBucketSetBased(key);
409
+ this.reservedBuckets.set(key, reserved);
410
+ }
411
+ return reserved;
412
+ }
413
+ findBucket(key) {
414
+ return this.slots.get(key) ?? this.reservedBuckets.get(key);
415
+ }
416
+ retainBucket(bucket) {
417
+ if (bucket.size === 0) return;
418
+ this.reservedBuckets.delete(bucket.key);
419
+ if (!this.slots.has(bucket.key)) this.slots.set(bucket.key, bucket);
420
+ }
421
+ releaseBucket(bucket) {
422
+ if (bucket.size > 0) return;
423
+ this.slots.delete(bucket.key);
424
+ if (bucket.hasSubscribers()) {
425
+ this.reservedBuckets.set(bucket.key, bucket);
426
+ } else {
427
+ this.reservedBuckets.delete(bucket.key);
428
+ }
429
+ }
430
+ dropIfReserved(key) {
431
+ const reserved = this.reservedBuckets.get(key);
432
+ if (reserved && reserved.size === 0 && !reserved.hasSubscribers()) {
433
+ this.reservedBuckets.delete(key);
434
+ }
366
435
  }
367
436
  setOneByKey(key, slots) {
368
- this.slots.set(key, slots);
437
+ const bucket = this.getOrReserveBucket(key);
438
+ if (bucket !== slots) {
439
+ bucket.clear();
440
+ for (const slot of slots) bucket.add(slot);
441
+ }
442
+ if (bucket.size > 0) this.retainBucket(bucket);
443
+ else this.releaseBucket(bucket);
369
444
  }
370
445
  removeOneByKey(key) {
371
446
  this.slots.delete(key);
447
+ this.reservedBuckets.delete(key);
372
448
  }
373
449
  removeManyByKeys(keys) {
374
450
  for (const key of keys) {
375
451
  this.slots.delete(key);
452
+ this.reservedBuckets.delete(key);
376
453
  }
377
454
  }
378
455
  getOneByKey(key) {
@@ -381,10 +458,8 @@ var OIMIndexStoreMapDrivenSetBased = class extends OIMIndexStoreSetBased {
381
458
  getManyByKeys(keys) {
382
459
  const result = /* @__PURE__ */ new Map();
383
460
  for (const key of keys) {
384
- const slots = this.getOneByKey(key);
385
- if (slots !== void 0) {
386
- result.set(key, slots);
387
- }
461
+ const slots = this.slots.get(key);
462
+ if (slots !== void 0) result.set(key, slots);
388
463
  }
389
464
  return result;
390
465
  }
@@ -398,6 +473,12 @@ var OIMIndexStoreMapDrivenSetBased = class extends OIMIndexStoreSetBased {
398
473
  return this.slots.size;
399
474
  }
400
475
  clear() {
476
+ for (const bucket of this.slots.values()) {
477
+ bucket.clear();
478
+ if (bucket.hasSubscribers()) {
479
+ this.reservedBuckets.set(bucket.key, bucket);
480
+ }
481
+ }
401
482
  this.slots.clear();
402
483
  }
403
484
  };
@@ -405,10 +486,9 @@ var OIMIndexStoreMapDrivenSetBased = class extends OIMIndexStoreSetBased {
405
486
  // src/abstract/OIMIndexSetBased.ts
406
487
  var OIMIndexSetBased = class extends OIMIndex {
407
488
  constructor(options = {}) {
408
- super(
409
- options.store ?? new OIMIndexStoreMapDrivenSetBased(),
410
- options.comparePks
411
- );
489
+ const store = options.store ?? new OIMIndexStoreMapDrivenSetBased();
490
+ super(store, options.comparePks);
491
+ this.setStore = store;
412
492
  }
413
493
  getBucketSize(bucket) {
414
494
  return bucket.size;
@@ -436,22 +516,36 @@ var OIMIndexSetBased = class extends OIMIndex {
436
516
  for (const slot of slots) pks.add(slot.pk);
437
517
  return pks;
438
518
  }
439
- setSlotsWithComparison(key, newSlots) {
440
- if (this.comparePks) {
441
- const existingSlotsSet = this.store.getOneByKey(key);
442
- if (existingSlotsSet && existingSlotsSet.size === newSlots.size) {
443
- if (this.comparePks(
444
- Array.from(this.slotsToPks(existingSlotsSet)),
445
- Array.from(this.slotsToPks(newSlots))
446
- )) {
447
- return false;
448
- }
449
- } else if (!existingSlotsSet && newSlots.size === 0) {
450
- return false;
451
- }
452
- }
453
- this.store.setOneByKey(key, newSlots);
454
- return true;
519
+ /**
520
+ * True if a comparator is set and considers `bucket`'s current pks equal to
521
+ * `newSlots` — i.e. a `setSlots` would be a no-op and should not emit.
522
+ */
523
+ bucketMatchesComparator(bucket, newSlots) {
524
+ if (!this.comparePks) return false;
525
+ if (bucket.size !== newSlots.size) return false;
526
+ if (bucket.size === 0) return true;
527
+ return this.comparePks(
528
+ Array.from(this.slotsToPks(bucket)),
529
+ Array.from(this.slotsToPks(newSlots))
530
+ );
531
+ }
532
+ };
533
+
534
+ // src/core/OIMKeyedBucketArrayBased.ts
535
+ var OIMKeyedBucketArrayBased = class extends Array {
536
+ constructor(key) {
537
+ super();
538
+ this.key = key;
539
+ }
540
+ static get [Symbol.species]() {
541
+ return Array;
542
+ }
543
+ hasSubscribers() {
544
+ return this.subscribers !== void 0 && this.subscribers.size > 0;
545
+ }
546
+ /** Empty the bucket in place (Array has no `clear`). */
547
+ clear() {
548
+ this.length = 0;
455
549
  }
456
550
  };
457
551
 
@@ -459,17 +553,61 @@ var OIMIndexSetBased = class extends OIMIndex {
459
553
  var OIMIndexStoreMapDrivenArrayBased = class extends OIMIndexStoreArrayBased {
460
554
  constructor() {
461
555
  super(...arguments);
556
+ // Live buckets (non-empty). Enumeration reads these only.
462
557
  this.slots = /* @__PURE__ */ new Map();
558
+ // Empty buckets kept alive by their subscribers, invisible to reads.
559
+ this.reservedBuckets = /* @__PURE__ */ new Map();
560
+ }
561
+ getOrReserveBucket(key) {
562
+ const live = this.slots.get(key);
563
+ if (live) return live;
564
+ let reserved = this.reservedBuckets.get(key);
565
+ if (!reserved) {
566
+ reserved = new OIMKeyedBucketArrayBased(key);
567
+ this.reservedBuckets.set(key, reserved);
568
+ }
569
+ return reserved;
570
+ }
571
+ findBucket(key) {
572
+ return this.slots.get(key) ?? this.reservedBuckets.get(key);
573
+ }
574
+ retainBucket(bucket) {
575
+ if (bucket.length === 0) return;
576
+ this.reservedBuckets.delete(bucket.key);
577
+ if (!this.slots.has(bucket.key)) this.slots.set(bucket.key, bucket);
578
+ }
579
+ releaseBucket(bucket) {
580
+ if (bucket.length > 0) return;
581
+ this.slots.delete(bucket.key);
582
+ if (bucket.hasSubscribers()) {
583
+ this.reservedBuckets.set(bucket.key, bucket);
584
+ } else {
585
+ this.reservedBuckets.delete(bucket.key);
586
+ }
587
+ }
588
+ dropIfReserved(key) {
589
+ const reserved = this.reservedBuckets.get(key);
590
+ if (reserved && reserved.length === 0 && !reserved.hasSubscribers()) {
591
+ this.reservedBuckets.delete(key);
592
+ }
463
593
  }
464
594
  setOneByKey(key, slots) {
465
- this.slots.set(key, slots);
595
+ const bucket = this.getOrReserveBucket(key);
596
+ if (bucket !== slots) {
597
+ bucket.length = 0;
598
+ for (let i = 0; i < slots.length; i++) bucket.push(slots[i]);
599
+ }
600
+ if (bucket.length > 0) this.retainBucket(bucket);
601
+ else this.releaseBucket(bucket);
466
602
  }
467
603
  removeOneByKey(key) {
468
604
  this.slots.delete(key);
605
+ this.reservedBuckets.delete(key);
469
606
  }
470
607
  removeManyByKeys(keys) {
471
608
  for (const key of keys) {
472
609
  this.slots.delete(key);
610
+ this.reservedBuckets.delete(key);
473
611
  }
474
612
  }
475
613
  getOneByKey(key) {
@@ -478,10 +616,8 @@ var OIMIndexStoreMapDrivenArrayBased = class extends OIMIndexStoreArrayBased {
478
616
  getManyByKeys(keys) {
479
617
  const result = /* @__PURE__ */ new Map();
480
618
  for (const key of keys) {
481
- const slots = this.getOneByKey(key);
482
- if (slots !== void 0) {
483
- result.set(key, slots);
484
- }
619
+ const slots = this.slots.get(key);
620
+ if (slots !== void 0) result.set(key, slots);
485
621
  }
486
622
  return result;
487
623
  }
@@ -495,6 +631,12 @@ var OIMIndexStoreMapDrivenArrayBased = class extends OIMIndexStoreArrayBased {
495
631
  return this.slots.size;
496
632
  }
497
633
  clear() {
634
+ for (const bucket of this.slots.values()) {
635
+ bucket.length = 0;
636
+ if (bucket.hasSubscribers()) {
637
+ this.reservedBuckets.set(bucket.key, bucket);
638
+ }
639
+ }
498
640
  this.slots.clear();
499
641
  }
500
642
  };
@@ -502,10 +644,9 @@ var OIMIndexStoreMapDrivenArrayBased = class extends OIMIndexStoreArrayBased {
502
644
  // src/abstract/OIMIndexArrayBased.ts
503
645
  var OIMIndexArrayBased = class extends OIMIndex {
504
646
  constructor(options = {}) {
505
- super(
506
- options.store ?? new OIMIndexStoreMapDrivenArrayBased(),
507
- options.comparePks
508
- );
647
+ const store = options.store ?? new OIMIndexStoreMapDrivenArrayBased();
648
+ super(store, options.comparePks);
649
+ this.arrayStore = store;
509
650
  }
510
651
  getBucketSize(bucket) {
511
652
  return bucket.length;
@@ -523,10 +664,15 @@ var OIMIndexArrayBased = class extends OIMIndex {
523
664
  return result;
524
665
  }
525
666
  getSlotsByKey(key) {
526
- return this.store.getOneByKey(key) ?? [];
667
+ const bucket = this.store.getOneByKey(key);
668
+ return bucket ? bucket.slice() : [];
527
669
  }
528
670
  getSlotsByKeys(keys) {
529
- return this.store.getManyByKeys(keys);
671
+ const result = /* @__PURE__ */ new Map();
672
+ for (const [key, bucket] of this.store.getManyByKeys(keys)) {
673
+ result.set(key, bucket.slice());
674
+ }
675
+ return result;
530
676
  }
531
677
  slotsToPks(slots) {
532
678
  const pks = [];
@@ -534,22 +680,18 @@ var OIMIndexArrayBased = class extends OIMIndex {
534
680
  for (let i = 0; i < slots.length; i++) pks[i] = slots[i].pk;
535
681
  return pks;
536
682
  }
537
- setSlotsWithComparison(key, newSlots) {
538
- if (this.comparePks) {
539
- const existingSlotsArray = this.store.getOneByKey(key);
540
- if (existingSlotsArray && existingSlotsArray.length === newSlots.length) {
541
- if (this.comparePks(
542
- this.slotsToPks(existingSlotsArray),
543
- this.slotsToPks(newSlots)
544
- )) {
545
- return false;
546
- }
547
- } else if (!existingSlotsArray && newSlots.length === 0) {
548
- return false;
549
- }
550
- }
551
- this.store.setOneByKey(key, newSlots);
552
- return true;
683
+ /**
684
+ * True if a comparator is set and considers `bucket`'s pks equal to
685
+ * `newSlots` — i.e. a `setSlots` would be a no-op and should not emit.
686
+ */
687
+ bucketMatchesComparator(bucket, newSlots) {
688
+ if (!this.comparePks) return false;
689
+ if (bucket.length !== newSlots.length) return false;
690
+ if (bucket.length === 0) return true;
691
+ return this.comparePks(
692
+ this.slotsToPks(bucket),
693
+ this.slotsToPks(newSlots)
694
+ );
553
695
  }
554
696
  };
555
697
 
@@ -638,11 +780,19 @@ var OIMCarrierKeyedEmitter = class {
638
780
  this.scheduleFlush();
639
781
  }
640
782
  markUpdatedKey(key) {
783
+ if (this.subscribedCarriers.size === 0) {
784
+ this.assertNotInFlush();
785
+ return;
786
+ }
641
787
  const carrier = this.resolver.findCarrier(key);
642
788
  if (carrier) this.markUpdatedCarrier(carrier);
643
789
  else this.assertNotInFlush();
644
790
  }
645
791
  markUpdatedKeys(keys) {
792
+ if (this.subscribedCarriers.size === 0) {
793
+ this.assertNotInFlush();
794
+ return;
795
+ }
646
796
  for (let i = 0; i < keys.length; i++) this.markUpdatedKey(keys[i]);
647
797
  }
648
798
  markAllUpdated() {
@@ -742,8 +892,8 @@ var OIMKeyedCarrierResolver = class {
742
892
 
743
893
  // src/abstract/OIMReactiveIndex.ts
744
894
  var OIMReactiveIndex = class {
745
- constructor(queue, createIndex) {
746
- this.updateEmitter = new OIMCarrierKeyedEmitter(queue, new OIMKeyedCarrierResolver());
895
+ constructor(queue, createIndex, createResolver = () => new OIMKeyedCarrierResolver()) {
896
+ this.updateEmitter = new OIMCarrierKeyedEmitter(queue, createResolver());
747
897
  this.index = createIndex(this.updateEmitter);
748
898
  }
749
899
  getEntitiesByKey(key) {
@@ -799,8 +949,8 @@ var OIMReactiveIndex = class {
799
949
 
800
950
  // src/abstract/OIMReactiveIndexSetBased.ts
801
951
  var OIMReactiveIndexSetBased = class extends OIMReactiveIndex {
802
- constructor(queue, createIndex) {
803
- super(queue, createIndex);
952
+ constructor(queue, createIndex, createResolver) {
953
+ super(queue, createIndex, createResolver);
804
954
  }
805
955
  getPksByKey(key) {
806
956
  return this.index.getPksByKey(key);
@@ -818,8 +968,8 @@ var OIMReactiveIndexSetBased = class extends OIMReactiveIndex {
818
968
 
819
969
  // src/abstract/OIMReactiveIndexArrayBased.ts
820
970
  var OIMReactiveIndexArrayBased = class extends OIMReactiveIndex {
821
- constructor(queue, createIndex) {
822
- super(queue, createIndex);
971
+ constructor(queue, createIndex, createResolver) {
972
+ super(queue, createIndex, createResolver);
823
973
  }
824
974
  getPksByKey(key) {
825
975
  return this.index.getPksByKey(key);
@@ -1094,10 +1244,24 @@ var OIMPkSelectorFactory = class {
1094
1244
  }
1095
1245
  };
1096
1246
 
1247
+ // src/core/OIMKeyDomainNative.ts
1248
+ var OIMKeyDomainNative = class {
1249
+ createMap() {
1250
+ return /* @__PURE__ */ new Map();
1251
+ }
1252
+ createSet() {
1253
+ return /* @__PURE__ */ new Set();
1254
+ }
1255
+ canonicalize(key) {
1256
+ return key;
1257
+ }
1258
+ };
1259
+
1097
1260
  // src/core/OIMCollectionStoreMapDriven.ts
1098
1261
  var OIMCollectionStoreMapDriven = class extends OIMCollectionStore {
1099
1262
  constructor() {
1100
1263
  super(...arguments);
1264
+ this.keyDomain = new OIMKeyDomainNative();
1101
1265
  this.slots = /* @__PURE__ */ new Map();
1102
1266
  // Slots reserved by indexes for PKs whose entity has not arrived yet.
1103
1267
  // They carry `item: undefined` and live apart from `slots` so they do not
@@ -1270,6 +1434,10 @@ var OIMCollection = class {
1270
1434
  this.store = opts?.store ?? new OIMCollectionStoreMapDriven();
1271
1435
  this.updateEntity = opts?.updateEntity ?? new OIMEntityUpdaterFactory().createMergeEntityUpdater();
1272
1436
  }
1437
+ /** The collection's PK keying strategy — read by indexes and wrappers. */
1438
+ get keyDomain() {
1439
+ return this.store.keyDomain;
1440
+ }
1273
1441
  getOneByPk(pk) {
1274
1442
  return this.store.getOneByPk(pk);
1275
1443
  }
@@ -1350,15 +1518,12 @@ var OIMCollection = class {
1350
1518
  `[OIMCollection]: PK is required to upsert an entity ${JSON.stringify(entity)}`
1351
1519
  );
1352
1520
  }
1353
- const existingEntity = this.store.getOneByPk(pk);
1354
- if (existingEntity) {
1355
- return this.store.setOneByPk(
1356
- pk,
1357
- this.updateEntity(entity, existingEntity)
1358
- );
1359
- } else {
1360
- return this.store.setOneByPk(pk, entity);
1521
+ const slot = this.store.getSlotByPk(pk);
1522
+ if (slot !== void 0 && slot.item !== void 0) {
1523
+ slot.item = this.updateEntity(entity, slot.item);
1524
+ return slot;
1361
1525
  }
1526
+ return this.store.setOneByPk(pk, entity);
1362
1527
  }
1363
1528
  destroy() {
1364
1529
  this.store.destroy();
@@ -1371,13 +1536,13 @@ var OIMReactiveCollection = class extends OIMCollection {
1371
1536
  constructor(queue, opts) {
1372
1537
  super(opts);
1373
1538
  this.anyUpdateHandlers = /* @__PURE__ */ new Set();
1374
- this.pendingAnyUpdatePks = /* @__PURE__ */ new Set();
1539
+ this.pendingAnyUpdatePks = this.store.keyDomain.createSet();
1375
1540
  this.isAnyUpdateClearPending = false;
1376
1541
  this.isAnyUpdateScheduled = false;
1377
1542
  this.runAnyUpdate = () => {
1378
1543
  if (!this.isAnyUpdateScheduled) return;
1379
1544
  this.isAnyUpdateScheduled = false;
1380
- const pks = this.isAnyUpdateClearPending ? [] : Array.from(this.pendingAnyUpdatePks);
1545
+ const pks = this.isAnyUpdateClearPending ? [] : Array.from(this.pendingAnyUpdatePks.values());
1381
1546
  this.pendingAnyUpdatePks.clear();
1382
1547
  this.isAnyUpdateClearPending = false;
1383
1548
  const snapshot = Array.from(this.anyUpdateHandlers);
@@ -1536,38 +1701,40 @@ var OIMIndexManualOrderedArrayBased = class extends OIMIndexArrayBased {
1536
1701
  if (key === void 0) {
1537
1702
  const keys = this.getKeys();
1538
1703
  if (keys.length > 0) {
1539
- this.store.clear();
1704
+ this.arrayStore.clear();
1540
1705
  this.emitUpdate(Array.from(keys));
1541
1706
  }
1542
1707
  return;
1543
1708
  }
1544
- if (this.store.getOneByKey(key) !== void 0) {
1545
- this.store.removeOneByKey(key);
1709
+ const bucket = this.arrayStore.findBucket(key);
1710
+ if (bucket && bucket.length > 0) {
1711
+ bucket.length = 0;
1712
+ this.arrayStore.releaseBucket(bucket);
1546
1713
  this.emitUpdate([key]);
1547
1714
  }
1548
1715
  }
1549
1716
  pushSlot(key, slot) {
1550
- const list = this.getOrCreateList(key);
1551
- const index = list.length;
1552
- list.push(slot);
1717
+ const bucket = this.arrayStore.getOrReserveBucket(key);
1718
+ const index = bucket.length;
1719
+ bucket.push(slot);
1720
+ this.arrayStore.retainBucket(bucket);
1553
1721
  this.emitUpdate([key]);
1554
1722
  return index;
1555
1723
  }
1556
1724
  insertSlotAt(key, index, slot) {
1557
- const list = this.getOrCreateList(key);
1558
- const safeIndex = Math.max(0, Math.min(index, list.length));
1559
- list.splice(safeIndex, 0, slot);
1725
+ const bucket = this.arrayStore.getOrReserveBucket(key);
1726
+ const safeIndex = Math.max(0, Math.min(index, bucket.length));
1727
+ bucket.splice(safeIndex, 0, slot);
1728
+ this.arrayStore.retainBucket(bucket);
1560
1729
  this.emitUpdate([key]);
1561
1730
  return safeIndex;
1562
1731
  }
1563
1732
  removeAt(key, index) {
1564
- const list = this.store.getOneByKey(key);
1565
- if (!list) return void 0;
1566
- if (index < 0 || index >= list.length) return void 0;
1567
- const [removed] = list.splice(index, 1);
1568
- if (list.length === 0) {
1569
- this.store.removeOneByKey(key);
1570
- }
1733
+ const bucket = this.arrayStore.findBucket(key);
1734
+ if (!bucket) return void 0;
1735
+ if (index < 0 || index >= bucket.length) return void 0;
1736
+ const [removed] = bucket.splice(index, 1);
1737
+ if (bucket.length === 0) this.arrayStore.releaseBucket(bucket);
1571
1738
  this.emitUpdate([key]);
1572
1739
  return removed;
1573
1740
  }
@@ -1576,35 +1743,33 @@ var OIMIndexManualOrderedArrayBased = class extends OIMIndexArrayBased {
1576
1743
  * Returns the number actually removed (clamped to bounds; 0 if nothing).
1577
1744
  */
1578
1745
  removeRange(key, index, count) {
1579
- const list = this.store.getOneByKey(key);
1580
- if (!list) return 0;
1581
- if (index < 0 || index >= list.length || count <= 0) return 0;
1582
- const actual = Math.min(count, list.length - index);
1583
- list.splice(index, actual);
1584
- if (list.length === 0) {
1585
- this.store.removeOneByKey(key);
1586
- }
1746
+ const bucket = this.arrayStore.findBucket(key);
1747
+ if (!bucket) return 0;
1748
+ if (index < 0 || index >= bucket.length || count <= 0) return 0;
1749
+ const actual = Math.min(count, bucket.length - index);
1750
+ bucket.splice(index, actual);
1751
+ if (bucket.length === 0) this.arrayStore.releaseBucket(bucket);
1587
1752
  this.emitUpdate([key]);
1588
1753
  return actual;
1589
1754
  }
1590
1755
  move(key, fromIndex, toIndex) {
1591
- const list = this.store.getOneByKey(key);
1592
- if (!list) return void 0;
1593
- if (fromIndex < 0 || fromIndex >= list.length) return void 0;
1594
- if (toIndex < 0 || toIndex >= list.length) return void 0;
1595
- if (fromIndex === toIndex) return list[fromIndex];
1596
- const [slot] = list.splice(fromIndex, 1);
1756
+ const bucket = this.arrayStore.findBucket(key);
1757
+ if (!bucket) return void 0;
1758
+ if (fromIndex < 0 || fromIndex >= bucket.length) return void 0;
1759
+ if (toIndex < 0 || toIndex >= bucket.length) return void 0;
1760
+ if (fromIndex === toIndex) return bucket[fromIndex];
1761
+ const [slot] = bucket.splice(fromIndex, 1);
1597
1762
  if (slot === void 0) return void 0;
1598
- list.splice(toIndex, 0, slot);
1763
+ bucket.splice(toIndex, 0, slot);
1599
1764
  this.emitUpdate([key]);
1600
1765
  return slot;
1601
1766
  }
1602
1767
  /** Replace the slot at `index` in place. Returns the safe index, or -1. */
1603
1768
  setSlotAt(key, index, slot) {
1604
- const list = this.store.getOneByKey(key);
1605
- if (!list) return -1;
1606
- if (index < 0 || index >= list.length) return -1;
1607
- list[index] = slot;
1769
+ const bucket = this.arrayStore.findBucket(key);
1770
+ if (!bucket) return -1;
1771
+ if (index < 0 || index >= bucket.length) return -1;
1772
+ bucket[index] = slot;
1608
1773
  this.emitUpdate([key]);
1609
1774
  return index;
1610
1775
  }
@@ -1614,36 +1779,33 @@ var OIMIndexManualOrderedArrayBased = class extends OIMIndexArrayBased {
1614
1779
  * Returns the number actually moved (0 if a no-op / out of bounds).
1615
1780
  */
1616
1781
  moveRange(key, from, to, count) {
1617
- const list = this.store.getOneByKey(key);
1618
- if (!list) return 0;
1619
- if (from < 0 || from >= list.length || count <= 0) return 0;
1620
- const actual = Math.min(count, list.length - from);
1621
- const insertAt = Math.max(0, Math.min(to, list.length - actual));
1782
+ const bucket = this.arrayStore.findBucket(key);
1783
+ if (!bucket) return 0;
1784
+ if (from < 0 || from >= bucket.length || count <= 0) return 0;
1785
+ const actual = Math.min(count, bucket.length - from);
1786
+ const insertAt = Math.max(0, Math.min(to, bucket.length - actual));
1622
1787
  if (insertAt === from) return 0;
1623
- const block = list.splice(from, actual);
1624
- list.splice(insertAt, 0, ...block);
1788
+ const block = bucket.splice(from, actual);
1789
+ bucket.splice(insertAt, 0, ...block);
1625
1790
  this.emitUpdate([key]);
1626
1791
  return actual;
1627
1792
  }
1628
1793
  resetSlots(key, slots) {
1629
1794
  if (slots.length === 0) {
1630
- if (this.store.getOneByKey(key) !== void 0) {
1631
- this.store.removeOneByKey(key);
1795
+ const bucket2 = this.arrayStore.findBucket(key);
1796
+ if (bucket2 && bucket2.length > 0) {
1797
+ bucket2.length = 0;
1798
+ this.arrayStore.releaseBucket(bucket2);
1632
1799
  this.emitUpdate([key]);
1633
1800
  }
1634
1801
  return;
1635
1802
  }
1636
- this.store.setOneByKey(key, slots.slice());
1803
+ const bucket = this.arrayStore.getOrReserveBucket(key);
1804
+ bucket.length = 0;
1805
+ for (let i = 0; i < slots.length; i++) bucket.push(slots[i]);
1806
+ this.arrayStore.retainBucket(bucket);
1637
1807
  this.emitUpdate([key]);
1638
1808
  }
1639
- getOrCreateList(key) {
1640
- let list = this.store.getOneByKey(key);
1641
- if (!list) {
1642
- list = [];
1643
- this.store.setOneByKey(key, list);
1644
- }
1645
- return list;
1646
- }
1647
1809
  };
1648
1810
 
1649
1811
  // src/core/OIMCollectionIndexManualOrderedArrayBased.ts
@@ -2161,43 +2323,68 @@ var OIMIndexManualArrayBased = class extends OIMIndexArrayBased {
2161
2323
  super(options);
2162
2324
  }
2163
2325
  setSlots(key, slots) {
2164
- const hasChanges = this.setSlotsWithComparison(key, slots);
2165
- if (hasChanges) {
2166
- this.emitUpdateOne(key);
2167
- }
2326
+ const bucket = this.arrayStore.getOrReserveBucket(key);
2327
+ if (this.bucketMatchesComparator(bucket, slots)) return;
2328
+ bucket.length = 0;
2329
+ for (let i = 0; i < slots.length; i++) bucket.push(slots[i]);
2330
+ if (bucket.length > 0) this.arrayStore.retainBucket(bucket);
2331
+ else this.arrayStore.releaseBucket(bucket);
2332
+ this.emitBucketChanged(bucket);
2168
2333
  }
2169
2334
  /**
2170
- * Appends pre-deduplicated slots to a key's bucket IN PLACE (no copy of the
2171
- * existing bucket) and emits once. Callers own dedup (so this stays O(added),
2172
- * not O(bucket)); used by collection-bound indexes for fast `addPks`.
2335
+ * Appends pre-deduplicated slots to a key's bucket IN PLACE and emits once.
2336
+ * O(added); callers own dedup.
2173
2337
  */
2174
2338
  appendSlots(key, slots) {
2175
2339
  if (slots.length === 0) return;
2176
- let bucket = this.store.getOneByKey(key);
2177
- if (!bucket) {
2178
- bucket = [];
2179
- this.store.setOneByKey(key, bucket);
2180
- }
2340
+ const bucket = this.arrayStore.getOrReserveBucket(key);
2181
2341
  for (let i = 0; i < slots.length; i++) bucket.push(slots[i]);
2182
- this.emitUpdateOne(key);
2342
+ this.arrayStore.retainBucket(bucket);
2343
+ this.emitBucketChanged(bucket);
2183
2344
  }
2184
2345
  /**
2185
- * Clear all primary keys for a specific index key, or all keys if no key specified
2346
+ * Clear all primary keys for a specific index key, or all keys if none given.
2186
2347
  */
2187
2348
  clear(key) {
2188
2349
  if (key === void 0) {
2189
- const allKeys = this.store.getAllKeys();
2350
+ const allKeys = this.arrayStore.getAllKeys();
2190
2351
  if (allKeys.length > 0) {
2191
- this.store.clear();
2352
+ this.arrayStore.clear();
2192
2353
  this.emitUpdate(allKeys);
2193
2354
  }
2194
2355
  } else {
2195
- if (this.store.getOneByKey(key) !== void 0) {
2196
- this.store.removeOneByKey(key);
2197
- this.emitUpdateOne(key);
2356
+ const bucket = this.arrayStore.findBucket(key);
2357
+ if (bucket && bucket.length > 0) {
2358
+ bucket.length = 0;
2359
+ this.arrayStore.releaseBucket(bucket);
2360
+ this.emitBucketChanged(bucket);
2198
2361
  }
2199
2362
  }
2200
2363
  }
2364
+ /**
2365
+ * Emit that a bucket changed. Base (non-reactive) path emits by key on the
2366
+ * plain event emitter; the reactive subclass overrides this to mark the
2367
+ * bucket carrier directly (O(1), no key→carrier lookup).
2368
+ */
2369
+ emitBucketChanged(bucket) {
2370
+ this.emitUpdateOne(bucket.key);
2371
+ }
2372
+ };
2373
+
2374
+ // src/core/OIMBucketCarrierResolver.ts
2375
+ var OIMBucketCarrierResolver = class {
2376
+ constructor(store) {
2377
+ this.store = store;
2378
+ }
2379
+ getOrReserveCarrier(key) {
2380
+ return this.store.getOrReserveBucket(key);
2381
+ }
2382
+ findCarrier(key) {
2383
+ return this.store.findBucket(key);
2384
+ }
2385
+ onCarrierEmptied(carrier) {
2386
+ this.store.dropIfReserved(carrier.key);
2387
+ }
2201
2388
  };
2202
2389
 
2203
2390
  // src/core/OIMReactiveIndexManualArrayBased.ts
@@ -2206,6 +2393,10 @@ var OIMIndexManualArrayBasedReactive = class extends OIMIndexManualArrayBased {
2206
2393
  super(opts);
2207
2394
  this.updateEmitter = updateEmitter;
2208
2395
  }
2396
+ // Deliver straight off the bucket the write just touched — O(1).
2397
+ emitBucketChanged(bucket) {
2398
+ this.updateEmitter.markUpdatedCarrier(bucket);
2399
+ }
2209
2400
  emitUpdate(keys) {
2210
2401
  this.updateEmitter.markUpdatedKeys(keys);
2211
2402
  }
@@ -2215,12 +2406,15 @@ var OIMIndexManualArrayBasedReactive = class extends OIMIndexManualArrayBased {
2215
2406
  };
2216
2407
  var OIMReactiveIndexManualArrayBased = class extends OIMReactiveIndexArrayBased {
2217
2408
  constructor(queue, opts) {
2218
- super(queue, (updateEmitter) => {
2219
- return new OIMIndexManualArrayBasedReactive(
2220
- updateEmitter,
2221
- opts?.indexOptions
2222
- );
2223
- });
2409
+ const store = opts?.indexOptions?.store ?? new OIMIndexStoreMapDrivenArrayBased();
2410
+ super(
2411
+ queue,
2412
+ (updateEmitter) => new OIMIndexManualArrayBasedReactive(updateEmitter, {
2413
+ comparePks: opts?.indexOptions?.comparePks,
2414
+ store
2415
+ }),
2416
+ () => new OIMBucketCarrierResolver(store)
2417
+ );
2224
2418
  }
2225
2419
  setSlots(key, slots) {
2226
2420
  this.index.setSlots(key, slots);
@@ -2237,6 +2431,7 @@ var OIMReactiveIndexManualArrayBased = class extends OIMReactiveIndexArrayBased
2237
2431
  var OIMReactiveCollectionIndexManualArrayBased = class extends OIMReactiveIndexManualArrayBased {
2238
2432
  constructor(queue, opts) {
2239
2433
  const resolveSlot = opts.collection !== void 0 ? (pk) => opts.collection.getOrReserveSlotByPk(pk) : opts.resolveSlot;
2434
+ const pkDomain = opts.collection?.keyDomain ?? new OIMKeyDomainNative();
2240
2435
  super(queue, {
2241
2436
  indexOptions: opts.indexOptions
2242
2437
  });
@@ -2244,9 +2439,10 @@ var OIMReactiveCollectionIndexManualArrayBased = class extends OIMReactiveIndexM
2244
2439
  // a Set from the whole bucket on every call). Kept in sync by setSlots/clear.
2245
2440
  this.pksByKey = /* @__PURE__ */ new Map();
2246
2441
  this.resolveSlot = resolveSlot;
2442
+ this.pkDomain = pkDomain;
2247
2443
  }
2248
2444
  setPks(key, pks) {
2249
- const pkSet = /* @__PURE__ */ new Set();
2445
+ const pkSet = this.pkDomain.createSet();
2250
2446
  for (let i = 0; i < pks.length; i++) pkSet.add(pks[i]);
2251
2447
  this.pksByKey.set(key, pkSet);
2252
2448
  super.setSlots(key, this.resolveSlots(pks));
@@ -2289,7 +2485,7 @@ var OIMReactiveCollectionIndexManualArrayBased = class extends OIMReactiveIndexM
2289
2485
  membershipOf(key) {
2290
2486
  let pkSet = this.pksByKey.get(key);
2291
2487
  if (!pkSet) {
2292
- pkSet = /* @__PURE__ */ new Set();
2488
+ pkSet = this.pkDomain.createSet();
2293
2489
  const existing = this.index.getSlotsByKey(key);
2294
2490
  for (let i = 0; i < existing.length; i++) {
2295
2491
  pkSet.add(existing[i].pk);
@@ -2323,24 +2519,98 @@ var OIMReactiveCollectionIndexManualArrayBased = class extends OIMReactiveIndexM
2323
2519
  var OIMIndexManualSetBased = class extends OIMIndexSetBased {
2324
2520
  constructor(options = {}) {
2325
2521
  super(options);
2522
+ this.pkDomain = options.pkDomain ?? new OIMKeyDomainNative();
2326
2523
  }
2327
2524
  setSlots(key, slots) {
2328
- const hasChanges = this.setSlotsWithComparison(key, new Set(slots));
2329
- if (hasChanges) {
2330
- this.emitUpdateOne(key);
2525
+ const bucket = this.setStore.getOrReserveBucket(key);
2526
+ this.replaceBucketContents(bucket, slots);
2527
+ }
2528
+ // --- pk-oriented writes: resolve the bucket ONCE and use its own membership
2529
+ // (pk → slot) for dedup/removal, so a composite (trie) key is walked a single
2530
+ // time instead of once for the bucket and once for a separate membership map.
2531
+ setPks(key, pks, resolveSlot) {
2532
+ const bucket = this.setStore.getOrReserveBucket(key);
2533
+ const slots = [];
2534
+ for (let i = 0; i < pks.length; i++) slots.push(resolveSlot(pks[i]));
2535
+ this.replaceBucketContents(bucket, slots);
2536
+ const membership = bucket.membership ?? (bucket.membership = this.pkDomain.createMap());
2537
+ membership.clear();
2538
+ for (let i = 0; i < pks.length; i++) {
2539
+ if (!membership.has(pks[i])) membership.set(pks[i], slots[i]);
2540
+ }
2541
+ }
2542
+ addPks(key, pks, resolveSlot) {
2543
+ if (pks.length === 0) return;
2544
+ const bucket = this.setStore.getOrReserveBucket(key);
2545
+ const membership = this.membershipOf(bucket);
2546
+ let changed = false;
2547
+ for (let i = 0; i < pks.length; i++) {
2548
+ const pk = pks[i];
2549
+ if (membership.has(pk)) continue;
2550
+ const slot = resolveSlot(pk);
2551
+ membership.set(pk, slot);
2552
+ if (!bucket.has(slot)) {
2553
+ bucket.add(slot);
2554
+ changed = true;
2555
+ }
2556
+ }
2557
+ if (!changed) return;
2558
+ this.setStore.retainBucket(bucket);
2559
+ this.emitBucketChanged(bucket);
2560
+ }
2561
+ removePks(key, pks) {
2562
+ if (pks.length === 0) return;
2563
+ const bucket = this.setStore.findBucket(key);
2564
+ if (!bucket || bucket.size === 0) return;
2565
+ const membership = this.membershipOf(bucket);
2566
+ let changed = false;
2567
+ for (let i = 0; i < pks.length; i++) {
2568
+ const slot = membership.get(pks[i]);
2569
+ if (slot) {
2570
+ membership.delete(pks[i]);
2571
+ if (bucket.delete(slot)) changed = true;
2572
+ }
2573
+ }
2574
+ if (!changed) return;
2575
+ if (bucket.size === 0) this.setStore.releaseBucket(bucket);
2576
+ this.emitBucketChanged(bucket);
2577
+ }
2578
+ /**
2579
+ * The bucket's pk → slot membership, lazily created and seeded from the
2580
+ * bucket's current slots (so it stays correct even if slots were written
2581
+ * through the lower-level `setSlots` path).
2582
+ */
2583
+ membershipOf(bucket) {
2584
+ let membership = bucket.membership;
2585
+ if (!membership) {
2586
+ membership = this.pkDomain.createMap();
2587
+ bucket.membership = membership;
2588
+ }
2589
+ if (membership.size === 0 && bucket.size > 0) {
2590
+ for (const slot of bucket) membership.set(slot.pk, slot);
2591
+ }
2592
+ return membership;
2593
+ }
2594
+ replaceBucketContents(bucket, slots) {
2595
+ if (this.comparePks) {
2596
+ const newSet = slots instanceof Set ? slots : new Set(slots);
2597
+ if (this.bucketMatchesComparator(bucket, newSet)) return;
2598
+ bucket.clear();
2599
+ for (const slot of newSet) bucket.add(slot);
2600
+ } else {
2601
+ bucket.clear();
2602
+ for (const slot of slots) bucket.add(slot);
2331
2603
  }
2604
+ if (bucket.size > 0) this.setStore.retainBucket(bucket);
2605
+ else this.setStore.releaseBucket(bucket);
2606
+ this.emitBucketChanged(bucket);
2332
2607
  }
2333
2608
  /**
2334
- * Adds slots to a key's bucket IN PLACE (no copy of the existing Set) and
2335
- * emits once. O(added). Used by collection-bound indexes for fast `addPks`.
2609
+ * Adds slots to a key's bucket IN PLACE and emits once. O(added).
2336
2610
  */
2337
2611
  addSlots(key, slots) {
2338
2612
  if (slots.length === 0) return;
2339
- let bucket = this.store.getOneByKey(key);
2340
- if (!bucket) {
2341
- bucket = /* @__PURE__ */ new Set();
2342
- this.store.setOneByKey(key, bucket);
2343
- }
2613
+ const bucket = this.setStore.getOrReserveBucket(key);
2344
2614
  let changed = false;
2345
2615
  for (let i = 0; i < slots.length; i++) {
2346
2616
  if (!bucket.has(slots[i])) {
@@ -2348,25 +2618,27 @@ var OIMIndexManualSetBased = class extends OIMIndexSetBased {
2348
2618
  changed = true;
2349
2619
  }
2350
2620
  }
2351
- if (changed) this.emitUpdateOne(key);
2621
+ if (!changed) return;
2622
+ this.setStore.retainBucket(bucket);
2623
+ this.emitBucketChanged(bucket);
2352
2624
  }
2353
2625
  /**
2354
2626
  * Removes slots from a key's bucket IN PLACE (O(removed)) and emits once.
2355
2627
  */
2356
2628
  removeSlots(key, slots) {
2357
2629
  if (slots.length === 0) return;
2358
- const bucket = this.store.getOneByKey(key);
2630
+ const bucket = this.setStore.findBucket(key);
2359
2631
  if (!bucket) return;
2360
2632
  let changed = false;
2361
2633
  for (let i = 0; i < slots.length; i++) {
2362
2634
  if (bucket.delete(slots[i])) changed = true;
2363
2635
  }
2364
2636
  if (!changed) return;
2365
- if (bucket.size === 0) this.store.removeOneByKey(key);
2366
- this.emitUpdateOne(key);
2637
+ if (bucket.size === 0) this.setStore.releaseBucket(bucket);
2638
+ this.emitBucketChanged(bucket);
2367
2639
  }
2368
2640
  /**
2369
- * Clear all primary keys for a specific index key, or all keys if no key specified
2641
+ * Clear all primary keys for a specific index key, or all keys if none given.
2370
2642
  */
2371
2643
  clear(key) {
2372
2644
  if (key === void 0) {
@@ -2376,12 +2648,22 @@ var OIMIndexManualSetBased = class extends OIMIndexSetBased {
2376
2648
  this.emitUpdate(allKeys);
2377
2649
  }
2378
2650
  } else {
2379
- if (this.store.getOneByKey(key) !== void 0) {
2380
- this.store.removeOneByKey(key);
2381
- this.emitUpdateOne(key);
2651
+ const bucket = this.setStore.findBucket(key);
2652
+ if (bucket && bucket.size > 0) {
2653
+ bucket.clear();
2654
+ this.setStore.releaseBucket(bucket);
2655
+ this.emitBucketChanged(bucket);
2382
2656
  }
2383
2657
  }
2384
2658
  }
2659
+ /**
2660
+ * Emit that a bucket changed. Base (non-reactive) path emits by key on the
2661
+ * plain event emitter; the reactive subclass overrides this to mark the
2662
+ * bucket carrier directly (O(1), no key→carrier lookup).
2663
+ */
2664
+ emitBucketChanged(bucket) {
2665
+ this.emitUpdateOne(bucket.key);
2666
+ }
2385
2667
  };
2386
2668
 
2387
2669
  // src/core/OIMReactiveIndexManualSetBased.ts
@@ -2390,6 +2672,12 @@ var OIMIndexManualSetBasedReactive = class extends OIMIndexManualSetBased {
2390
2672
  super(opts);
2391
2673
  this.updateEmitter = updateEmitter;
2392
2674
  }
2675
+ // Deliver straight off the bucket the write just touched — O(1), no
2676
+ // key→carrier lookup (the whole point of bucket-as-carrier).
2677
+ emitBucketChanged(bucket) {
2678
+ this.updateEmitter.markUpdatedCarrier(bucket);
2679
+ }
2680
+ // clear(all) still emits by key set (rare path).
2393
2681
  emitUpdate(keys) {
2394
2682
  this.updateEmitter.markUpdatedKeys(keys);
2395
2683
  }
@@ -2399,12 +2687,16 @@ var OIMIndexManualSetBasedReactive = class extends OIMIndexManualSetBased {
2399
2687
  };
2400
2688
  var OIMReactiveIndexManualSetBased = class extends OIMReactiveIndexSetBased {
2401
2689
  constructor(queue, opts) {
2402
- super(queue, (updateEmitter) => {
2403
- return new OIMIndexManualSetBasedReactive(
2404
- updateEmitter,
2405
- opts?.indexOptions
2406
- );
2407
- });
2690
+ const store = opts?.indexOptions?.store ?? new OIMIndexStoreMapDrivenSetBased();
2691
+ super(
2692
+ queue,
2693
+ (updateEmitter) => new OIMIndexManualSetBasedReactive(updateEmitter, {
2694
+ comparePks: opts?.indexOptions?.comparePks,
2695
+ store,
2696
+ pkDomain: opts?.pkDomain
2697
+ }),
2698
+ () => new OIMBucketCarrierResolver(store)
2699
+ );
2408
2700
  }
2409
2701
  setSlots(key, slots) {
2410
2702
  this.index.setSlots(key, slots);
@@ -2425,71 +2717,26 @@ var OIMReactiveCollectionIndexManualSetBased = class extends OIMReactiveIndexMan
2425
2717
  constructor(queue, opts) {
2426
2718
  const resolveSlot = opts.collection !== void 0 ? (pk) => opts.collection.getOrReserveSlotByPk(pk) : opts.resolveSlot;
2427
2719
  super(queue, {
2428
- indexOptions: opts.indexOptions
2720
+ indexOptions: opts.indexOptions,
2721
+ pkDomain: opts.collection?.keyDomain
2429
2722
  });
2430
- // Persistent per-key pk → slot map for O(1) add (dedup) and O(1) remove
2431
- // (slot lookup by pk). Kept in sync by setPks/addPks/removePks/clear.
2432
- this.slotByPk = /* @__PURE__ */ new Map();
2723
+ // Bound once so the hot pk-write path passes a stable function (no per-call
2724
+ // closure allocation).
2725
+ this.resolveRequiredSlot = (pk) => {
2726
+ const slot = this.resolveSlot(pk);
2727
+ if (!slot) return { pk, item: void 0 };
2728
+ return slot;
2729
+ };
2433
2730
  this.resolveSlot = resolveSlot;
2434
2731
  }
2435
2732
  setPks(key, pks) {
2436
- const map = /* @__PURE__ */ new Map();
2437
- const slots = [];
2438
- for (let i = 0; i < pks.length; i++) {
2439
- const pk = pks[i];
2440
- const slot = this.resolveRequiredSlot(pk);
2441
- if (!map.has(pk)) map.set(pk, slot);
2442
- slots.push(slot);
2443
- }
2444
- this.slotByPk.set(key, map);
2445
- super.setSlots(key, slots);
2733
+ this.index.setPks(key, pks, this.resolveRequiredSlot);
2446
2734
  }
2447
2735
  addPks(key, pks) {
2448
- if (pks.length === 0) return;
2449
- const map = this.membershipOf(key);
2450
- const newSlots = [];
2451
- for (const pk of pks) {
2452
- if (map.has(pk)) continue;
2453
- const slot = this.resolveRequiredSlot(pk);
2454
- map.set(pk, slot);
2455
- newSlots.push(slot);
2456
- }
2457
- if (newSlots.length > 0) this.addSlots(key, newSlots);
2736
+ this.index.addPks(key, pks, this.resolveRequiredSlot);
2458
2737
  }
2459
2738
  removePks(key, pks) {
2460
- if (pks.length === 0) return;
2461
- const map = this.membershipOf(key);
2462
- if (map.size === 0) return;
2463
- const removed = [];
2464
- for (const pk of pks) {
2465
- const slot = map.get(pk);
2466
- if (slot) {
2467
- map.delete(pk);
2468
- removed.push(slot);
2469
- }
2470
- }
2471
- if (removed.length === 0) return;
2472
- if (map.size === 0) this.slotByPk.delete(key);
2473
- this.removeSlots(key, removed);
2474
- }
2475
- clear(key) {
2476
- if (key === void 0) this.slotByPk.clear();
2477
- else this.slotByPk.delete(key);
2478
- super.clear(key);
2479
- }
2480
- /**
2481
- * The pk→slot map for a key, lazily seeded from the current bucket so it
2482
- * stays correct even if slots were set through a lower-level path.
2483
- */
2484
- membershipOf(key) {
2485
- let map = this.slotByPk.get(key);
2486
- if (!map) {
2487
- map = /* @__PURE__ */ new Map();
2488
- const existing = this.index.getSlotsByKey(key);
2489
- for (const slot of existing) map.set(slot.pk, slot);
2490
- this.slotByPk.set(key, map);
2491
- }
2492
- return map;
2739
+ this.index.removePks(key, pks);
2493
2740
  }
2494
2741
  getEntitiesByKey(key) {
2495
2742
  return super.getEntitiesByKey(key);
@@ -2497,50 +2744,459 @@ var OIMReactiveCollectionIndexManualSetBased = class extends OIMReactiveIndexMan
2497
2744
  getEntitiesByKeys(keys) {
2498
2745
  return super.getEntitiesByKeys(keys);
2499
2746
  }
2500
- resolveRequiredSlot(pk) {
2501
- const slot = this.resolveSlot(pk);
2502
- if (!slot) return { pk, item: void 0 };
2503
- return slot;
2504
- }
2505
2747
  };
2506
2748
 
2507
- // src/core/OIMDerivedCollectionIndexSetBased.ts
2508
- var OIMDerivedCollectionIndexSetBased = class extends OIMReactiveCollectionIndexManualSetBased {
2509
- constructor(queue, collection, opts) {
2510
- super(queue, {
2511
- collection,
2512
- indexOptions: opts.indexOptions
2513
- });
2514
- this.keysByPk = /* @__PURE__ */ new Map();
2515
- this.onCollectionUpdate = (pks) => {
2516
- if (pks.length === 0) {
2517
- this.rebuildFromCollection();
2518
- return;
2749
+ // src/core/OIMTrieMap.ts
2750
+ var OIMTrieMap = class {
2751
+ constructor() {
2752
+ this.root = { hasValue: false };
2753
+ this._size = 0;
2754
+ }
2755
+ /** Number of stored paths (terminal nodes), not the number of trie nodes. */
2756
+ get size() {
2757
+ return this._size;
2758
+ }
2759
+ set(path, value) {
2760
+ let node = this.root;
2761
+ for (let i = 0; i < path.length; i++) {
2762
+ let children = node.children;
2763
+ if (!children) {
2764
+ children = /* @__PURE__ */ new Map();
2765
+ node.children = children;
2519
2766
  }
2520
- for (const pk of pks) {
2521
- this.syncPk(pk);
2767
+ let next = children.get(path[i]);
2768
+ if (!next) {
2769
+ next = { hasValue: false };
2770
+ children.set(path[i], next);
2522
2771
  }
2523
- };
2524
- this.collection = collection;
2525
- this.selectIndexKeys = opts.selectIndexKeys;
2526
- this.unsubscribeFromCollection = collection.emitter.on(
2527
- 0 /* UPDATE */,
2528
- (payload) => this.onCollectionUpdate(payload.pks)
2529
- );
2530
- if (opts.buildInitial !== false) {
2531
- this.rebuildFromCollection();
2772
+ node = next;
2773
+ }
2774
+ if (!node.hasValue) {
2775
+ node.hasValue = true;
2776
+ this._size++;
2777
+ }
2778
+ node.value = value;
2779
+ }
2780
+ get(path) {
2781
+ const node = this.findNode(path);
2782
+ return node && node.hasValue ? node.value : void 0;
2783
+ }
2784
+ has(path) {
2785
+ const node = this.findNode(path);
2786
+ return !!node && node.hasValue;
2787
+ }
2788
+ delete(path) {
2789
+ const parents = [];
2790
+ let node = this.root;
2791
+ for (let i = 0; i < path.length; i++) {
2792
+ const next = node.children?.get(path[i]);
2793
+ if (!next) return false;
2794
+ parents.push(node);
2795
+ node = next;
2796
+ }
2797
+ if (!node.hasValue) return false;
2798
+ node.hasValue = false;
2799
+ node.value = void 0;
2800
+ this._size--;
2801
+ let child = node;
2802
+ for (let i = parents.length - 1; i >= 0; i--) {
2803
+ if (child.hasValue || child.children && child.children.size > 0) {
2804
+ break;
2805
+ }
2806
+ const parent = parents[i];
2807
+ parent.children.delete(path[i]);
2808
+ if (parent.children.size === 0) parent.children = void 0;
2809
+ child = parent;
2532
2810
  }
2811
+ return true;
2533
2812
  }
2534
- rebuildFromCollection() {
2535
- this.keysByPk.clear();
2536
- super.clear();
2537
- const slotsByKey = /* @__PURE__ */ new Map();
2538
- const slots = this.collection.getAllSlots();
2539
- for (const slot of slots) {
2540
- if (slot.item === void 0) continue;
2541
- const keys = this.normalizeKeys(this.selectIndexKeys(slot.item));
2542
- if (keys.length === 0) continue;
2543
- this.keysByPk.set(slot.pk, new Set(keys));
2813
+ clear() {
2814
+ this.root.children = void 0;
2815
+ this.root.value = void 0;
2816
+ this.root.hasValue = false;
2817
+ this._size = 0;
2818
+ }
2819
+ /** Depth-first iteration over `[path, value]` for every stored path. */
2820
+ *entries() {
2821
+ const stack = [{ node: this.root, path: [] }];
2822
+ while (stack.length > 0) {
2823
+ const frame = stack.pop();
2824
+ const { node, path } = frame;
2825
+ if (node.hasValue) {
2826
+ yield [path, node.value];
2827
+ }
2828
+ if (node.children) {
2829
+ for (const [segment, child] of node.children) {
2830
+ stack.push({ node: child, path: [...path, segment] });
2831
+ }
2832
+ }
2833
+ }
2834
+ }
2835
+ *keys() {
2836
+ for (const [path] of this.entries()) yield path;
2837
+ }
2838
+ *values() {
2839
+ for (const [, value] of this.entries()) yield value;
2840
+ }
2841
+ findNode(path) {
2842
+ let node = this.root;
2843
+ for (let i = 0; i < path.length; i++) {
2844
+ node = node.children?.get(path[i]);
2845
+ if (!node) return void 0;
2846
+ }
2847
+ return node;
2848
+ }
2849
+ };
2850
+
2851
+ // src/core/OIMIndexStoreTrieDrivenSetBased.ts
2852
+ var OIMIndexStoreTrieDrivenSetBased = class extends OIMIndexStoreSetBased {
2853
+ constructor() {
2854
+ super(...arguments);
2855
+ this.slots = new OIMTrieMap();
2856
+ this.reservedBuckets = new OIMTrieMap();
2857
+ }
2858
+ getOrReserveBucket(key) {
2859
+ const live = this.slots.get(key);
2860
+ if (live) return live;
2861
+ let reserved = this.reservedBuckets.get(key);
2862
+ if (!reserved) {
2863
+ reserved = new OIMKeyedBucketSetBased(key);
2864
+ this.reservedBuckets.set(key, reserved);
2865
+ }
2866
+ return reserved;
2867
+ }
2868
+ findBucket(key) {
2869
+ return this.slots.get(key) ?? this.reservedBuckets.get(key);
2870
+ }
2871
+ retainBucket(bucket) {
2872
+ if (bucket.size === 0) return;
2873
+ this.reservedBuckets.delete(bucket.key);
2874
+ if (!this.slots.has(bucket.key)) this.slots.set(bucket.key, bucket);
2875
+ }
2876
+ releaseBucket(bucket) {
2877
+ if (bucket.size > 0) return;
2878
+ this.slots.delete(bucket.key);
2879
+ if (bucket.hasSubscribers()) {
2880
+ this.reservedBuckets.set(bucket.key, bucket);
2881
+ } else {
2882
+ this.reservedBuckets.delete(bucket.key);
2883
+ }
2884
+ }
2885
+ dropIfReserved(key) {
2886
+ const reserved = this.reservedBuckets.get(key);
2887
+ if (reserved && reserved.size === 0 && !reserved.hasSubscribers()) {
2888
+ this.reservedBuckets.delete(key);
2889
+ }
2890
+ }
2891
+ setOneByKey(key, slots) {
2892
+ const bucket = this.getOrReserveBucket(key);
2893
+ if (bucket !== slots) {
2894
+ bucket.clear();
2895
+ for (const slot of slots) bucket.add(slot);
2896
+ }
2897
+ if (bucket.size > 0) this.retainBucket(bucket);
2898
+ else this.releaseBucket(bucket);
2899
+ }
2900
+ removeOneByKey(key) {
2901
+ this.slots.delete(key);
2902
+ this.reservedBuckets.delete(key);
2903
+ }
2904
+ removeManyByKeys(keys) {
2905
+ for (let i = 0; i < keys.length; i++) {
2906
+ this.slots.delete(keys[i]);
2907
+ this.reservedBuckets.delete(keys[i]);
2908
+ }
2909
+ }
2910
+ getOneByKey(key) {
2911
+ return this.slots.get(key);
2912
+ }
2913
+ getManyByKeys(keys) {
2914
+ const result = /* @__PURE__ */ new Map();
2915
+ for (let i = 0; i < keys.length; i++) {
2916
+ const slots = this.slots.get(keys[i]);
2917
+ if (slots !== void 0) result.set(keys[i], slots);
2918
+ }
2919
+ return result;
2920
+ }
2921
+ getAllKeys() {
2922
+ return Array.from(this.slots.keys());
2923
+ }
2924
+ getAll() {
2925
+ const result = /* @__PURE__ */ new Map();
2926
+ for (const [path, bucket] of this.slots.entries()) {
2927
+ result.set(path, bucket);
2928
+ }
2929
+ return result;
2930
+ }
2931
+ countAll() {
2932
+ return this.slots.size;
2933
+ }
2934
+ clear() {
2935
+ for (const bucket of this.slots.values()) {
2936
+ bucket.clear();
2937
+ if (bucket.hasSubscribers()) {
2938
+ this.reservedBuckets.set(bucket.key, bucket);
2939
+ }
2940
+ }
2941
+ this.slots.clear();
2942
+ }
2943
+ };
2944
+
2945
+ // src/core/OIMReactiveCollectionIndexCompositeTrieSetBased.ts
2946
+ var OIMReactiveCollectionIndexCompositeTrieSetBased = class extends OIMReactiveIndexManualSetBased {
2947
+ constructor(queue, opts) {
2948
+ const resolveSlot = opts.collection !== void 0 ? (pk) => opts.collection.getOrReserveSlotByPk(pk) : opts.resolveSlot;
2949
+ super(queue, {
2950
+ indexOptions: {
2951
+ comparePks: opts.indexOptions?.comparePks,
2952
+ store: opts.indexOptions?.store ?? new OIMIndexStoreTrieDrivenSetBased()
2953
+ },
2954
+ pkDomain: opts.collection?.keyDomain
2955
+ });
2956
+ this.resolveRequiredSlot = (pk) => {
2957
+ const slot = this.resolveSlot(pk);
2958
+ if (!slot) return { pk, item: void 0 };
2959
+ return slot;
2960
+ };
2961
+ this.resolveSlot = resolveSlot;
2962
+ }
2963
+ setPks(key, pks) {
2964
+ this.index.setPks(key, pks, this.resolveRequiredSlot);
2965
+ }
2966
+ addPks(key, pks) {
2967
+ this.index.addPks(key, pks, this.resolveRequiredSlot);
2968
+ }
2969
+ removePks(key, pks) {
2970
+ this.index.removePks(key, pks);
2971
+ }
2972
+ getEntitiesByKey(key) {
2973
+ return super.getEntitiesByKey(key);
2974
+ }
2975
+ getEntitiesByKeys(keys) {
2976
+ return super.getEntitiesByKeys(keys);
2977
+ }
2978
+ };
2979
+
2980
+ // src/core/OIMIndexStoreTrieDrivenArrayBased.ts
2981
+ var OIMIndexStoreTrieDrivenArrayBased = class extends OIMIndexStoreArrayBased {
2982
+ constructor() {
2983
+ super(...arguments);
2984
+ this.slots = new OIMTrieMap();
2985
+ this.reservedBuckets = new OIMTrieMap();
2986
+ }
2987
+ getOrReserveBucket(key) {
2988
+ const live = this.slots.get(key);
2989
+ if (live) return live;
2990
+ let reserved = this.reservedBuckets.get(key);
2991
+ if (!reserved) {
2992
+ reserved = new OIMKeyedBucketArrayBased(key);
2993
+ this.reservedBuckets.set(key, reserved);
2994
+ }
2995
+ return reserved;
2996
+ }
2997
+ findBucket(key) {
2998
+ return this.slots.get(key) ?? this.reservedBuckets.get(key);
2999
+ }
3000
+ retainBucket(bucket) {
3001
+ if (bucket.length === 0) return;
3002
+ this.reservedBuckets.delete(bucket.key);
3003
+ if (!this.slots.has(bucket.key)) this.slots.set(bucket.key, bucket);
3004
+ }
3005
+ releaseBucket(bucket) {
3006
+ if (bucket.length > 0) return;
3007
+ this.slots.delete(bucket.key);
3008
+ if (bucket.hasSubscribers()) {
3009
+ this.reservedBuckets.set(bucket.key, bucket);
3010
+ } else {
3011
+ this.reservedBuckets.delete(bucket.key);
3012
+ }
3013
+ }
3014
+ dropIfReserved(key) {
3015
+ const reserved = this.reservedBuckets.get(key);
3016
+ if (reserved && reserved.length === 0 && !reserved.hasSubscribers()) {
3017
+ this.reservedBuckets.delete(key);
3018
+ }
3019
+ }
3020
+ setOneByKey(key, slots) {
3021
+ const bucket = this.getOrReserveBucket(key);
3022
+ if (bucket !== slots) {
3023
+ bucket.length = 0;
3024
+ for (let i = 0; i < slots.length; i++) bucket.push(slots[i]);
3025
+ }
3026
+ if (bucket.length > 0) this.retainBucket(bucket);
3027
+ else this.releaseBucket(bucket);
3028
+ }
3029
+ removeOneByKey(key) {
3030
+ this.slots.delete(key);
3031
+ this.reservedBuckets.delete(key);
3032
+ }
3033
+ removeManyByKeys(keys) {
3034
+ for (let i = 0; i < keys.length; i++) {
3035
+ this.slots.delete(keys[i]);
3036
+ this.reservedBuckets.delete(keys[i]);
3037
+ }
3038
+ }
3039
+ getOneByKey(key) {
3040
+ return this.slots.get(key);
3041
+ }
3042
+ getManyByKeys(keys) {
3043
+ const result = /* @__PURE__ */ new Map();
3044
+ for (let i = 0; i < keys.length; i++) {
3045
+ const slots = this.slots.get(keys[i]);
3046
+ if (slots !== void 0) result.set(keys[i], slots);
3047
+ }
3048
+ return result;
3049
+ }
3050
+ getAllKeys() {
3051
+ return Array.from(this.slots.keys());
3052
+ }
3053
+ getAll() {
3054
+ const result = /* @__PURE__ */ new Map();
3055
+ for (const [path, bucket] of this.slots.entries()) {
3056
+ result.set(path, bucket);
3057
+ }
3058
+ return result;
3059
+ }
3060
+ countAll() {
3061
+ return this.slots.size;
3062
+ }
3063
+ clear() {
3064
+ for (const bucket of this.slots.values()) {
3065
+ bucket.length = 0;
3066
+ if (bucket.hasSubscribers()) {
3067
+ this.reservedBuckets.set(bucket.key, bucket);
3068
+ }
3069
+ }
3070
+ this.slots.clear();
3071
+ }
3072
+ };
3073
+
3074
+ // src/core/OIMReactiveCollectionIndexCompositeTrieArrayBased.ts
3075
+ var OIMReactiveCollectionIndexCompositeTrieArrayBased = class extends OIMReactiveIndexManualArrayBased {
3076
+ constructor(queue, opts) {
3077
+ const resolveSlot = opts.collection !== void 0 ? (pk) => opts.collection.getOrReserveSlotByPk(pk) : opts.resolveSlot;
3078
+ super(queue, {
3079
+ indexOptions: {
3080
+ comparePks: opts.indexOptions?.comparePks,
3081
+ store: opts.indexOptions?.store ?? new OIMIndexStoreTrieDrivenArrayBased()
3082
+ }
3083
+ });
3084
+ // Persistent per-key-path pk membership for O(1) `addPks` dedup. Trie-keyed
3085
+ // so a rebuilt key path matches by content.
3086
+ this.pksByKey = new OIMTrieMap();
3087
+ this.resolveSlot = resolveSlot;
3088
+ }
3089
+ setPks(key, pks) {
3090
+ const pkSet = /* @__PURE__ */ new Set();
3091
+ for (let i = 0; i < pks.length; i++) pkSet.add(pks[i]);
3092
+ this.pksByKey.set(key, pkSet);
3093
+ super.setSlots(key, this.resolveSlots(pks));
3094
+ }
3095
+ addPks(key, pks) {
3096
+ if (pks.length === 0) return;
3097
+ const pkSet = this.membershipOf(key);
3098
+ const newSlots = [];
3099
+ for (const pk of pks) {
3100
+ if (pkSet.has(pk)) continue;
3101
+ pkSet.add(pk);
3102
+ newSlots.push(this.resolveRequiredSlot(pk));
3103
+ }
3104
+ if (newSlots.length > 0) this.appendSlots(key, newSlots);
3105
+ }
3106
+ removePks(key, pks) {
3107
+ if (pks.length === 0) return;
3108
+ const pksToRemove = new Set(pks);
3109
+ const existingSlots = this.index.getSlotsByKey(key);
3110
+ const nextSlots = existingSlots.filter(
3111
+ (slot) => !pksToRemove.has(slot.pk)
3112
+ );
3113
+ if (nextSlots.length === existingSlots.length) return;
3114
+ const pkSet = this.pksByKey.get(key);
3115
+ if (pkSet) {
3116
+ for (let i = 0; i < pks.length; i++) pkSet.delete(pks[i]);
3117
+ }
3118
+ if (nextSlots.length === 0) this.clear(key);
3119
+ else super.setSlots(key, nextSlots);
3120
+ }
3121
+ clear(key) {
3122
+ if (key === void 0) this.pksByKey.clear();
3123
+ else this.pksByKey.delete(key);
3124
+ super.clear(key);
3125
+ }
3126
+ /**
3127
+ * The membership set for a key path, lazily seeded from the current bucket so
3128
+ * it stays correct even if slots were set through a lower-level path.
3129
+ */
3130
+ membershipOf(key) {
3131
+ let pkSet = this.pksByKey.get(key);
3132
+ if (!pkSet) {
3133
+ pkSet = /* @__PURE__ */ new Set();
3134
+ const existing = this.index.getSlotsByKey(key);
3135
+ for (let i = 0; i < existing.length; i++) {
3136
+ pkSet.add(existing[i].pk);
3137
+ }
3138
+ this.pksByKey.set(key, pkSet);
3139
+ }
3140
+ return pkSet;
3141
+ }
3142
+ getEntitiesByKey(key) {
3143
+ return super.getEntitiesByKey(key);
3144
+ }
3145
+ getEntitiesByKeys(keys) {
3146
+ return super.getEntitiesByKeys(keys);
3147
+ }
3148
+ resolveSlots(pks) {
3149
+ const slots = [];
3150
+ slots.length = pks.length;
3151
+ for (let i = 0; i < pks.length; i++) {
3152
+ slots[i] = this.resolveRequiredSlot(pks[i]);
3153
+ }
3154
+ return slots;
3155
+ }
3156
+ resolveRequiredSlot(pk) {
3157
+ const slot = this.resolveSlot(pk);
3158
+ if (!slot) return { pk, item: void 0 };
3159
+ return slot;
3160
+ }
3161
+ };
3162
+
3163
+ // src/core/OIMDerivedCollectionIndexSetBased.ts
3164
+ var OIMDerivedCollectionIndexSetBased = class extends OIMReactiveCollectionIndexManualSetBased {
3165
+ constructor(queue, collection, opts) {
3166
+ super(queue, {
3167
+ collection,
3168
+ indexOptions: opts.indexOptions
3169
+ });
3170
+ this.keysByPk = /* @__PURE__ */ new Map();
3171
+ this.onCollectionUpdate = (pks) => {
3172
+ if (pks.length === 0) {
3173
+ this.rebuildFromCollection();
3174
+ return;
3175
+ }
3176
+ for (const pk of pks) {
3177
+ this.syncPk(pk);
3178
+ }
3179
+ };
3180
+ this.collection = collection;
3181
+ this.selectIndexKeys = opts.selectIndexKeys;
3182
+ this.unsubscribeFromCollection = collection.emitter.on(
3183
+ 0 /* UPDATE */,
3184
+ (payload) => this.onCollectionUpdate(payload.pks)
3185
+ );
3186
+ if (opts.buildInitial !== false) {
3187
+ this.rebuildFromCollection();
3188
+ }
3189
+ }
3190
+ rebuildFromCollection() {
3191
+ this.keysByPk.clear();
3192
+ super.clear();
3193
+ const slotsByKey = /* @__PURE__ */ new Map();
3194
+ const slots = this.collection.getAllSlots();
3195
+ for (const slot of slots) {
3196
+ if (slot.item === void 0) continue;
3197
+ const keys = this.normalizeKeys(this.selectIndexKeys(slot.item));
3198
+ if (keys.length === 0) continue;
3199
+ this.keysByPk.set(slot.pk, new Set(keys));
2544
3200
  for (const key of keys) {
2545
3201
  let slotsForKey = slotsByKey.get(key);
2546
3202
  if (!slotsForKey) {
@@ -3202,6 +3858,30 @@ var OIMCollectionIndexFactory = class {
3202
3858
  indexOptions: opts.indexOptions
3203
3859
  });
3204
3860
  }
3861
+ /**
3862
+ * Composite (trie-backed) manual Set-based index keyed by an
3863
+ * arbitrary-length key path, e.g. `setPks([userId, projectId, role], pks)`.
3864
+ * Key paths are matched by content in O(arity) — no string concatenation,
3865
+ * no separator collisions, each segment keeps its type. Primitive-keyed
3866
+ * indexes are unaffected and keep their native-`Map` fast path.
3867
+ */
3868
+ compositeSetIndex(opts = {}) {
3869
+ return new OIMReactiveCollectionIndexCompositeTrieSetBased(this.queue, {
3870
+ collection: this.collection,
3871
+ indexOptions: opts.indexOptions
3872
+ });
3873
+ }
3874
+ /**
3875
+ * Composite (trie-backed) manual Array-based (ordered) index keyed by an
3876
+ * arbitrary-length key path, e.g. `setPks([channelId, threadId], pks)`.
3877
+ * Ordered counterpart of `compositeSetIndex`.
3878
+ */
3879
+ compositeArrayIndex(opts = {}) {
3880
+ return new OIMReactiveCollectionIndexCompositeTrieArrayBased(this.queue, {
3881
+ collection: this.collection,
3882
+ indexOptions: opts.indexOptions
3883
+ });
3884
+ }
3205
3885
  derivedSetIndex(selectIndexKeys, opts = {}) {
3206
3886
  return new OIMDerivedCollectionIndexSetBased(
3207
3887
  this.queue,
@@ -3450,6 +4130,195 @@ var OIMReactiveObject = class extends OIMObject {
3450
4130
  }
3451
4131
  };
3452
4132
 
4133
+ // src/core/OIMTrieSet.ts
4134
+ var OIMTrieSet = class {
4135
+ constructor() {
4136
+ this.map = new OIMTrieMap();
4137
+ }
4138
+ get size() {
4139
+ return this.map.size;
4140
+ }
4141
+ add(key) {
4142
+ this.map.set(key, true);
4143
+ }
4144
+ has(key) {
4145
+ return this.map.has(key);
4146
+ }
4147
+ delete(key) {
4148
+ return this.map.delete(key);
4149
+ }
4150
+ *values() {
4151
+ yield* this.map.keys();
4152
+ }
4153
+ clear() {
4154
+ this.map.clear();
4155
+ }
4156
+ };
4157
+
4158
+ // src/core/OIMKeyDomainTrie.ts
4159
+ var OIMKeyDomainTrie = class {
4160
+ constructor() {
4161
+ // Interning pool: logical key (by content) → the one canonical reference.
4162
+ this.pool = new OIMTrieMap();
4163
+ }
4164
+ createMap() {
4165
+ return new OIMTrieMap();
4166
+ }
4167
+ createSet() {
4168
+ return new OIMTrieSet();
4169
+ }
4170
+ canonicalize(key) {
4171
+ const existing = this.pool.get(key);
4172
+ if (existing !== void 0) return existing;
4173
+ const canonical = Object.freeze(key.slice());
4174
+ this.pool.set(canonical, canonical);
4175
+ return canonical;
4176
+ }
4177
+ };
4178
+
4179
+ // src/core/OIMCollectionStoreTrieDriven.ts
4180
+ var OIMCollectionStoreTrieDriven = class extends OIMCollectionStore {
4181
+ constructor(keyDomain = new OIMKeyDomainTrie()) {
4182
+ super();
4183
+ this.keyDomain = keyDomain;
4184
+ this.slots = new OIMTrieMap();
4185
+ this.reservedSlots = new OIMTrieMap();
4186
+ }
4187
+ setOneByPk(pk, entity) {
4188
+ const slot = this.slots.get(pk);
4189
+ if (slot) {
4190
+ slot.item = entity;
4191
+ return slot;
4192
+ }
4193
+ const reserved = this.reservedSlots.get(pk);
4194
+ if (reserved) {
4195
+ reserved.item = entity;
4196
+ this.reservedSlots.delete(pk);
4197
+ this.slots.set(pk, reserved);
4198
+ return reserved;
4199
+ }
4200
+ const nextSlot = {
4201
+ pk: this.keyDomain.canonicalize(pk),
4202
+ item: entity
4203
+ };
4204
+ this.slots.set(pk, nextSlot);
4205
+ return nextSlot;
4206
+ }
4207
+ getOrReserveSlotByPk(pk) {
4208
+ const slot = this.slots.get(pk);
4209
+ if (slot) return slot;
4210
+ let reserved = this.reservedSlots.get(pk);
4211
+ if (!reserved) {
4212
+ reserved = { pk: this.keyDomain.canonicalize(pk), item: void 0 };
4213
+ this.reservedSlots.set(pk, reserved);
4214
+ }
4215
+ return reserved;
4216
+ }
4217
+ setManyByPks(pks, entities) {
4218
+ const slots = [];
4219
+ slots.length = pks.length;
4220
+ for (let i = 0; i < pks.length; i++) {
4221
+ slots[i] = this.setOneByPk(pks[i], entities[i]);
4222
+ }
4223
+ return slots;
4224
+ }
4225
+ getSlotByPk(pk) {
4226
+ return this.slots.get(pk);
4227
+ }
4228
+ findSlotByPk(pk) {
4229
+ return this.slots.get(pk) ?? this.reservedSlots.get(pk);
4230
+ }
4231
+ hasSubscribers(slot) {
4232
+ return slot.subscribers !== void 0 && slot.subscribers.size > 0;
4233
+ }
4234
+ getSlotsByPks(pks) {
4235
+ const result = [];
4236
+ result.length = pks.length;
4237
+ let writeIndex = 0;
4238
+ for (let i = 0; i < pks.length; i++) {
4239
+ const slot = this.slots.get(pks[i]);
4240
+ if (slot !== void 0) result[writeIndex++] = slot;
4241
+ }
4242
+ result.length = writeIndex;
4243
+ return result;
4244
+ }
4245
+ getAllSlots() {
4246
+ return Array.from(this.slots.values());
4247
+ }
4248
+ removeOneByPk(pk) {
4249
+ const slot = this.slots.get(pk);
4250
+ if (slot) {
4251
+ slot.item = void 0;
4252
+ this.slots.delete(pk);
4253
+ if (this.hasSubscribers(slot)) {
4254
+ this.reservedSlots.set(pk, slot);
4255
+ }
4256
+ return;
4257
+ }
4258
+ const reserved = this.reservedSlots.get(pk);
4259
+ if (reserved) {
4260
+ reserved.item = void 0;
4261
+ if (!this.hasSubscribers(reserved)) {
4262
+ this.reservedSlots.delete(pk);
4263
+ }
4264
+ }
4265
+ }
4266
+ removeManyByPks(pks) {
4267
+ for (const pk of pks) this.removeOneByPk(pk);
4268
+ }
4269
+ getOneByPk(pk) {
4270
+ return this.slots.get(pk)?.item;
4271
+ }
4272
+ getManyByPks(pks) {
4273
+ const result = [];
4274
+ result.length = pks.length;
4275
+ let writeIndex = 0;
4276
+ for (let i = 0; i < pks.length; i++) {
4277
+ const entity = this.getOneByPk(pks[i]);
4278
+ if (entity !== void 0) result[writeIndex++] = entity;
4279
+ }
4280
+ result.length = writeIndex;
4281
+ return result;
4282
+ }
4283
+ getAll() {
4284
+ const result = [];
4285
+ for (const slot of this.slots.values()) {
4286
+ if (slot.item !== void 0) result.push(slot.item);
4287
+ }
4288
+ return result;
4289
+ }
4290
+ countAll() {
4291
+ return this.slots.size;
4292
+ }
4293
+ clear() {
4294
+ const retained = [];
4295
+ for (const slot of this.slots.values()) {
4296
+ slot.item = void 0;
4297
+ if (this.hasSubscribers(slot)) retained.push(slot);
4298
+ }
4299
+ this.slots.clear();
4300
+ for (const slot of this.reservedSlots.values()) {
4301
+ slot.item = void 0;
4302
+ if (this.hasSubscribers(slot)) retained.push(slot);
4303
+ }
4304
+ this.reservedSlots.clear();
4305
+ for (const slot of retained) {
4306
+ this.reservedSlots.set(slot.pk, slot);
4307
+ }
4308
+ }
4309
+ getAllPks() {
4310
+ return Array.from(this.slots.keys());
4311
+ }
4312
+ destroy() {
4313
+ this.clear();
4314
+ for (const slot of this.reservedSlots.values()) {
4315
+ slot.subscribers?.clear();
4316
+ slot.subscribers = void 0;
4317
+ }
4318
+ this.reservedSlots.clear();
4319
+ }
4320
+ };
4321
+
3453
4322
  // src/core/OIMObjectStoreMapDriven.ts
3454
4323
  var OIMObjectStoreMapDriven = class extends OIMObjectStore {
3455
4324
  constructor() {
@@ -3497,6 +4366,27 @@ var OIMObjectStoreMapDriven = class extends OIMObjectStore {
3497
4366
  }
3498
4367
  };
3499
4368
 
4369
+ // src/core/OIMKeyPathCarrierResolver.ts
4370
+ var OIMKeyPathCarrierResolver = class {
4371
+ constructor() {
4372
+ this.carriers = new OIMTrieMap();
4373
+ }
4374
+ getOrReserveCarrier(key) {
4375
+ let carrier = this.carriers.get(key);
4376
+ if (!carrier) {
4377
+ carrier = { key };
4378
+ this.carriers.set(key, carrier);
4379
+ }
4380
+ return carrier;
4381
+ }
4382
+ findCarrier(key) {
4383
+ return this.carriers.get(key);
4384
+ }
4385
+ onCarrierEmptied(carrier) {
4386
+ this.carriers.delete(carrier.key);
4387
+ }
4388
+ };
4389
+
3500
4390
  // src/core/OIMComparatorFactory.ts
3501
4391
  var OIMComparatorFactory = class {
3502
4392
  createShallowComparator() {
@@ -3715,6 +4605,78 @@ var OIMMap2Keys = class {
3715
4605
  }
3716
4606
  };
3717
4607
 
4608
+ // src/core/OIMDisposeScope.ts
4609
+ var OIMDisposeScope = class _OIMDisposeScope {
4610
+ constructor() {
4611
+ this.disposables = [];
4612
+ this.disposed = false;
4613
+ }
4614
+ /** Number of not-yet-disposed items currently held. */
4615
+ get size() {
4616
+ return this.disposables.length;
4617
+ }
4618
+ get isDisposed() {
4619
+ return this.disposed;
4620
+ }
4621
+ /**
4622
+ * Register a disposable (object with `destroy()` or an unsubscribe function)
4623
+ * and return it unchanged, so it can be captured inline. If the scope is
4624
+ * already disposed the item is disposed immediately, so nothing leaks.
4625
+ */
4626
+ add(disposable) {
4627
+ if (this.disposed) {
4628
+ this.disposeOne(disposable);
4629
+ return disposable;
4630
+ }
4631
+ this.disposables.push(disposable);
4632
+ return disposable;
4633
+ }
4634
+ /** A nested scope, itself registered here — dispose it early or with the parent. */
4635
+ child() {
4636
+ return this.add(new _OIMDisposeScope());
4637
+ }
4638
+ /**
4639
+ * Dispose everything in reverse registration order, then clear. Idempotent.
4640
+ * If any `destroy()`/unsubscribe throws, the rest are still disposed and the
4641
+ * first error is rethrown once teardown completes.
4642
+ */
4643
+ destroy() {
4644
+ if (this.disposed) return;
4645
+ this.disposed = true;
4646
+ let firstError;
4647
+ let hasError = false;
4648
+ for (let i = this.disposables.length - 1; i >= 0; i--) {
4649
+ try {
4650
+ this.disposeOne(this.disposables[i]);
4651
+ } catch (error) {
4652
+ if (!hasError) {
4653
+ hasError = true;
4654
+ firstError = error;
4655
+ }
4656
+ }
4657
+ }
4658
+ this.disposables.length = 0;
4659
+ if (hasError) throw firstError;
4660
+ }
4661
+ disposeOne(disposable) {
4662
+ if (typeof disposable === "function") disposable();
4663
+ else disposable.destroy();
4664
+ }
4665
+ };
4666
+ function createOIMDisposeScope() {
4667
+ return new OIMDisposeScope();
4668
+ }
4669
+
4670
+ // src/core/OIMPkCodecKeyPath.ts
4671
+ var OIMPkCodecKeyPath = class {
4672
+ encode(pk) {
4673
+ return JSON.stringify(pk);
4674
+ }
4675
+ decode(encoded) {
4676
+ return JSON.parse(encoded);
4677
+ }
4678
+ };
4679
+
3718
4680
  // src/modules/compute/core/OIMComputeRuntime.ts
3719
4681
  var OIMComputeRuntime = class {
3720
4682
  constructor(queue) {
@@ -4318,6 +5280,27 @@ var OIMCollectionSelectors = class {
4318
5280
  key
4319
5281
  );
4320
5282
  }
5283
+ /**
5284
+ * Entities of a composite (key-path) set-based index for one key path, e.g.
5285
+ * `entitiesByCompositeSetIndexKey(index, [projectId, role])`.
5286
+ */
5287
+ entitiesByCompositeSetIndexKey(index, key) {
5288
+ return new OIMEntitiesByIndexKeySetBasedSelector(
5289
+ this.runtime,
5290
+ this.collection,
5291
+ index,
5292
+ key
5293
+ );
5294
+ }
5295
+ /** Entities of a composite (key-path) array-based index for one key path. */
5296
+ entitiesByCompositeArrayIndexKey(index, key) {
5297
+ return new OIMEntitiesByIndexKeyArrayBasedSelector(
5298
+ this.runtime,
5299
+ this.collection,
5300
+ index,
5301
+ key
5302
+ );
5303
+ }
4321
5304
  /** Entities of a keyless (whole-collection) array-based index, ordered. */
4322
5305
  entitiesByArrayGlobalIndex(index) {
4323
5306
  return new OIMEntitiesByGlobalIndexArrayBasedSelector(
@@ -4341,7 +5324,16 @@ function createOIMCollectionKit(queue, opts) {
4341
5324
  const collection = new OIMReactiveCollection(queue, opts);
4342
5325
  const indexFactory = new OIMCollectionIndexFactory(queue, collection);
4343
5326
  const select = new OIMCollectionSelectors(queue, collection);
4344
- return { queue, collection, indexFactory, select };
5327
+ const scope = new OIMDisposeScope();
5328
+ scope.add(collection);
5329
+ return {
5330
+ queue,
5331
+ collection,
5332
+ indexFactory,
5333
+ select,
5334
+ scope,
5335
+ destroy: () => scope.destroy()
5336
+ };
4345
5337
  }
4346
5338
 
4347
5339
  // src/modules/wrapper/collection/OIMCollectionChangedFields.ts
@@ -5530,6 +6522,7 @@ var OIMIndexPksByKeysArrayBasedSelector = class extends OIMSelector {
5530
6522
  EOIMEventQueueSchedulerEventType,
5531
6523
  EOIMIndexEventType,
5532
6524
  EOIMObjectEventType,
6525
+ OIMBucketCarrierResolver,
5533
6526
  OIMCarrierKeyedEmitter,
5534
6527
  OIMCarrierSingleEmitter,
5535
6528
  OIMCollection,
@@ -5542,6 +6535,7 @@ var OIMIndexPksByKeysArrayBasedSelector = class extends OIMSelector {
5542
6535
  OIMCollectionSelectors,
5543
6536
  OIMCollectionStore,
5544
6537
  OIMCollectionStoreMapDriven,
6538
+ OIMCollectionStoreTrieDriven,
5545
6539
  OIMComparatorFactory,
5546
6540
  OIMComputeRuntime,
5547
6541
  OIMComputed,
@@ -5550,6 +6544,7 @@ var OIMIndexPksByKeysArrayBasedSelector = class extends OIMSelector {
5550
6544
  OIMDerivedCollectionGlobalIndexSetBased,
5551
6545
  OIMDerivedCollectionIndexArrayBased,
5552
6546
  OIMDerivedCollectionIndexSetBased,
6547
+ OIMDisposeScope,
5553
6548
  OIMEffect,
5554
6549
  OIMEffectDependencyComputed,
5555
6550
  OIMEffectDependencyKeyed,
@@ -5592,6 +6587,13 @@ var OIMIndexPksByKeysArrayBasedSelector = class extends OIMSelector {
5592
6587
  OIMIndexStoreMapDrivenArrayBased,
5593
6588
  OIMIndexStoreMapDrivenSetBased,
5594
6589
  OIMIndexStoreSetBased,
6590
+ OIMIndexStoreTrieDrivenArrayBased,
6591
+ OIMIndexStoreTrieDrivenSetBased,
6592
+ OIMKeyDomainNative,
6593
+ OIMKeyDomainTrie,
6594
+ OIMKeyPathCarrierResolver,
6595
+ OIMKeyedBucketArrayBased,
6596
+ OIMKeyedBucketSetBased,
5595
6597
  OIMMap2Keys,
5596
6598
  OIMObject,
5597
6599
  OIMObjectStore,
@@ -5603,10 +6605,13 @@ var OIMIndexPksByKeysArrayBasedSelector = class extends OIMSelector {
5603
6605
  OIMOrderedListCommandStream,
5604
6606
  OIMOrderedListCommandStreamDiffDriven,
5605
6607
  OIMOrderedListMappedCommandStream,
6608
+ OIMPkCodecKeyPath,
5606
6609
  OIMPkSelectorFactory,
5607
6610
  OIMReactiveCollection,
5608
6611
  OIMReactiveCollectionGlobalIndexManualArrayBased,
5609
6612
  OIMReactiveCollectionGlobalIndexManualSetBased,
6613
+ OIMReactiveCollectionIndexCompositeTrieArrayBased,
6614
+ OIMReactiveCollectionIndexCompositeTrieSetBased,
5610
6615
  OIMReactiveCollectionIndexManualArrayBased,
5611
6616
  OIMReactiveCollectionIndexManualSetBased,
5612
6617
  OIMReactiveGlobalIndex,
@@ -5621,10 +6626,13 @@ var OIMIndexPksByKeysArrayBasedSelector = class extends OIMSelector {
5621
6626
  OIMReactiveIndexSetBased,
5622
6627
  OIMReactiveObject,
5623
6628
  OIMSelector,
6629
+ OIMTrieMap,
6630
+ OIMTrieSet,
5624
6631
  createInPlaceEntityUpdater,
5625
6632
  createMergeEntityUpdater,
5626
6633
  createOIMCollectionIndexFactory,
5627
6634
  createOIMCollectionKit,
6635
+ createOIMDisposeScope,
5628
6636
  createOIMGlobalIndexSlotMap,
5629
6637
  createOIMIndexSlotMap,
5630
6638
  createOIMOrderedListCommandStreamDiffDriven,