@oimdb/core 3.6.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.js CHANGED
@@ -227,21 +227,82 @@ var OIMEventQueueScheduler = class {
227
227
  }
228
228
  };
229
229
 
230
+ // src/core/OIMKeyedBucketSetBased.ts
231
+ var OIMKeyedBucketSetBased = class extends Set {
232
+ constructor(key) {
233
+ super();
234
+ this.key = key;
235
+ }
236
+ hasSubscribers() {
237
+ return this.subscribers !== void 0 && this.subscribers.size > 0;
238
+ }
239
+ /** Clearing the slot set also clears the pk membership — they stay in sync. */
240
+ clear() {
241
+ super.clear();
242
+ this.membership?.clear();
243
+ }
244
+ };
245
+
230
246
  // src/core/OIMIndexStoreMapDrivenSetBased.ts
231
247
  var OIMIndexStoreMapDrivenSetBased = class extends OIMIndexStoreSetBased {
232
248
  constructor() {
233
249
  super(...arguments);
250
+ // Live buckets (non-empty, or freshly promoted). Enumeration reads these only.
234
251
  this.slots = /* @__PURE__ */ new Map();
252
+ // Empty buckets kept alive solely by their subscribers, so a subscription
253
+ // survives a key going empty → re-filled. Invisible to reads/enumeration.
254
+ this.reservedBuckets = /* @__PURE__ */ new Map();
255
+ }
256
+ getOrReserveBucket(key) {
257
+ const live = this.slots.get(key);
258
+ if (live) return live;
259
+ let reserved = this.reservedBuckets.get(key);
260
+ if (!reserved) {
261
+ reserved = new OIMKeyedBucketSetBased(key);
262
+ this.reservedBuckets.set(key, reserved);
263
+ }
264
+ return reserved;
265
+ }
266
+ findBucket(key) {
267
+ return this.slots.get(key) ?? this.reservedBuckets.get(key);
268
+ }
269
+ retainBucket(bucket) {
270
+ if (bucket.size === 0) return;
271
+ this.reservedBuckets.delete(bucket.key);
272
+ if (!this.slots.has(bucket.key)) this.slots.set(bucket.key, bucket);
273
+ }
274
+ releaseBucket(bucket) {
275
+ if (bucket.size > 0) return;
276
+ this.slots.delete(bucket.key);
277
+ if (bucket.hasSubscribers()) {
278
+ this.reservedBuckets.set(bucket.key, bucket);
279
+ } else {
280
+ this.reservedBuckets.delete(bucket.key);
281
+ }
282
+ }
283
+ dropIfReserved(key) {
284
+ const reserved = this.reservedBuckets.get(key);
285
+ if (reserved && reserved.size === 0 && !reserved.hasSubscribers()) {
286
+ this.reservedBuckets.delete(key);
287
+ }
235
288
  }
236
289
  setOneByKey(key, slots) {
237
- this.slots.set(key, slots);
290
+ const bucket = this.getOrReserveBucket(key);
291
+ if (bucket !== slots) {
292
+ bucket.clear();
293
+ for (const slot of slots) bucket.add(slot);
294
+ }
295
+ if (bucket.size > 0) this.retainBucket(bucket);
296
+ else this.releaseBucket(bucket);
238
297
  }
239
298
  removeOneByKey(key) {
240
299
  this.slots.delete(key);
300
+ this.reservedBuckets.delete(key);
241
301
  }
242
302
  removeManyByKeys(keys) {
243
303
  for (const key of keys) {
244
304
  this.slots.delete(key);
305
+ this.reservedBuckets.delete(key);
245
306
  }
246
307
  }
247
308
  getOneByKey(key) {
@@ -250,10 +311,8 @@ var OIMIndexStoreMapDrivenSetBased = class extends OIMIndexStoreSetBased {
250
311
  getManyByKeys(keys) {
251
312
  const result = /* @__PURE__ */ new Map();
252
313
  for (const key of keys) {
253
- const slots = this.getOneByKey(key);
254
- if (slots !== void 0) {
255
- result.set(key, slots);
256
- }
314
+ const slots = this.slots.get(key);
315
+ if (slots !== void 0) result.set(key, slots);
257
316
  }
258
317
  return result;
259
318
  }
@@ -267,6 +326,12 @@ var OIMIndexStoreMapDrivenSetBased = class extends OIMIndexStoreSetBased {
267
326
  return this.slots.size;
268
327
  }
269
328
  clear() {
329
+ for (const bucket of this.slots.values()) {
330
+ bucket.clear();
331
+ if (bucket.hasSubscribers()) {
332
+ this.reservedBuckets.set(bucket.key, bucket);
333
+ }
334
+ }
270
335
  this.slots.clear();
271
336
  }
272
337
  };
@@ -274,10 +339,9 @@ var OIMIndexStoreMapDrivenSetBased = class extends OIMIndexStoreSetBased {
274
339
  // src/abstract/OIMIndexSetBased.ts
275
340
  var OIMIndexSetBased = class extends OIMIndex {
276
341
  constructor(options = {}) {
277
- super(
278
- options.store ?? new OIMIndexStoreMapDrivenSetBased(),
279
- options.comparePks
280
- );
342
+ const store = options.store ?? new OIMIndexStoreMapDrivenSetBased();
343
+ super(store, options.comparePks);
344
+ this.setStore = store;
281
345
  }
282
346
  getBucketSize(bucket) {
283
347
  return bucket.size;
@@ -305,22 +369,36 @@ var OIMIndexSetBased = class extends OIMIndex {
305
369
  for (const slot of slots) pks.add(slot.pk);
306
370
  return pks;
307
371
  }
308
- setSlotsWithComparison(key, newSlots) {
309
- if (this.comparePks) {
310
- const existingSlotsSet = this.store.getOneByKey(key);
311
- if (existingSlotsSet && existingSlotsSet.size === newSlots.size) {
312
- if (this.comparePks(
313
- Array.from(this.slotsToPks(existingSlotsSet)),
314
- Array.from(this.slotsToPks(newSlots))
315
- )) {
316
- return false;
317
- }
318
- } else if (!existingSlotsSet && newSlots.size === 0) {
319
- return false;
320
- }
321
- }
322
- this.store.setOneByKey(key, newSlots);
323
- return true;
372
+ /**
373
+ * True if a comparator is set and considers `bucket`'s current pks equal to
374
+ * `newSlots` — i.e. a `setSlots` would be a no-op and should not emit.
375
+ */
376
+ bucketMatchesComparator(bucket, newSlots) {
377
+ if (!this.comparePks) return false;
378
+ if (bucket.size !== newSlots.size) return false;
379
+ if (bucket.size === 0) return true;
380
+ return this.comparePks(
381
+ Array.from(this.slotsToPks(bucket)),
382
+ Array.from(this.slotsToPks(newSlots))
383
+ );
384
+ }
385
+ };
386
+
387
+ // src/core/OIMKeyedBucketArrayBased.ts
388
+ var OIMKeyedBucketArrayBased = class extends Array {
389
+ constructor(key) {
390
+ super();
391
+ this.key = key;
392
+ }
393
+ static get [Symbol.species]() {
394
+ return Array;
395
+ }
396
+ hasSubscribers() {
397
+ return this.subscribers !== void 0 && this.subscribers.size > 0;
398
+ }
399
+ /** Empty the bucket in place (Array has no `clear`). */
400
+ clear() {
401
+ this.length = 0;
324
402
  }
325
403
  };
326
404
 
@@ -328,17 +406,61 @@ var OIMIndexSetBased = class extends OIMIndex {
328
406
  var OIMIndexStoreMapDrivenArrayBased = class extends OIMIndexStoreArrayBased {
329
407
  constructor() {
330
408
  super(...arguments);
409
+ // Live buckets (non-empty). Enumeration reads these only.
331
410
  this.slots = /* @__PURE__ */ new Map();
411
+ // Empty buckets kept alive by their subscribers, invisible to reads.
412
+ this.reservedBuckets = /* @__PURE__ */ new Map();
413
+ }
414
+ getOrReserveBucket(key) {
415
+ const live = this.slots.get(key);
416
+ if (live) return live;
417
+ let reserved = this.reservedBuckets.get(key);
418
+ if (!reserved) {
419
+ reserved = new OIMKeyedBucketArrayBased(key);
420
+ this.reservedBuckets.set(key, reserved);
421
+ }
422
+ return reserved;
423
+ }
424
+ findBucket(key) {
425
+ return this.slots.get(key) ?? this.reservedBuckets.get(key);
426
+ }
427
+ retainBucket(bucket) {
428
+ if (bucket.length === 0) return;
429
+ this.reservedBuckets.delete(bucket.key);
430
+ if (!this.slots.has(bucket.key)) this.slots.set(bucket.key, bucket);
431
+ }
432
+ releaseBucket(bucket) {
433
+ if (bucket.length > 0) return;
434
+ this.slots.delete(bucket.key);
435
+ if (bucket.hasSubscribers()) {
436
+ this.reservedBuckets.set(bucket.key, bucket);
437
+ } else {
438
+ this.reservedBuckets.delete(bucket.key);
439
+ }
440
+ }
441
+ dropIfReserved(key) {
442
+ const reserved = this.reservedBuckets.get(key);
443
+ if (reserved && reserved.length === 0 && !reserved.hasSubscribers()) {
444
+ this.reservedBuckets.delete(key);
445
+ }
332
446
  }
333
447
  setOneByKey(key, slots) {
334
- this.slots.set(key, slots);
448
+ const bucket = this.getOrReserveBucket(key);
449
+ if (bucket !== slots) {
450
+ bucket.length = 0;
451
+ for (let i = 0; i < slots.length; i++) bucket.push(slots[i]);
452
+ }
453
+ if (bucket.length > 0) this.retainBucket(bucket);
454
+ else this.releaseBucket(bucket);
335
455
  }
336
456
  removeOneByKey(key) {
337
457
  this.slots.delete(key);
458
+ this.reservedBuckets.delete(key);
338
459
  }
339
460
  removeManyByKeys(keys) {
340
461
  for (const key of keys) {
341
462
  this.slots.delete(key);
463
+ this.reservedBuckets.delete(key);
342
464
  }
343
465
  }
344
466
  getOneByKey(key) {
@@ -347,10 +469,8 @@ var OIMIndexStoreMapDrivenArrayBased = class extends OIMIndexStoreArrayBased {
347
469
  getManyByKeys(keys) {
348
470
  const result = /* @__PURE__ */ new Map();
349
471
  for (const key of keys) {
350
- const slots = this.getOneByKey(key);
351
- if (slots !== void 0) {
352
- result.set(key, slots);
353
- }
472
+ const slots = this.slots.get(key);
473
+ if (slots !== void 0) result.set(key, slots);
354
474
  }
355
475
  return result;
356
476
  }
@@ -364,6 +484,12 @@ var OIMIndexStoreMapDrivenArrayBased = class extends OIMIndexStoreArrayBased {
364
484
  return this.slots.size;
365
485
  }
366
486
  clear() {
487
+ for (const bucket of this.slots.values()) {
488
+ bucket.length = 0;
489
+ if (bucket.hasSubscribers()) {
490
+ this.reservedBuckets.set(bucket.key, bucket);
491
+ }
492
+ }
367
493
  this.slots.clear();
368
494
  }
369
495
  };
@@ -371,10 +497,9 @@ var OIMIndexStoreMapDrivenArrayBased = class extends OIMIndexStoreArrayBased {
371
497
  // src/abstract/OIMIndexArrayBased.ts
372
498
  var OIMIndexArrayBased = class extends OIMIndex {
373
499
  constructor(options = {}) {
374
- super(
375
- options.store ?? new OIMIndexStoreMapDrivenArrayBased(),
376
- options.comparePks
377
- );
500
+ const store = options.store ?? new OIMIndexStoreMapDrivenArrayBased();
501
+ super(store, options.comparePks);
502
+ this.arrayStore = store;
378
503
  }
379
504
  getBucketSize(bucket) {
380
505
  return bucket.length;
@@ -392,10 +517,15 @@ var OIMIndexArrayBased = class extends OIMIndex {
392
517
  return result;
393
518
  }
394
519
  getSlotsByKey(key) {
395
- return this.store.getOneByKey(key) ?? [];
520
+ const bucket = this.store.getOneByKey(key);
521
+ return bucket ? bucket.slice() : [];
396
522
  }
397
523
  getSlotsByKeys(keys) {
398
- return this.store.getManyByKeys(keys);
524
+ const result = /* @__PURE__ */ new Map();
525
+ for (const [key, bucket] of this.store.getManyByKeys(keys)) {
526
+ result.set(key, bucket.slice());
527
+ }
528
+ return result;
399
529
  }
400
530
  slotsToPks(slots) {
401
531
  const pks = [];
@@ -403,22 +533,18 @@ var OIMIndexArrayBased = class extends OIMIndex {
403
533
  for (let i = 0; i < slots.length; i++) pks[i] = slots[i].pk;
404
534
  return pks;
405
535
  }
406
- setSlotsWithComparison(key, newSlots) {
407
- if (this.comparePks) {
408
- const existingSlotsArray = this.store.getOneByKey(key);
409
- if (existingSlotsArray && existingSlotsArray.length === newSlots.length) {
410
- if (this.comparePks(
411
- this.slotsToPks(existingSlotsArray),
412
- this.slotsToPks(newSlots)
413
- )) {
414
- return false;
415
- }
416
- } else if (!existingSlotsArray && newSlots.length === 0) {
417
- return false;
418
- }
419
- }
420
- this.store.setOneByKey(key, newSlots);
421
- return true;
536
+ /**
537
+ * True if a comparator is set and considers `bucket`'s pks equal to
538
+ * `newSlots` — i.e. a `setSlots` would be a no-op and should not emit.
539
+ */
540
+ bucketMatchesComparator(bucket, newSlots) {
541
+ if (!this.comparePks) return false;
542
+ if (bucket.length !== newSlots.length) return false;
543
+ if (bucket.length === 0) return true;
544
+ return this.comparePks(
545
+ this.slotsToPks(bucket),
546
+ this.slotsToPks(newSlots)
547
+ );
422
548
  }
423
549
  };
424
550
 
@@ -507,11 +633,19 @@ var OIMCarrierKeyedEmitter = class {
507
633
  this.scheduleFlush();
508
634
  }
509
635
  markUpdatedKey(key) {
636
+ if (this.subscribedCarriers.size === 0) {
637
+ this.assertNotInFlush();
638
+ return;
639
+ }
510
640
  const carrier = this.resolver.findCarrier(key);
511
641
  if (carrier) this.markUpdatedCarrier(carrier);
512
642
  else this.assertNotInFlush();
513
643
  }
514
644
  markUpdatedKeys(keys) {
645
+ if (this.subscribedCarriers.size === 0) {
646
+ this.assertNotInFlush();
647
+ return;
648
+ }
515
649
  for (let i = 0; i < keys.length; i++) this.markUpdatedKey(keys[i]);
516
650
  }
517
651
  markAllUpdated() {
@@ -611,8 +745,8 @@ var OIMKeyedCarrierResolver = class {
611
745
 
612
746
  // src/abstract/OIMReactiveIndex.ts
613
747
  var OIMReactiveIndex = class {
614
- constructor(queue, createIndex) {
615
- this.updateEmitter = new OIMCarrierKeyedEmitter(queue, new OIMKeyedCarrierResolver());
748
+ constructor(queue, createIndex, createResolver = () => new OIMKeyedCarrierResolver()) {
749
+ this.updateEmitter = new OIMCarrierKeyedEmitter(queue, createResolver());
616
750
  this.index = createIndex(this.updateEmitter);
617
751
  }
618
752
  getEntitiesByKey(key) {
@@ -668,8 +802,8 @@ var OIMReactiveIndex = class {
668
802
 
669
803
  // src/abstract/OIMReactiveIndexSetBased.ts
670
804
  var OIMReactiveIndexSetBased = class extends OIMReactiveIndex {
671
- constructor(queue, createIndex) {
672
- super(queue, createIndex);
805
+ constructor(queue, createIndex, createResolver) {
806
+ super(queue, createIndex, createResolver);
673
807
  }
674
808
  getPksByKey(key) {
675
809
  return this.index.getPksByKey(key);
@@ -687,8 +821,8 @@ var OIMReactiveIndexSetBased = class extends OIMReactiveIndex {
687
821
 
688
822
  // src/abstract/OIMReactiveIndexArrayBased.ts
689
823
  var OIMReactiveIndexArrayBased = class extends OIMReactiveIndex {
690
- constructor(queue, createIndex) {
691
- super(queue, createIndex);
824
+ constructor(queue, createIndex, createResolver) {
825
+ super(queue, createIndex, createResolver);
692
826
  }
693
827
  getPksByKey(key) {
694
828
  return this.index.getPksByKey(key);
@@ -963,10 +1097,24 @@ var OIMPkSelectorFactory = class {
963
1097
  }
964
1098
  };
965
1099
 
1100
+ // src/core/OIMKeyDomainNative.ts
1101
+ var OIMKeyDomainNative = class {
1102
+ createMap() {
1103
+ return /* @__PURE__ */ new Map();
1104
+ }
1105
+ createSet() {
1106
+ return /* @__PURE__ */ new Set();
1107
+ }
1108
+ canonicalize(key) {
1109
+ return key;
1110
+ }
1111
+ };
1112
+
966
1113
  // src/core/OIMCollectionStoreMapDriven.ts
967
1114
  var OIMCollectionStoreMapDriven = class extends OIMCollectionStore {
968
1115
  constructor() {
969
1116
  super(...arguments);
1117
+ this.keyDomain = new OIMKeyDomainNative();
970
1118
  this.slots = /* @__PURE__ */ new Map();
971
1119
  // Slots reserved by indexes for PKs whose entity has not arrived yet.
972
1120
  // They carry `item: undefined` and live apart from `slots` so they do not
@@ -1139,6 +1287,10 @@ var OIMCollection = class {
1139
1287
  this.store = opts?.store ?? new OIMCollectionStoreMapDriven();
1140
1288
  this.updateEntity = opts?.updateEntity ?? new OIMEntityUpdaterFactory().createMergeEntityUpdater();
1141
1289
  }
1290
+ /** The collection's PK keying strategy — read by indexes and wrappers. */
1291
+ get keyDomain() {
1292
+ return this.store.keyDomain;
1293
+ }
1142
1294
  getOneByPk(pk) {
1143
1295
  return this.store.getOneByPk(pk);
1144
1296
  }
@@ -1219,15 +1371,12 @@ var OIMCollection = class {
1219
1371
  `[OIMCollection]: PK is required to upsert an entity ${JSON.stringify(entity)}`
1220
1372
  );
1221
1373
  }
1222
- const existingEntity = this.store.getOneByPk(pk);
1223
- if (existingEntity) {
1224
- return this.store.setOneByPk(
1225
- pk,
1226
- this.updateEntity(entity, existingEntity)
1227
- );
1228
- } else {
1229
- return this.store.setOneByPk(pk, entity);
1374
+ const slot = this.store.getSlotByPk(pk);
1375
+ if (slot !== void 0 && slot.item !== void 0) {
1376
+ slot.item = this.updateEntity(entity, slot.item);
1377
+ return slot;
1230
1378
  }
1379
+ return this.store.setOneByPk(pk, entity);
1231
1380
  }
1232
1381
  destroy() {
1233
1382
  this.store.destroy();
@@ -1240,13 +1389,13 @@ var OIMReactiveCollection = class extends OIMCollection {
1240
1389
  constructor(queue, opts) {
1241
1390
  super(opts);
1242
1391
  this.anyUpdateHandlers = /* @__PURE__ */ new Set();
1243
- this.pendingAnyUpdatePks = /* @__PURE__ */ new Set();
1392
+ this.pendingAnyUpdatePks = this.store.keyDomain.createSet();
1244
1393
  this.isAnyUpdateClearPending = false;
1245
1394
  this.isAnyUpdateScheduled = false;
1246
1395
  this.runAnyUpdate = () => {
1247
1396
  if (!this.isAnyUpdateScheduled) return;
1248
1397
  this.isAnyUpdateScheduled = false;
1249
- const pks = this.isAnyUpdateClearPending ? [] : Array.from(this.pendingAnyUpdatePks);
1398
+ const pks = this.isAnyUpdateClearPending ? [] : Array.from(this.pendingAnyUpdatePks.values());
1250
1399
  this.pendingAnyUpdatePks.clear();
1251
1400
  this.isAnyUpdateClearPending = false;
1252
1401
  const snapshot = Array.from(this.anyUpdateHandlers);
@@ -1405,38 +1554,40 @@ var OIMIndexManualOrderedArrayBased = class extends OIMIndexArrayBased {
1405
1554
  if (key === void 0) {
1406
1555
  const keys = this.getKeys();
1407
1556
  if (keys.length > 0) {
1408
- this.store.clear();
1557
+ this.arrayStore.clear();
1409
1558
  this.emitUpdate(Array.from(keys));
1410
1559
  }
1411
1560
  return;
1412
1561
  }
1413
- if (this.store.getOneByKey(key) !== void 0) {
1414
- this.store.removeOneByKey(key);
1562
+ const bucket = this.arrayStore.findBucket(key);
1563
+ if (bucket && bucket.length > 0) {
1564
+ bucket.length = 0;
1565
+ this.arrayStore.releaseBucket(bucket);
1415
1566
  this.emitUpdate([key]);
1416
1567
  }
1417
1568
  }
1418
1569
  pushSlot(key, slot) {
1419
- const list = this.getOrCreateList(key);
1420
- const index = list.length;
1421
- list.push(slot);
1570
+ const bucket = this.arrayStore.getOrReserveBucket(key);
1571
+ const index = bucket.length;
1572
+ bucket.push(slot);
1573
+ this.arrayStore.retainBucket(bucket);
1422
1574
  this.emitUpdate([key]);
1423
1575
  return index;
1424
1576
  }
1425
1577
  insertSlotAt(key, index, slot) {
1426
- const list = this.getOrCreateList(key);
1427
- const safeIndex = Math.max(0, Math.min(index, list.length));
1428
- list.splice(safeIndex, 0, slot);
1578
+ const bucket = this.arrayStore.getOrReserveBucket(key);
1579
+ const safeIndex = Math.max(0, Math.min(index, bucket.length));
1580
+ bucket.splice(safeIndex, 0, slot);
1581
+ this.arrayStore.retainBucket(bucket);
1429
1582
  this.emitUpdate([key]);
1430
1583
  return safeIndex;
1431
1584
  }
1432
1585
  removeAt(key, index) {
1433
- const list = this.store.getOneByKey(key);
1434
- if (!list) return void 0;
1435
- if (index < 0 || index >= list.length) return void 0;
1436
- const [removed] = list.splice(index, 1);
1437
- if (list.length === 0) {
1438
- this.store.removeOneByKey(key);
1439
- }
1586
+ const bucket = this.arrayStore.findBucket(key);
1587
+ if (!bucket) return void 0;
1588
+ if (index < 0 || index >= bucket.length) return void 0;
1589
+ const [removed] = bucket.splice(index, 1);
1590
+ if (bucket.length === 0) this.arrayStore.releaseBucket(bucket);
1440
1591
  this.emitUpdate([key]);
1441
1592
  return removed;
1442
1593
  }
@@ -1445,35 +1596,33 @@ var OIMIndexManualOrderedArrayBased = class extends OIMIndexArrayBased {
1445
1596
  * Returns the number actually removed (clamped to bounds; 0 if nothing).
1446
1597
  */
1447
1598
  removeRange(key, index, count) {
1448
- const list = this.store.getOneByKey(key);
1449
- if (!list) return 0;
1450
- if (index < 0 || index >= list.length || count <= 0) return 0;
1451
- const actual = Math.min(count, list.length - index);
1452
- list.splice(index, actual);
1453
- if (list.length === 0) {
1454
- this.store.removeOneByKey(key);
1455
- }
1599
+ const bucket = this.arrayStore.findBucket(key);
1600
+ if (!bucket) return 0;
1601
+ if (index < 0 || index >= bucket.length || count <= 0) return 0;
1602
+ const actual = Math.min(count, bucket.length - index);
1603
+ bucket.splice(index, actual);
1604
+ if (bucket.length === 0) this.arrayStore.releaseBucket(bucket);
1456
1605
  this.emitUpdate([key]);
1457
1606
  return actual;
1458
1607
  }
1459
1608
  move(key, fromIndex, toIndex) {
1460
- const list = this.store.getOneByKey(key);
1461
- if (!list) return void 0;
1462
- if (fromIndex < 0 || fromIndex >= list.length) return void 0;
1463
- if (toIndex < 0 || toIndex >= list.length) return void 0;
1464
- if (fromIndex === toIndex) return list[fromIndex];
1465
- const [slot] = list.splice(fromIndex, 1);
1609
+ const bucket = this.arrayStore.findBucket(key);
1610
+ if (!bucket) return void 0;
1611
+ if (fromIndex < 0 || fromIndex >= bucket.length) return void 0;
1612
+ if (toIndex < 0 || toIndex >= bucket.length) return void 0;
1613
+ if (fromIndex === toIndex) return bucket[fromIndex];
1614
+ const [slot] = bucket.splice(fromIndex, 1);
1466
1615
  if (slot === void 0) return void 0;
1467
- list.splice(toIndex, 0, slot);
1616
+ bucket.splice(toIndex, 0, slot);
1468
1617
  this.emitUpdate([key]);
1469
1618
  return slot;
1470
1619
  }
1471
1620
  /** Replace the slot at `index` in place. Returns the safe index, or -1. */
1472
1621
  setSlotAt(key, index, slot) {
1473
- const list = this.store.getOneByKey(key);
1474
- if (!list) return -1;
1475
- if (index < 0 || index >= list.length) return -1;
1476
- list[index] = slot;
1622
+ const bucket = this.arrayStore.findBucket(key);
1623
+ if (!bucket) return -1;
1624
+ if (index < 0 || index >= bucket.length) return -1;
1625
+ bucket[index] = slot;
1477
1626
  this.emitUpdate([key]);
1478
1627
  return index;
1479
1628
  }
@@ -1483,36 +1632,33 @@ var OIMIndexManualOrderedArrayBased = class extends OIMIndexArrayBased {
1483
1632
  * Returns the number actually moved (0 if a no-op / out of bounds).
1484
1633
  */
1485
1634
  moveRange(key, from, to, count) {
1486
- const list = this.store.getOneByKey(key);
1487
- if (!list) return 0;
1488
- if (from < 0 || from >= list.length || count <= 0) return 0;
1489
- const actual = Math.min(count, list.length - from);
1490
- const insertAt = Math.max(0, Math.min(to, list.length - actual));
1635
+ const bucket = this.arrayStore.findBucket(key);
1636
+ if (!bucket) return 0;
1637
+ if (from < 0 || from >= bucket.length || count <= 0) return 0;
1638
+ const actual = Math.min(count, bucket.length - from);
1639
+ const insertAt = Math.max(0, Math.min(to, bucket.length - actual));
1491
1640
  if (insertAt === from) return 0;
1492
- const block = list.splice(from, actual);
1493
- list.splice(insertAt, 0, ...block);
1641
+ const block = bucket.splice(from, actual);
1642
+ bucket.splice(insertAt, 0, ...block);
1494
1643
  this.emitUpdate([key]);
1495
1644
  return actual;
1496
1645
  }
1497
1646
  resetSlots(key, slots) {
1498
1647
  if (slots.length === 0) {
1499
- if (this.store.getOneByKey(key) !== void 0) {
1500
- this.store.removeOneByKey(key);
1648
+ const bucket2 = this.arrayStore.findBucket(key);
1649
+ if (bucket2 && bucket2.length > 0) {
1650
+ bucket2.length = 0;
1651
+ this.arrayStore.releaseBucket(bucket2);
1501
1652
  this.emitUpdate([key]);
1502
1653
  }
1503
1654
  return;
1504
1655
  }
1505
- this.store.setOneByKey(key, slots.slice());
1656
+ const bucket = this.arrayStore.getOrReserveBucket(key);
1657
+ bucket.length = 0;
1658
+ for (let i = 0; i < slots.length; i++) bucket.push(slots[i]);
1659
+ this.arrayStore.retainBucket(bucket);
1506
1660
  this.emitUpdate([key]);
1507
1661
  }
1508
- getOrCreateList(key) {
1509
- let list = this.store.getOneByKey(key);
1510
- if (!list) {
1511
- list = [];
1512
- this.store.setOneByKey(key, list);
1513
- }
1514
- return list;
1515
- }
1516
1662
  };
1517
1663
 
1518
1664
  // src/core/OIMCollectionIndexManualOrderedArrayBased.ts
@@ -2030,43 +2176,68 @@ var OIMIndexManualArrayBased = class extends OIMIndexArrayBased {
2030
2176
  super(options);
2031
2177
  }
2032
2178
  setSlots(key, slots) {
2033
- const hasChanges = this.setSlotsWithComparison(key, slots);
2034
- if (hasChanges) {
2035
- this.emitUpdateOne(key);
2036
- }
2179
+ const bucket = this.arrayStore.getOrReserveBucket(key);
2180
+ if (this.bucketMatchesComparator(bucket, slots)) return;
2181
+ bucket.length = 0;
2182
+ for (let i = 0; i < slots.length; i++) bucket.push(slots[i]);
2183
+ if (bucket.length > 0) this.arrayStore.retainBucket(bucket);
2184
+ else this.arrayStore.releaseBucket(bucket);
2185
+ this.emitBucketChanged(bucket);
2037
2186
  }
2038
2187
  /**
2039
- * Appends pre-deduplicated slots to a key's bucket IN PLACE (no copy of the
2040
- * existing bucket) and emits once. Callers own dedup (so this stays O(added),
2041
- * not O(bucket)); used by collection-bound indexes for fast `addPks`.
2188
+ * Appends pre-deduplicated slots to a key's bucket IN PLACE and emits once.
2189
+ * O(added); callers own dedup.
2042
2190
  */
2043
2191
  appendSlots(key, slots) {
2044
2192
  if (slots.length === 0) return;
2045
- let bucket = this.store.getOneByKey(key);
2046
- if (!bucket) {
2047
- bucket = [];
2048
- this.store.setOneByKey(key, bucket);
2049
- }
2193
+ const bucket = this.arrayStore.getOrReserveBucket(key);
2050
2194
  for (let i = 0; i < slots.length; i++) bucket.push(slots[i]);
2051
- this.emitUpdateOne(key);
2195
+ this.arrayStore.retainBucket(bucket);
2196
+ this.emitBucketChanged(bucket);
2052
2197
  }
2053
2198
  /**
2054
- * Clear all primary keys for a specific index key, or all keys if no key specified
2199
+ * Clear all primary keys for a specific index key, or all keys if none given.
2055
2200
  */
2056
2201
  clear(key) {
2057
2202
  if (key === void 0) {
2058
- const allKeys = this.store.getAllKeys();
2203
+ const allKeys = this.arrayStore.getAllKeys();
2059
2204
  if (allKeys.length > 0) {
2060
- this.store.clear();
2205
+ this.arrayStore.clear();
2061
2206
  this.emitUpdate(allKeys);
2062
2207
  }
2063
2208
  } else {
2064
- if (this.store.getOneByKey(key) !== void 0) {
2065
- this.store.removeOneByKey(key);
2066
- this.emitUpdateOne(key);
2209
+ const bucket = this.arrayStore.findBucket(key);
2210
+ if (bucket && bucket.length > 0) {
2211
+ bucket.length = 0;
2212
+ this.arrayStore.releaseBucket(bucket);
2213
+ this.emitBucketChanged(bucket);
2067
2214
  }
2068
2215
  }
2069
2216
  }
2217
+ /**
2218
+ * Emit that a bucket changed. Base (non-reactive) path emits by key on the
2219
+ * plain event emitter; the reactive subclass overrides this to mark the
2220
+ * bucket carrier directly (O(1), no key→carrier lookup).
2221
+ */
2222
+ emitBucketChanged(bucket) {
2223
+ this.emitUpdateOne(bucket.key);
2224
+ }
2225
+ };
2226
+
2227
+ // src/core/OIMBucketCarrierResolver.ts
2228
+ var OIMBucketCarrierResolver = class {
2229
+ constructor(store) {
2230
+ this.store = store;
2231
+ }
2232
+ getOrReserveCarrier(key) {
2233
+ return this.store.getOrReserveBucket(key);
2234
+ }
2235
+ findCarrier(key) {
2236
+ return this.store.findBucket(key);
2237
+ }
2238
+ onCarrierEmptied(carrier) {
2239
+ this.store.dropIfReserved(carrier.key);
2240
+ }
2070
2241
  };
2071
2242
 
2072
2243
  // src/core/OIMReactiveIndexManualArrayBased.ts
@@ -2075,6 +2246,10 @@ var OIMIndexManualArrayBasedReactive = class extends OIMIndexManualArrayBased {
2075
2246
  super(opts);
2076
2247
  this.updateEmitter = updateEmitter;
2077
2248
  }
2249
+ // Deliver straight off the bucket the write just touched — O(1).
2250
+ emitBucketChanged(bucket) {
2251
+ this.updateEmitter.markUpdatedCarrier(bucket);
2252
+ }
2078
2253
  emitUpdate(keys) {
2079
2254
  this.updateEmitter.markUpdatedKeys(keys);
2080
2255
  }
@@ -2084,12 +2259,15 @@ var OIMIndexManualArrayBasedReactive = class extends OIMIndexManualArrayBased {
2084
2259
  };
2085
2260
  var OIMReactiveIndexManualArrayBased = class extends OIMReactiveIndexArrayBased {
2086
2261
  constructor(queue, opts) {
2087
- super(queue, (updateEmitter) => {
2088
- return new OIMIndexManualArrayBasedReactive(
2089
- updateEmitter,
2090
- opts?.indexOptions
2091
- );
2092
- });
2262
+ const store = opts?.indexOptions?.store ?? new OIMIndexStoreMapDrivenArrayBased();
2263
+ super(
2264
+ queue,
2265
+ (updateEmitter) => new OIMIndexManualArrayBasedReactive(updateEmitter, {
2266
+ comparePks: opts?.indexOptions?.comparePks,
2267
+ store
2268
+ }),
2269
+ () => new OIMBucketCarrierResolver(store)
2270
+ );
2093
2271
  }
2094
2272
  setSlots(key, slots) {
2095
2273
  this.index.setSlots(key, slots);
@@ -2106,6 +2284,7 @@ var OIMReactiveIndexManualArrayBased = class extends OIMReactiveIndexArrayBased
2106
2284
  var OIMReactiveCollectionIndexManualArrayBased = class extends OIMReactiveIndexManualArrayBased {
2107
2285
  constructor(queue, opts) {
2108
2286
  const resolveSlot = opts.collection !== void 0 ? (pk) => opts.collection.getOrReserveSlotByPk(pk) : opts.resolveSlot;
2287
+ const pkDomain = opts.collection?.keyDomain ?? new OIMKeyDomainNative();
2109
2288
  super(queue, {
2110
2289
  indexOptions: opts.indexOptions
2111
2290
  });
@@ -2113,9 +2292,10 @@ var OIMReactiveCollectionIndexManualArrayBased = class extends OIMReactiveIndexM
2113
2292
  // a Set from the whole bucket on every call). Kept in sync by setSlots/clear.
2114
2293
  this.pksByKey = /* @__PURE__ */ new Map();
2115
2294
  this.resolveSlot = resolveSlot;
2295
+ this.pkDomain = pkDomain;
2116
2296
  }
2117
2297
  setPks(key, pks) {
2118
- const pkSet = /* @__PURE__ */ new Set();
2298
+ const pkSet = this.pkDomain.createSet();
2119
2299
  for (let i = 0; i < pks.length; i++) pkSet.add(pks[i]);
2120
2300
  this.pksByKey.set(key, pkSet);
2121
2301
  super.setSlots(key, this.resolveSlots(pks));
@@ -2158,7 +2338,7 @@ var OIMReactiveCollectionIndexManualArrayBased = class extends OIMReactiveIndexM
2158
2338
  membershipOf(key) {
2159
2339
  let pkSet = this.pksByKey.get(key);
2160
2340
  if (!pkSet) {
2161
- pkSet = /* @__PURE__ */ new Set();
2341
+ pkSet = this.pkDomain.createSet();
2162
2342
  const existing = this.index.getSlotsByKey(key);
2163
2343
  for (let i = 0; i < existing.length; i++) {
2164
2344
  pkSet.add(existing[i].pk);
@@ -2192,24 +2372,98 @@ var OIMReactiveCollectionIndexManualArrayBased = class extends OIMReactiveIndexM
2192
2372
  var OIMIndexManualSetBased = class extends OIMIndexSetBased {
2193
2373
  constructor(options = {}) {
2194
2374
  super(options);
2375
+ this.pkDomain = options.pkDomain ?? new OIMKeyDomainNative();
2195
2376
  }
2196
2377
  setSlots(key, slots) {
2197
- const hasChanges = this.setSlotsWithComparison(key, new Set(slots));
2198
- if (hasChanges) {
2199
- this.emitUpdateOne(key);
2378
+ const bucket = this.setStore.getOrReserveBucket(key);
2379
+ this.replaceBucketContents(bucket, slots);
2380
+ }
2381
+ // --- pk-oriented writes: resolve the bucket ONCE and use its own membership
2382
+ // (pk → slot) for dedup/removal, so a composite (trie) key is walked a single
2383
+ // time instead of once for the bucket and once for a separate membership map.
2384
+ setPks(key, pks, resolveSlot) {
2385
+ const bucket = this.setStore.getOrReserveBucket(key);
2386
+ const slots = [];
2387
+ for (let i = 0; i < pks.length; i++) slots.push(resolveSlot(pks[i]));
2388
+ this.replaceBucketContents(bucket, slots);
2389
+ const membership = bucket.membership ?? (bucket.membership = this.pkDomain.createMap());
2390
+ membership.clear();
2391
+ for (let i = 0; i < pks.length; i++) {
2392
+ if (!membership.has(pks[i])) membership.set(pks[i], slots[i]);
2393
+ }
2394
+ }
2395
+ addPks(key, pks, resolveSlot) {
2396
+ if (pks.length === 0) return;
2397
+ const bucket = this.setStore.getOrReserveBucket(key);
2398
+ const membership = this.membershipOf(bucket);
2399
+ let changed = false;
2400
+ for (let i = 0; i < pks.length; i++) {
2401
+ const pk = pks[i];
2402
+ if (membership.has(pk)) continue;
2403
+ const slot = resolveSlot(pk);
2404
+ membership.set(pk, slot);
2405
+ if (!bucket.has(slot)) {
2406
+ bucket.add(slot);
2407
+ changed = true;
2408
+ }
2409
+ }
2410
+ if (!changed) return;
2411
+ this.setStore.retainBucket(bucket);
2412
+ this.emitBucketChanged(bucket);
2413
+ }
2414
+ removePks(key, pks) {
2415
+ if (pks.length === 0) return;
2416
+ const bucket = this.setStore.findBucket(key);
2417
+ if (!bucket || bucket.size === 0) return;
2418
+ const membership = this.membershipOf(bucket);
2419
+ let changed = false;
2420
+ for (let i = 0; i < pks.length; i++) {
2421
+ const slot = membership.get(pks[i]);
2422
+ if (slot) {
2423
+ membership.delete(pks[i]);
2424
+ if (bucket.delete(slot)) changed = true;
2425
+ }
2426
+ }
2427
+ if (!changed) return;
2428
+ if (bucket.size === 0) this.setStore.releaseBucket(bucket);
2429
+ this.emitBucketChanged(bucket);
2430
+ }
2431
+ /**
2432
+ * The bucket's pk → slot membership, lazily created and seeded from the
2433
+ * bucket's current slots (so it stays correct even if slots were written
2434
+ * through the lower-level `setSlots` path).
2435
+ */
2436
+ membershipOf(bucket) {
2437
+ let membership = bucket.membership;
2438
+ if (!membership) {
2439
+ membership = this.pkDomain.createMap();
2440
+ bucket.membership = membership;
2441
+ }
2442
+ if (membership.size === 0 && bucket.size > 0) {
2443
+ for (const slot of bucket) membership.set(slot.pk, slot);
2444
+ }
2445
+ return membership;
2446
+ }
2447
+ replaceBucketContents(bucket, slots) {
2448
+ if (this.comparePks) {
2449
+ const newSet = slots instanceof Set ? slots : new Set(slots);
2450
+ if (this.bucketMatchesComparator(bucket, newSet)) return;
2451
+ bucket.clear();
2452
+ for (const slot of newSet) bucket.add(slot);
2453
+ } else {
2454
+ bucket.clear();
2455
+ for (const slot of slots) bucket.add(slot);
2200
2456
  }
2457
+ if (bucket.size > 0) this.setStore.retainBucket(bucket);
2458
+ else this.setStore.releaseBucket(bucket);
2459
+ this.emitBucketChanged(bucket);
2201
2460
  }
2202
2461
  /**
2203
- * Adds slots to a key's bucket IN PLACE (no copy of the existing Set) and
2204
- * emits once. O(added). Used by collection-bound indexes for fast `addPks`.
2462
+ * Adds slots to a key's bucket IN PLACE and emits once. O(added).
2205
2463
  */
2206
2464
  addSlots(key, slots) {
2207
2465
  if (slots.length === 0) return;
2208
- let bucket = this.store.getOneByKey(key);
2209
- if (!bucket) {
2210
- bucket = /* @__PURE__ */ new Set();
2211
- this.store.setOneByKey(key, bucket);
2212
- }
2466
+ const bucket = this.setStore.getOrReserveBucket(key);
2213
2467
  let changed = false;
2214
2468
  for (let i = 0; i < slots.length; i++) {
2215
2469
  if (!bucket.has(slots[i])) {
@@ -2217,25 +2471,27 @@ var OIMIndexManualSetBased = class extends OIMIndexSetBased {
2217
2471
  changed = true;
2218
2472
  }
2219
2473
  }
2220
- if (changed) this.emitUpdateOne(key);
2474
+ if (!changed) return;
2475
+ this.setStore.retainBucket(bucket);
2476
+ this.emitBucketChanged(bucket);
2221
2477
  }
2222
2478
  /**
2223
2479
  * Removes slots from a key's bucket IN PLACE (O(removed)) and emits once.
2224
2480
  */
2225
2481
  removeSlots(key, slots) {
2226
2482
  if (slots.length === 0) return;
2227
- const bucket = this.store.getOneByKey(key);
2483
+ const bucket = this.setStore.findBucket(key);
2228
2484
  if (!bucket) return;
2229
2485
  let changed = false;
2230
2486
  for (let i = 0; i < slots.length; i++) {
2231
2487
  if (bucket.delete(slots[i])) changed = true;
2232
2488
  }
2233
2489
  if (!changed) return;
2234
- if (bucket.size === 0) this.store.removeOneByKey(key);
2235
- this.emitUpdateOne(key);
2490
+ if (bucket.size === 0) this.setStore.releaseBucket(bucket);
2491
+ this.emitBucketChanged(bucket);
2236
2492
  }
2237
2493
  /**
2238
- * Clear all primary keys for a specific index key, or all keys if no key specified
2494
+ * Clear all primary keys for a specific index key, or all keys if none given.
2239
2495
  */
2240
2496
  clear(key) {
2241
2497
  if (key === void 0) {
@@ -2245,12 +2501,22 @@ var OIMIndexManualSetBased = class extends OIMIndexSetBased {
2245
2501
  this.emitUpdate(allKeys);
2246
2502
  }
2247
2503
  } else {
2248
- if (this.store.getOneByKey(key) !== void 0) {
2249
- this.store.removeOneByKey(key);
2250
- this.emitUpdateOne(key);
2504
+ const bucket = this.setStore.findBucket(key);
2505
+ if (bucket && bucket.size > 0) {
2506
+ bucket.clear();
2507
+ this.setStore.releaseBucket(bucket);
2508
+ this.emitBucketChanged(bucket);
2251
2509
  }
2252
2510
  }
2253
2511
  }
2512
+ /**
2513
+ * Emit that a bucket changed. Base (non-reactive) path emits by key on the
2514
+ * plain event emitter; the reactive subclass overrides this to mark the
2515
+ * bucket carrier directly (O(1), no key→carrier lookup).
2516
+ */
2517
+ emitBucketChanged(bucket) {
2518
+ this.emitUpdateOne(bucket.key);
2519
+ }
2254
2520
  };
2255
2521
 
2256
2522
  // src/core/OIMReactiveIndexManualSetBased.ts
@@ -2259,6 +2525,12 @@ var OIMIndexManualSetBasedReactive = class extends OIMIndexManualSetBased {
2259
2525
  super(opts);
2260
2526
  this.updateEmitter = updateEmitter;
2261
2527
  }
2528
+ // Deliver straight off the bucket the write just touched — O(1), no
2529
+ // key→carrier lookup (the whole point of bucket-as-carrier).
2530
+ emitBucketChanged(bucket) {
2531
+ this.updateEmitter.markUpdatedCarrier(bucket);
2532
+ }
2533
+ // clear(all) still emits by key set (rare path).
2262
2534
  emitUpdate(keys) {
2263
2535
  this.updateEmitter.markUpdatedKeys(keys);
2264
2536
  }
@@ -2268,12 +2540,16 @@ var OIMIndexManualSetBasedReactive = class extends OIMIndexManualSetBased {
2268
2540
  };
2269
2541
  var OIMReactiveIndexManualSetBased = class extends OIMReactiveIndexSetBased {
2270
2542
  constructor(queue, opts) {
2271
- super(queue, (updateEmitter) => {
2272
- return new OIMIndexManualSetBasedReactive(
2273
- updateEmitter,
2274
- opts?.indexOptions
2275
- );
2276
- });
2543
+ const store = opts?.indexOptions?.store ?? new OIMIndexStoreMapDrivenSetBased();
2544
+ super(
2545
+ queue,
2546
+ (updateEmitter) => new OIMIndexManualSetBasedReactive(updateEmitter, {
2547
+ comparePks: opts?.indexOptions?.comparePks,
2548
+ store,
2549
+ pkDomain: opts?.pkDomain
2550
+ }),
2551
+ () => new OIMBucketCarrierResolver(store)
2552
+ );
2277
2553
  }
2278
2554
  setSlots(key, slots) {
2279
2555
  this.index.setSlots(key, slots);
@@ -2294,71 +2570,26 @@ var OIMReactiveCollectionIndexManualSetBased = class extends OIMReactiveIndexMan
2294
2570
  constructor(queue, opts) {
2295
2571
  const resolveSlot = opts.collection !== void 0 ? (pk) => opts.collection.getOrReserveSlotByPk(pk) : opts.resolveSlot;
2296
2572
  super(queue, {
2297
- indexOptions: opts.indexOptions
2573
+ indexOptions: opts.indexOptions,
2574
+ pkDomain: opts.collection?.keyDomain
2298
2575
  });
2299
- // Persistent per-key pk → slot map for O(1) add (dedup) and O(1) remove
2300
- // (slot lookup by pk). Kept in sync by setPks/addPks/removePks/clear.
2301
- this.slotByPk = /* @__PURE__ */ new Map();
2576
+ // Bound once so the hot pk-write path passes a stable function (no per-call
2577
+ // closure allocation).
2578
+ this.resolveRequiredSlot = (pk) => {
2579
+ const slot = this.resolveSlot(pk);
2580
+ if (!slot) return { pk, item: void 0 };
2581
+ return slot;
2582
+ };
2302
2583
  this.resolveSlot = resolveSlot;
2303
2584
  }
2304
2585
  setPks(key, pks) {
2305
- const map = /* @__PURE__ */ new Map();
2306
- const slots = [];
2307
- for (let i = 0; i < pks.length; i++) {
2308
- const pk = pks[i];
2309
- const slot = this.resolveRequiredSlot(pk);
2310
- if (!map.has(pk)) map.set(pk, slot);
2311
- slots.push(slot);
2312
- }
2313
- this.slotByPk.set(key, map);
2314
- super.setSlots(key, slots);
2586
+ this.index.setPks(key, pks, this.resolveRequiredSlot);
2315
2587
  }
2316
2588
  addPks(key, pks) {
2317
- if (pks.length === 0) return;
2318
- const map = this.membershipOf(key);
2319
- const newSlots = [];
2320
- for (const pk of pks) {
2321
- if (map.has(pk)) continue;
2322
- const slot = this.resolveRequiredSlot(pk);
2323
- map.set(pk, slot);
2324
- newSlots.push(slot);
2325
- }
2326
- if (newSlots.length > 0) this.addSlots(key, newSlots);
2589
+ this.index.addPks(key, pks, this.resolveRequiredSlot);
2327
2590
  }
2328
2591
  removePks(key, pks) {
2329
- if (pks.length === 0) return;
2330
- const map = this.membershipOf(key);
2331
- if (map.size === 0) return;
2332
- const removed = [];
2333
- for (const pk of pks) {
2334
- const slot = map.get(pk);
2335
- if (slot) {
2336
- map.delete(pk);
2337
- removed.push(slot);
2338
- }
2339
- }
2340
- if (removed.length === 0) return;
2341
- if (map.size === 0) this.slotByPk.delete(key);
2342
- this.removeSlots(key, removed);
2343
- }
2344
- clear(key) {
2345
- if (key === void 0) this.slotByPk.clear();
2346
- else this.slotByPk.delete(key);
2347
- super.clear(key);
2348
- }
2349
- /**
2350
- * The pk→slot map for a key, lazily seeded from the current bucket so it
2351
- * stays correct even if slots were set through a lower-level path.
2352
- */
2353
- membershipOf(key) {
2354
- let map = this.slotByPk.get(key);
2355
- if (!map) {
2356
- map = /* @__PURE__ */ new Map();
2357
- const existing = this.index.getSlotsByKey(key);
2358
- for (const slot of existing) map.set(slot.pk, slot);
2359
- this.slotByPk.set(key, map);
2360
- }
2361
- return map;
2592
+ this.index.removePks(key, pks);
2362
2593
  }
2363
2594
  getEntitiesByKey(key) {
2364
2595
  return super.getEntitiesByKey(key);
@@ -2366,50 +2597,459 @@ var OIMReactiveCollectionIndexManualSetBased = class extends OIMReactiveIndexMan
2366
2597
  getEntitiesByKeys(keys) {
2367
2598
  return super.getEntitiesByKeys(keys);
2368
2599
  }
2369
- resolveRequiredSlot(pk) {
2370
- const slot = this.resolveSlot(pk);
2371
- if (!slot) return { pk, item: void 0 };
2372
- return slot;
2373
- }
2374
2600
  };
2375
2601
 
2376
- // src/core/OIMDerivedCollectionIndexSetBased.ts
2377
- var OIMDerivedCollectionIndexSetBased = class extends OIMReactiveCollectionIndexManualSetBased {
2378
- constructor(queue, collection, opts) {
2379
- super(queue, {
2380
- collection,
2381
- indexOptions: opts.indexOptions
2382
- });
2383
- this.keysByPk = /* @__PURE__ */ new Map();
2384
- this.onCollectionUpdate = (pks) => {
2385
- if (pks.length === 0) {
2386
- this.rebuildFromCollection();
2387
- return;
2602
+ // src/core/OIMTrieMap.ts
2603
+ var OIMTrieMap = class {
2604
+ constructor() {
2605
+ this.root = { hasValue: false };
2606
+ this._size = 0;
2607
+ }
2608
+ /** Number of stored paths (terminal nodes), not the number of trie nodes. */
2609
+ get size() {
2610
+ return this._size;
2611
+ }
2612
+ set(path, value) {
2613
+ let node = this.root;
2614
+ for (let i = 0; i < path.length; i++) {
2615
+ let children = node.children;
2616
+ if (!children) {
2617
+ children = /* @__PURE__ */ new Map();
2618
+ node.children = children;
2388
2619
  }
2389
- for (const pk of pks) {
2390
- this.syncPk(pk);
2620
+ let next = children.get(path[i]);
2621
+ if (!next) {
2622
+ next = { hasValue: false };
2623
+ children.set(path[i], next);
2391
2624
  }
2392
- };
2393
- this.collection = collection;
2394
- this.selectIndexKeys = opts.selectIndexKeys;
2395
- this.unsubscribeFromCollection = collection.emitter.on(
2396
- 0 /* UPDATE */,
2397
- (payload) => this.onCollectionUpdate(payload.pks)
2398
- );
2399
- if (opts.buildInitial !== false) {
2400
- this.rebuildFromCollection();
2625
+ node = next;
2626
+ }
2627
+ if (!node.hasValue) {
2628
+ node.hasValue = true;
2629
+ this._size++;
2630
+ }
2631
+ node.value = value;
2632
+ }
2633
+ get(path) {
2634
+ const node = this.findNode(path);
2635
+ return node && node.hasValue ? node.value : void 0;
2636
+ }
2637
+ has(path) {
2638
+ const node = this.findNode(path);
2639
+ return !!node && node.hasValue;
2640
+ }
2641
+ delete(path) {
2642
+ const parents = [];
2643
+ let node = this.root;
2644
+ for (let i = 0; i < path.length; i++) {
2645
+ const next = node.children?.get(path[i]);
2646
+ if (!next) return false;
2647
+ parents.push(node);
2648
+ node = next;
2649
+ }
2650
+ if (!node.hasValue) return false;
2651
+ node.hasValue = false;
2652
+ node.value = void 0;
2653
+ this._size--;
2654
+ let child = node;
2655
+ for (let i = parents.length - 1; i >= 0; i--) {
2656
+ if (child.hasValue || child.children && child.children.size > 0) {
2657
+ break;
2658
+ }
2659
+ const parent = parents[i];
2660
+ parent.children.delete(path[i]);
2661
+ if (parent.children.size === 0) parent.children = void 0;
2662
+ child = parent;
2401
2663
  }
2664
+ return true;
2402
2665
  }
2403
- rebuildFromCollection() {
2404
- this.keysByPk.clear();
2405
- super.clear();
2406
- const slotsByKey = /* @__PURE__ */ new Map();
2407
- const slots = this.collection.getAllSlots();
2408
- for (const slot of slots) {
2409
- if (slot.item === void 0) continue;
2410
- const keys = this.normalizeKeys(this.selectIndexKeys(slot.item));
2411
- if (keys.length === 0) continue;
2412
- this.keysByPk.set(slot.pk, new Set(keys));
2666
+ clear() {
2667
+ this.root.children = void 0;
2668
+ this.root.value = void 0;
2669
+ this.root.hasValue = false;
2670
+ this._size = 0;
2671
+ }
2672
+ /** Depth-first iteration over `[path, value]` for every stored path. */
2673
+ *entries() {
2674
+ const stack = [{ node: this.root, path: [] }];
2675
+ while (stack.length > 0) {
2676
+ const frame = stack.pop();
2677
+ const { node, path } = frame;
2678
+ if (node.hasValue) {
2679
+ yield [path, node.value];
2680
+ }
2681
+ if (node.children) {
2682
+ for (const [segment, child] of node.children) {
2683
+ stack.push({ node: child, path: [...path, segment] });
2684
+ }
2685
+ }
2686
+ }
2687
+ }
2688
+ *keys() {
2689
+ for (const [path] of this.entries()) yield path;
2690
+ }
2691
+ *values() {
2692
+ for (const [, value] of this.entries()) yield value;
2693
+ }
2694
+ findNode(path) {
2695
+ let node = this.root;
2696
+ for (let i = 0; i < path.length; i++) {
2697
+ node = node.children?.get(path[i]);
2698
+ if (!node) return void 0;
2699
+ }
2700
+ return node;
2701
+ }
2702
+ };
2703
+
2704
+ // src/core/OIMIndexStoreTrieDrivenSetBased.ts
2705
+ var OIMIndexStoreTrieDrivenSetBased = class extends OIMIndexStoreSetBased {
2706
+ constructor() {
2707
+ super(...arguments);
2708
+ this.slots = new OIMTrieMap();
2709
+ this.reservedBuckets = new OIMTrieMap();
2710
+ }
2711
+ getOrReserveBucket(key) {
2712
+ const live = this.slots.get(key);
2713
+ if (live) return live;
2714
+ let reserved = this.reservedBuckets.get(key);
2715
+ if (!reserved) {
2716
+ reserved = new OIMKeyedBucketSetBased(key);
2717
+ this.reservedBuckets.set(key, reserved);
2718
+ }
2719
+ return reserved;
2720
+ }
2721
+ findBucket(key) {
2722
+ return this.slots.get(key) ?? this.reservedBuckets.get(key);
2723
+ }
2724
+ retainBucket(bucket) {
2725
+ if (bucket.size === 0) return;
2726
+ this.reservedBuckets.delete(bucket.key);
2727
+ if (!this.slots.has(bucket.key)) this.slots.set(bucket.key, bucket);
2728
+ }
2729
+ releaseBucket(bucket) {
2730
+ if (bucket.size > 0) return;
2731
+ this.slots.delete(bucket.key);
2732
+ if (bucket.hasSubscribers()) {
2733
+ this.reservedBuckets.set(bucket.key, bucket);
2734
+ } else {
2735
+ this.reservedBuckets.delete(bucket.key);
2736
+ }
2737
+ }
2738
+ dropIfReserved(key) {
2739
+ const reserved = this.reservedBuckets.get(key);
2740
+ if (reserved && reserved.size === 0 && !reserved.hasSubscribers()) {
2741
+ this.reservedBuckets.delete(key);
2742
+ }
2743
+ }
2744
+ setOneByKey(key, slots) {
2745
+ const bucket = this.getOrReserveBucket(key);
2746
+ if (bucket !== slots) {
2747
+ bucket.clear();
2748
+ for (const slot of slots) bucket.add(slot);
2749
+ }
2750
+ if (bucket.size > 0) this.retainBucket(bucket);
2751
+ else this.releaseBucket(bucket);
2752
+ }
2753
+ removeOneByKey(key) {
2754
+ this.slots.delete(key);
2755
+ this.reservedBuckets.delete(key);
2756
+ }
2757
+ removeManyByKeys(keys) {
2758
+ for (let i = 0; i < keys.length; i++) {
2759
+ this.slots.delete(keys[i]);
2760
+ this.reservedBuckets.delete(keys[i]);
2761
+ }
2762
+ }
2763
+ getOneByKey(key) {
2764
+ return this.slots.get(key);
2765
+ }
2766
+ getManyByKeys(keys) {
2767
+ const result = /* @__PURE__ */ new Map();
2768
+ for (let i = 0; i < keys.length; i++) {
2769
+ const slots = this.slots.get(keys[i]);
2770
+ if (slots !== void 0) result.set(keys[i], slots);
2771
+ }
2772
+ return result;
2773
+ }
2774
+ getAllKeys() {
2775
+ return Array.from(this.slots.keys());
2776
+ }
2777
+ getAll() {
2778
+ const result = /* @__PURE__ */ new Map();
2779
+ for (const [path, bucket] of this.slots.entries()) {
2780
+ result.set(path, bucket);
2781
+ }
2782
+ return result;
2783
+ }
2784
+ countAll() {
2785
+ return this.slots.size;
2786
+ }
2787
+ clear() {
2788
+ for (const bucket of this.slots.values()) {
2789
+ bucket.clear();
2790
+ if (bucket.hasSubscribers()) {
2791
+ this.reservedBuckets.set(bucket.key, bucket);
2792
+ }
2793
+ }
2794
+ this.slots.clear();
2795
+ }
2796
+ };
2797
+
2798
+ // src/core/OIMReactiveCollectionIndexCompositeTrieSetBased.ts
2799
+ var OIMReactiveCollectionIndexCompositeTrieSetBased = class extends OIMReactiveIndexManualSetBased {
2800
+ constructor(queue, opts) {
2801
+ const resolveSlot = opts.collection !== void 0 ? (pk) => opts.collection.getOrReserveSlotByPk(pk) : opts.resolveSlot;
2802
+ super(queue, {
2803
+ indexOptions: {
2804
+ comparePks: opts.indexOptions?.comparePks,
2805
+ store: opts.indexOptions?.store ?? new OIMIndexStoreTrieDrivenSetBased()
2806
+ },
2807
+ pkDomain: opts.collection?.keyDomain
2808
+ });
2809
+ this.resolveRequiredSlot = (pk) => {
2810
+ const slot = this.resolveSlot(pk);
2811
+ if (!slot) return { pk, item: void 0 };
2812
+ return slot;
2813
+ };
2814
+ this.resolveSlot = resolveSlot;
2815
+ }
2816
+ setPks(key, pks) {
2817
+ this.index.setPks(key, pks, this.resolveRequiredSlot);
2818
+ }
2819
+ addPks(key, pks) {
2820
+ this.index.addPks(key, pks, this.resolveRequiredSlot);
2821
+ }
2822
+ removePks(key, pks) {
2823
+ this.index.removePks(key, pks);
2824
+ }
2825
+ getEntitiesByKey(key) {
2826
+ return super.getEntitiesByKey(key);
2827
+ }
2828
+ getEntitiesByKeys(keys) {
2829
+ return super.getEntitiesByKeys(keys);
2830
+ }
2831
+ };
2832
+
2833
+ // src/core/OIMIndexStoreTrieDrivenArrayBased.ts
2834
+ var OIMIndexStoreTrieDrivenArrayBased = class extends OIMIndexStoreArrayBased {
2835
+ constructor() {
2836
+ super(...arguments);
2837
+ this.slots = new OIMTrieMap();
2838
+ this.reservedBuckets = new OIMTrieMap();
2839
+ }
2840
+ getOrReserveBucket(key) {
2841
+ const live = this.slots.get(key);
2842
+ if (live) return live;
2843
+ let reserved = this.reservedBuckets.get(key);
2844
+ if (!reserved) {
2845
+ reserved = new OIMKeyedBucketArrayBased(key);
2846
+ this.reservedBuckets.set(key, reserved);
2847
+ }
2848
+ return reserved;
2849
+ }
2850
+ findBucket(key) {
2851
+ return this.slots.get(key) ?? this.reservedBuckets.get(key);
2852
+ }
2853
+ retainBucket(bucket) {
2854
+ if (bucket.length === 0) return;
2855
+ this.reservedBuckets.delete(bucket.key);
2856
+ if (!this.slots.has(bucket.key)) this.slots.set(bucket.key, bucket);
2857
+ }
2858
+ releaseBucket(bucket) {
2859
+ if (bucket.length > 0) return;
2860
+ this.slots.delete(bucket.key);
2861
+ if (bucket.hasSubscribers()) {
2862
+ this.reservedBuckets.set(bucket.key, bucket);
2863
+ } else {
2864
+ this.reservedBuckets.delete(bucket.key);
2865
+ }
2866
+ }
2867
+ dropIfReserved(key) {
2868
+ const reserved = this.reservedBuckets.get(key);
2869
+ if (reserved && reserved.length === 0 && !reserved.hasSubscribers()) {
2870
+ this.reservedBuckets.delete(key);
2871
+ }
2872
+ }
2873
+ setOneByKey(key, slots) {
2874
+ const bucket = this.getOrReserveBucket(key);
2875
+ if (bucket !== slots) {
2876
+ bucket.length = 0;
2877
+ for (let i = 0; i < slots.length; i++) bucket.push(slots[i]);
2878
+ }
2879
+ if (bucket.length > 0) this.retainBucket(bucket);
2880
+ else this.releaseBucket(bucket);
2881
+ }
2882
+ removeOneByKey(key) {
2883
+ this.slots.delete(key);
2884
+ this.reservedBuckets.delete(key);
2885
+ }
2886
+ removeManyByKeys(keys) {
2887
+ for (let i = 0; i < keys.length; i++) {
2888
+ this.slots.delete(keys[i]);
2889
+ this.reservedBuckets.delete(keys[i]);
2890
+ }
2891
+ }
2892
+ getOneByKey(key) {
2893
+ return this.slots.get(key);
2894
+ }
2895
+ getManyByKeys(keys) {
2896
+ const result = /* @__PURE__ */ new Map();
2897
+ for (let i = 0; i < keys.length; i++) {
2898
+ const slots = this.slots.get(keys[i]);
2899
+ if (slots !== void 0) result.set(keys[i], slots);
2900
+ }
2901
+ return result;
2902
+ }
2903
+ getAllKeys() {
2904
+ return Array.from(this.slots.keys());
2905
+ }
2906
+ getAll() {
2907
+ const result = /* @__PURE__ */ new Map();
2908
+ for (const [path, bucket] of this.slots.entries()) {
2909
+ result.set(path, bucket);
2910
+ }
2911
+ return result;
2912
+ }
2913
+ countAll() {
2914
+ return this.slots.size;
2915
+ }
2916
+ clear() {
2917
+ for (const bucket of this.slots.values()) {
2918
+ bucket.length = 0;
2919
+ if (bucket.hasSubscribers()) {
2920
+ this.reservedBuckets.set(bucket.key, bucket);
2921
+ }
2922
+ }
2923
+ this.slots.clear();
2924
+ }
2925
+ };
2926
+
2927
+ // src/core/OIMReactiveCollectionIndexCompositeTrieArrayBased.ts
2928
+ var OIMReactiveCollectionIndexCompositeTrieArrayBased = class extends OIMReactiveIndexManualArrayBased {
2929
+ constructor(queue, opts) {
2930
+ const resolveSlot = opts.collection !== void 0 ? (pk) => opts.collection.getOrReserveSlotByPk(pk) : opts.resolveSlot;
2931
+ super(queue, {
2932
+ indexOptions: {
2933
+ comparePks: opts.indexOptions?.comparePks,
2934
+ store: opts.indexOptions?.store ?? new OIMIndexStoreTrieDrivenArrayBased()
2935
+ }
2936
+ });
2937
+ // Persistent per-key-path pk membership for O(1) `addPks` dedup. Trie-keyed
2938
+ // so a rebuilt key path matches by content.
2939
+ this.pksByKey = new OIMTrieMap();
2940
+ this.resolveSlot = resolveSlot;
2941
+ }
2942
+ setPks(key, pks) {
2943
+ const pkSet = /* @__PURE__ */ new Set();
2944
+ for (let i = 0; i < pks.length; i++) pkSet.add(pks[i]);
2945
+ this.pksByKey.set(key, pkSet);
2946
+ super.setSlots(key, this.resolveSlots(pks));
2947
+ }
2948
+ addPks(key, pks) {
2949
+ if (pks.length === 0) return;
2950
+ const pkSet = this.membershipOf(key);
2951
+ const newSlots = [];
2952
+ for (const pk of pks) {
2953
+ if (pkSet.has(pk)) continue;
2954
+ pkSet.add(pk);
2955
+ newSlots.push(this.resolveRequiredSlot(pk));
2956
+ }
2957
+ if (newSlots.length > 0) this.appendSlots(key, newSlots);
2958
+ }
2959
+ removePks(key, pks) {
2960
+ if (pks.length === 0) return;
2961
+ const pksToRemove = new Set(pks);
2962
+ const existingSlots = this.index.getSlotsByKey(key);
2963
+ const nextSlots = existingSlots.filter(
2964
+ (slot) => !pksToRemove.has(slot.pk)
2965
+ );
2966
+ if (nextSlots.length === existingSlots.length) return;
2967
+ const pkSet = this.pksByKey.get(key);
2968
+ if (pkSet) {
2969
+ for (let i = 0; i < pks.length; i++) pkSet.delete(pks[i]);
2970
+ }
2971
+ if (nextSlots.length === 0) this.clear(key);
2972
+ else super.setSlots(key, nextSlots);
2973
+ }
2974
+ clear(key) {
2975
+ if (key === void 0) this.pksByKey.clear();
2976
+ else this.pksByKey.delete(key);
2977
+ super.clear(key);
2978
+ }
2979
+ /**
2980
+ * The membership set for a key path, lazily seeded from the current bucket so
2981
+ * it stays correct even if slots were set through a lower-level path.
2982
+ */
2983
+ membershipOf(key) {
2984
+ let pkSet = this.pksByKey.get(key);
2985
+ if (!pkSet) {
2986
+ pkSet = /* @__PURE__ */ new Set();
2987
+ const existing = this.index.getSlotsByKey(key);
2988
+ for (let i = 0; i < existing.length; i++) {
2989
+ pkSet.add(existing[i].pk);
2990
+ }
2991
+ this.pksByKey.set(key, pkSet);
2992
+ }
2993
+ return pkSet;
2994
+ }
2995
+ getEntitiesByKey(key) {
2996
+ return super.getEntitiesByKey(key);
2997
+ }
2998
+ getEntitiesByKeys(keys) {
2999
+ return super.getEntitiesByKeys(keys);
3000
+ }
3001
+ resolveSlots(pks) {
3002
+ const slots = [];
3003
+ slots.length = pks.length;
3004
+ for (let i = 0; i < pks.length; i++) {
3005
+ slots[i] = this.resolveRequiredSlot(pks[i]);
3006
+ }
3007
+ return slots;
3008
+ }
3009
+ resolveRequiredSlot(pk) {
3010
+ const slot = this.resolveSlot(pk);
3011
+ if (!slot) return { pk, item: void 0 };
3012
+ return slot;
3013
+ }
3014
+ };
3015
+
3016
+ // src/core/OIMDerivedCollectionIndexSetBased.ts
3017
+ var OIMDerivedCollectionIndexSetBased = class extends OIMReactiveCollectionIndexManualSetBased {
3018
+ constructor(queue, collection, opts) {
3019
+ super(queue, {
3020
+ collection,
3021
+ indexOptions: opts.indexOptions
3022
+ });
3023
+ this.keysByPk = /* @__PURE__ */ new Map();
3024
+ this.onCollectionUpdate = (pks) => {
3025
+ if (pks.length === 0) {
3026
+ this.rebuildFromCollection();
3027
+ return;
3028
+ }
3029
+ for (const pk of pks) {
3030
+ this.syncPk(pk);
3031
+ }
3032
+ };
3033
+ this.collection = collection;
3034
+ this.selectIndexKeys = opts.selectIndexKeys;
3035
+ this.unsubscribeFromCollection = collection.emitter.on(
3036
+ 0 /* UPDATE */,
3037
+ (payload) => this.onCollectionUpdate(payload.pks)
3038
+ );
3039
+ if (opts.buildInitial !== false) {
3040
+ this.rebuildFromCollection();
3041
+ }
3042
+ }
3043
+ rebuildFromCollection() {
3044
+ this.keysByPk.clear();
3045
+ super.clear();
3046
+ const slotsByKey = /* @__PURE__ */ new Map();
3047
+ const slots = this.collection.getAllSlots();
3048
+ for (const slot of slots) {
3049
+ if (slot.item === void 0) continue;
3050
+ const keys = this.normalizeKeys(this.selectIndexKeys(slot.item));
3051
+ if (keys.length === 0) continue;
3052
+ this.keysByPk.set(slot.pk, new Set(keys));
2413
3053
  for (const key of keys) {
2414
3054
  let slotsForKey = slotsByKey.get(key);
2415
3055
  if (!slotsForKey) {
@@ -3065,8 +3705,32 @@ var OIMCollectionIndexFactory = class {
3065
3705
  this.queue = queue;
3066
3706
  this.collection = collection;
3067
3707
  }
3068
- setBasedIndex(opts = {}) {
3069
- return new OIMReactiveCollectionIndexManualSetBased(this.queue, {
3708
+ setBasedIndex(opts = {}) {
3709
+ return new OIMReactiveCollectionIndexManualSetBased(this.queue, {
3710
+ collection: this.collection,
3711
+ indexOptions: opts.indexOptions
3712
+ });
3713
+ }
3714
+ /**
3715
+ * Composite (trie-backed) manual Set-based index keyed by an
3716
+ * arbitrary-length key path, e.g. `setPks([userId, projectId, role], pks)`.
3717
+ * Key paths are matched by content in O(arity) — no string concatenation,
3718
+ * no separator collisions, each segment keeps its type. Primitive-keyed
3719
+ * indexes are unaffected and keep their native-`Map` fast path.
3720
+ */
3721
+ compositeSetIndex(opts = {}) {
3722
+ return new OIMReactiveCollectionIndexCompositeTrieSetBased(this.queue, {
3723
+ collection: this.collection,
3724
+ indexOptions: opts.indexOptions
3725
+ });
3726
+ }
3727
+ /**
3728
+ * Composite (trie-backed) manual Array-based (ordered) index keyed by an
3729
+ * arbitrary-length key path, e.g. `setPks([channelId, threadId], pks)`.
3730
+ * Ordered counterpart of `compositeSetIndex`.
3731
+ */
3732
+ compositeArrayIndex(opts = {}) {
3733
+ return new OIMReactiveCollectionIndexCompositeTrieArrayBased(this.queue, {
3070
3734
  collection: this.collection,
3071
3735
  indexOptions: opts.indexOptions
3072
3736
  });
@@ -3319,6 +3983,195 @@ var OIMReactiveObject = class extends OIMObject {
3319
3983
  }
3320
3984
  };
3321
3985
 
3986
+ // src/core/OIMTrieSet.ts
3987
+ var OIMTrieSet = class {
3988
+ constructor() {
3989
+ this.map = new OIMTrieMap();
3990
+ }
3991
+ get size() {
3992
+ return this.map.size;
3993
+ }
3994
+ add(key) {
3995
+ this.map.set(key, true);
3996
+ }
3997
+ has(key) {
3998
+ return this.map.has(key);
3999
+ }
4000
+ delete(key) {
4001
+ return this.map.delete(key);
4002
+ }
4003
+ *values() {
4004
+ yield* this.map.keys();
4005
+ }
4006
+ clear() {
4007
+ this.map.clear();
4008
+ }
4009
+ };
4010
+
4011
+ // src/core/OIMKeyDomainTrie.ts
4012
+ var OIMKeyDomainTrie = class {
4013
+ constructor() {
4014
+ // Interning pool: logical key (by content) → the one canonical reference.
4015
+ this.pool = new OIMTrieMap();
4016
+ }
4017
+ createMap() {
4018
+ return new OIMTrieMap();
4019
+ }
4020
+ createSet() {
4021
+ return new OIMTrieSet();
4022
+ }
4023
+ canonicalize(key) {
4024
+ const existing = this.pool.get(key);
4025
+ if (existing !== void 0) return existing;
4026
+ const canonical = Object.freeze(key.slice());
4027
+ this.pool.set(canonical, canonical);
4028
+ return canonical;
4029
+ }
4030
+ };
4031
+
4032
+ // src/core/OIMCollectionStoreTrieDriven.ts
4033
+ var OIMCollectionStoreTrieDriven = class extends OIMCollectionStore {
4034
+ constructor(keyDomain = new OIMKeyDomainTrie()) {
4035
+ super();
4036
+ this.keyDomain = keyDomain;
4037
+ this.slots = new OIMTrieMap();
4038
+ this.reservedSlots = new OIMTrieMap();
4039
+ }
4040
+ setOneByPk(pk, entity) {
4041
+ const slot = this.slots.get(pk);
4042
+ if (slot) {
4043
+ slot.item = entity;
4044
+ return slot;
4045
+ }
4046
+ const reserved = this.reservedSlots.get(pk);
4047
+ if (reserved) {
4048
+ reserved.item = entity;
4049
+ this.reservedSlots.delete(pk);
4050
+ this.slots.set(pk, reserved);
4051
+ return reserved;
4052
+ }
4053
+ const nextSlot = {
4054
+ pk: this.keyDomain.canonicalize(pk),
4055
+ item: entity
4056
+ };
4057
+ this.slots.set(pk, nextSlot);
4058
+ return nextSlot;
4059
+ }
4060
+ getOrReserveSlotByPk(pk) {
4061
+ const slot = this.slots.get(pk);
4062
+ if (slot) return slot;
4063
+ let reserved = this.reservedSlots.get(pk);
4064
+ if (!reserved) {
4065
+ reserved = { pk: this.keyDomain.canonicalize(pk), item: void 0 };
4066
+ this.reservedSlots.set(pk, reserved);
4067
+ }
4068
+ return reserved;
4069
+ }
4070
+ setManyByPks(pks, entities) {
4071
+ const slots = [];
4072
+ slots.length = pks.length;
4073
+ for (let i = 0; i < pks.length; i++) {
4074
+ slots[i] = this.setOneByPk(pks[i], entities[i]);
4075
+ }
4076
+ return slots;
4077
+ }
4078
+ getSlotByPk(pk) {
4079
+ return this.slots.get(pk);
4080
+ }
4081
+ findSlotByPk(pk) {
4082
+ return this.slots.get(pk) ?? this.reservedSlots.get(pk);
4083
+ }
4084
+ hasSubscribers(slot) {
4085
+ return slot.subscribers !== void 0 && slot.subscribers.size > 0;
4086
+ }
4087
+ getSlotsByPks(pks) {
4088
+ const result = [];
4089
+ result.length = pks.length;
4090
+ let writeIndex = 0;
4091
+ for (let i = 0; i < pks.length; i++) {
4092
+ const slot = this.slots.get(pks[i]);
4093
+ if (slot !== void 0) result[writeIndex++] = slot;
4094
+ }
4095
+ result.length = writeIndex;
4096
+ return result;
4097
+ }
4098
+ getAllSlots() {
4099
+ return Array.from(this.slots.values());
4100
+ }
4101
+ removeOneByPk(pk) {
4102
+ const slot = this.slots.get(pk);
4103
+ if (slot) {
4104
+ slot.item = void 0;
4105
+ this.slots.delete(pk);
4106
+ if (this.hasSubscribers(slot)) {
4107
+ this.reservedSlots.set(pk, slot);
4108
+ }
4109
+ return;
4110
+ }
4111
+ const reserved = this.reservedSlots.get(pk);
4112
+ if (reserved) {
4113
+ reserved.item = void 0;
4114
+ if (!this.hasSubscribers(reserved)) {
4115
+ this.reservedSlots.delete(pk);
4116
+ }
4117
+ }
4118
+ }
4119
+ removeManyByPks(pks) {
4120
+ for (const pk of pks) this.removeOneByPk(pk);
4121
+ }
4122
+ getOneByPk(pk) {
4123
+ return this.slots.get(pk)?.item;
4124
+ }
4125
+ getManyByPks(pks) {
4126
+ const result = [];
4127
+ result.length = pks.length;
4128
+ let writeIndex = 0;
4129
+ for (let i = 0; i < pks.length; i++) {
4130
+ const entity = this.getOneByPk(pks[i]);
4131
+ if (entity !== void 0) result[writeIndex++] = entity;
4132
+ }
4133
+ result.length = writeIndex;
4134
+ return result;
4135
+ }
4136
+ getAll() {
4137
+ const result = [];
4138
+ for (const slot of this.slots.values()) {
4139
+ if (slot.item !== void 0) result.push(slot.item);
4140
+ }
4141
+ return result;
4142
+ }
4143
+ countAll() {
4144
+ return this.slots.size;
4145
+ }
4146
+ clear() {
4147
+ const retained = [];
4148
+ for (const slot of this.slots.values()) {
4149
+ slot.item = void 0;
4150
+ if (this.hasSubscribers(slot)) retained.push(slot);
4151
+ }
4152
+ this.slots.clear();
4153
+ for (const slot of this.reservedSlots.values()) {
4154
+ slot.item = void 0;
4155
+ if (this.hasSubscribers(slot)) retained.push(slot);
4156
+ }
4157
+ this.reservedSlots.clear();
4158
+ for (const slot of retained) {
4159
+ this.reservedSlots.set(slot.pk, slot);
4160
+ }
4161
+ }
4162
+ getAllPks() {
4163
+ return Array.from(this.slots.keys());
4164
+ }
4165
+ destroy() {
4166
+ this.clear();
4167
+ for (const slot of this.reservedSlots.values()) {
4168
+ slot.subscribers?.clear();
4169
+ slot.subscribers = void 0;
4170
+ }
4171
+ this.reservedSlots.clear();
4172
+ }
4173
+ };
4174
+
3322
4175
  // src/core/OIMObjectStoreMapDriven.ts
3323
4176
  var OIMObjectStoreMapDriven = class extends OIMObjectStore {
3324
4177
  constructor() {
@@ -3366,6 +4219,27 @@ var OIMObjectStoreMapDriven = class extends OIMObjectStore {
3366
4219
  }
3367
4220
  };
3368
4221
 
4222
+ // src/core/OIMKeyPathCarrierResolver.ts
4223
+ var OIMKeyPathCarrierResolver = class {
4224
+ constructor() {
4225
+ this.carriers = new OIMTrieMap();
4226
+ }
4227
+ getOrReserveCarrier(key) {
4228
+ let carrier = this.carriers.get(key);
4229
+ if (!carrier) {
4230
+ carrier = { key };
4231
+ this.carriers.set(key, carrier);
4232
+ }
4233
+ return carrier;
4234
+ }
4235
+ findCarrier(key) {
4236
+ return this.carriers.get(key);
4237
+ }
4238
+ onCarrierEmptied(carrier) {
4239
+ this.carriers.delete(carrier.key);
4240
+ }
4241
+ };
4242
+
3369
4243
  // src/core/OIMComparatorFactory.ts
3370
4244
  var OIMComparatorFactory = class {
3371
4245
  createShallowComparator() {
@@ -3584,6 +4458,78 @@ var OIMMap2Keys = class {
3584
4458
  }
3585
4459
  };
3586
4460
 
4461
+ // src/core/OIMDisposeScope.ts
4462
+ var OIMDisposeScope = class _OIMDisposeScope {
4463
+ constructor() {
4464
+ this.disposables = [];
4465
+ this.disposed = false;
4466
+ }
4467
+ /** Number of not-yet-disposed items currently held. */
4468
+ get size() {
4469
+ return this.disposables.length;
4470
+ }
4471
+ get isDisposed() {
4472
+ return this.disposed;
4473
+ }
4474
+ /**
4475
+ * Register a disposable (object with `destroy()` or an unsubscribe function)
4476
+ * and return it unchanged, so it can be captured inline. If the scope is
4477
+ * already disposed the item is disposed immediately, so nothing leaks.
4478
+ */
4479
+ add(disposable) {
4480
+ if (this.disposed) {
4481
+ this.disposeOne(disposable);
4482
+ return disposable;
4483
+ }
4484
+ this.disposables.push(disposable);
4485
+ return disposable;
4486
+ }
4487
+ /** A nested scope, itself registered here — dispose it early or with the parent. */
4488
+ child() {
4489
+ return this.add(new _OIMDisposeScope());
4490
+ }
4491
+ /**
4492
+ * Dispose everything in reverse registration order, then clear. Idempotent.
4493
+ * If any `destroy()`/unsubscribe throws, the rest are still disposed and the
4494
+ * first error is rethrown once teardown completes.
4495
+ */
4496
+ destroy() {
4497
+ if (this.disposed) return;
4498
+ this.disposed = true;
4499
+ let firstError;
4500
+ let hasError = false;
4501
+ for (let i = this.disposables.length - 1; i >= 0; i--) {
4502
+ try {
4503
+ this.disposeOne(this.disposables[i]);
4504
+ } catch (error) {
4505
+ if (!hasError) {
4506
+ hasError = true;
4507
+ firstError = error;
4508
+ }
4509
+ }
4510
+ }
4511
+ this.disposables.length = 0;
4512
+ if (hasError) throw firstError;
4513
+ }
4514
+ disposeOne(disposable) {
4515
+ if (typeof disposable === "function") disposable();
4516
+ else disposable.destroy();
4517
+ }
4518
+ };
4519
+ function createOIMDisposeScope() {
4520
+ return new OIMDisposeScope();
4521
+ }
4522
+
4523
+ // src/core/OIMPkCodecKeyPath.ts
4524
+ var OIMPkCodecKeyPath = class {
4525
+ encode(pk) {
4526
+ return JSON.stringify(pk);
4527
+ }
4528
+ decode(encoded) {
4529
+ return JSON.parse(encoded);
4530
+ }
4531
+ };
4532
+
3587
4533
  // src/modules/compute/core/OIMComputeRuntime.ts
3588
4534
  var OIMComputeRuntime = class {
3589
4535
  constructor(queue) {
@@ -4187,6 +5133,27 @@ var OIMCollectionSelectors = class {
4187
5133
  key
4188
5134
  );
4189
5135
  }
5136
+ /**
5137
+ * Entities of a composite (key-path) set-based index for one key path, e.g.
5138
+ * `entitiesByCompositeSetIndexKey(index, [projectId, role])`.
5139
+ */
5140
+ entitiesByCompositeSetIndexKey(index, key) {
5141
+ return new OIMEntitiesByIndexKeySetBasedSelector(
5142
+ this.runtime,
5143
+ this.collection,
5144
+ index,
5145
+ key
5146
+ );
5147
+ }
5148
+ /** Entities of a composite (key-path) array-based index for one key path. */
5149
+ entitiesByCompositeArrayIndexKey(index, key) {
5150
+ return new OIMEntitiesByIndexKeyArrayBasedSelector(
5151
+ this.runtime,
5152
+ this.collection,
5153
+ index,
5154
+ key
5155
+ );
5156
+ }
4190
5157
  /** Entities of a keyless (whole-collection) array-based index, ordered. */
4191
5158
  entitiesByArrayGlobalIndex(index) {
4192
5159
  return new OIMEntitiesByGlobalIndexArrayBasedSelector(
@@ -4210,7 +5177,16 @@ function createOIMCollectionKit(queue, opts) {
4210
5177
  const collection = new OIMReactiveCollection(queue, opts);
4211
5178
  const indexFactory = new OIMCollectionIndexFactory(queue, collection);
4212
5179
  const select = new OIMCollectionSelectors(queue, collection);
4213
- return { queue, collection, indexFactory, select };
5180
+ const scope = new OIMDisposeScope();
5181
+ scope.add(collection);
5182
+ return {
5183
+ queue,
5184
+ collection,
5185
+ indexFactory,
5186
+ select,
5187
+ scope,
5188
+ destroy: () => scope.destroy()
5189
+ };
4214
5190
  }
4215
5191
 
4216
5192
  // src/modules/wrapper/collection/OIMCollectionChangedFields.ts
@@ -4597,6 +5573,56 @@ function createOIMIndexSlotMap(index, create) {
4597
5573
  return new OIMIndexSlotMap(index, create);
4598
5574
  }
4599
5575
 
5576
+ // src/modules/wrapper/index/OIMGlobalIndexSlotMap.ts
5577
+ var OIMGlobalIndexSlotMap = class _OIMGlobalIndexSlotMap {
5578
+ constructor(index, create) {
5579
+ this.cache = /* @__PURE__ */ new WeakMap();
5580
+ this.index = index;
5581
+ this.create = create;
5582
+ }
5583
+ /** The mapped object for a slot, created once and cached by slot identity. */
5584
+ get(slot) {
5585
+ let mapped = this.cache.get(slot);
5586
+ if (mapped === void 0 && !this.cache.has(slot)) {
5587
+ mapped = this.create(slot);
5588
+ this.cache.set(slot, mapped);
5589
+ }
5590
+ return mapped;
5591
+ }
5592
+ /**
5593
+ * Mapped objects for every slot in the index, with stable references. For a
5594
+ * set-based global index the order is unspecified (as the set is); for an
5595
+ * array-based one it follows the index order.
5596
+ */
5597
+ getAll() {
5598
+ const result = [];
5599
+ for (const slot of this.index.getSlots()) {
5600
+ result.push(this.get(slot));
5601
+ }
5602
+ return result;
5603
+ }
5604
+ /**
5605
+ * Chain another projection. `map(f)` returns a slot map over the SAME index
5606
+ * whose objects are `f(this.get(slot))`, cached by slot identity — both
5607
+ * levels hand back stable references and each `create` runs once per slot.
5608
+ */
5609
+ map(create) {
5610
+ return new _OIMGlobalIndexSlotMap(
5611
+ this.index,
5612
+ (slot) => create(this.get(slot))
5613
+ );
5614
+ }
5615
+ /** Subscribe to the index's changes — passthrough to the index. */
5616
+ subscribe(handler) {
5617
+ return this.index.subscribe(handler);
5618
+ }
5619
+ };
5620
+
5621
+ // src/modules/wrapper/index/createOIMGlobalIndexSlotMap.ts
5622
+ function createOIMGlobalIndexSlotMap(index, create) {
5623
+ return new OIMGlobalIndexSlotMap(index, create);
5624
+ }
5625
+
4600
5626
  // src/modules/wrapper/index/diffOrderedListByPk.ts
4601
5627
  function diffOrderedListByPk(prev, next) {
4602
5628
  const commands = [];
@@ -5348,6 +6374,7 @@ export {
5348
6374
  EOIMEventQueueSchedulerEventType,
5349
6375
  EOIMIndexEventType,
5350
6376
  EOIMObjectEventType,
6377
+ OIMBucketCarrierResolver,
5351
6378
  OIMCarrierKeyedEmitter,
5352
6379
  OIMCarrierSingleEmitter,
5353
6380
  OIMCollection,
@@ -5360,6 +6387,7 @@ export {
5360
6387
  OIMCollectionSelectors,
5361
6388
  OIMCollectionStore,
5362
6389
  OIMCollectionStoreMapDriven,
6390
+ OIMCollectionStoreTrieDriven,
5363
6391
  OIMComparatorFactory,
5364
6392
  OIMComputeRuntime,
5365
6393
  OIMComputed,
@@ -5368,6 +6396,7 @@ export {
5368
6396
  OIMDerivedCollectionGlobalIndexSetBased,
5369
6397
  OIMDerivedCollectionIndexArrayBased,
5370
6398
  OIMDerivedCollectionIndexSetBased,
6399
+ OIMDisposeScope,
5371
6400
  OIMEffect,
5372
6401
  OIMEffectDependencyComputed,
5373
6402
  OIMEffectDependencyKeyed,
@@ -5393,6 +6422,7 @@ export {
5393
6422
  OIMGlobalIndexManualArrayBased,
5394
6423
  OIMGlobalIndexManualSetBased,
5395
6424
  OIMGlobalIndexSetBased,
6425
+ OIMGlobalIndexSlotMap,
5396
6426
  OIMIndex,
5397
6427
  OIMIndexArrayBased,
5398
6428
  OIMIndexComparatorFactory,
@@ -5409,6 +6439,13 @@ export {
5409
6439
  OIMIndexStoreMapDrivenArrayBased,
5410
6440
  OIMIndexStoreMapDrivenSetBased,
5411
6441
  OIMIndexStoreSetBased,
6442
+ OIMIndexStoreTrieDrivenArrayBased,
6443
+ OIMIndexStoreTrieDrivenSetBased,
6444
+ OIMKeyDomainNative,
6445
+ OIMKeyDomainTrie,
6446
+ OIMKeyPathCarrierResolver,
6447
+ OIMKeyedBucketArrayBased,
6448
+ OIMKeyedBucketSetBased,
5412
6449
  OIMMap2Keys,
5413
6450
  OIMObject,
5414
6451
  OIMObjectStore,
@@ -5420,10 +6457,13 @@ export {
5420
6457
  OIMOrderedListCommandStream,
5421
6458
  OIMOrderedListCommandStreamDiffDriven,
5422
6459
  OIMOrderedListMappedCommandStream,
6460
+ OIMPkCodecKeyPath,
5423
6461
  OIMPkSelectorFactory,
5424
6462
  OIMReactiveCollection,
5425
6463
  OIMReactiveCollectionGlobalIndexManualArrayBased,
5426
6464
  OIMReactiveCollectionGlobalIndexManualSetBased,
6465
+ OIMReactiveCollectionIndexCompositeTrieArrayBased,
6466
+ OIMReactiveCollectionIndexCompositeTrieSetBased,
5427
6467
  OIMReactiveCollectionIndexManualArrayBased,
5428
6468
  OIMReactiveCollectionIndexManualSetBased,
5429
6469
  OIMReactiveGlobalIndex,
@@ -5438,10 +6478,14 @@ export {
5438
6478
  OIMReactiveIndexSetBased,
5439
6479
  OIMReactiveObject,
5440
6480
  OIMSelector,
6481
+ OIMTrieMap,
6482
+ OIMTrieSet,
5441
6483
  createInPlaceEntityUpdater,
5442
6484
  createMergeEntityUpdater,
5443
6485
  createOIMCollectionIndexFactory,
5444
6486
  createOIMCollectionKit,
6487
+ createOIMDisposeScope,
6488
+ createOIMGlobalIndexSlotMap,
5445
6489
  createOIMIndexSlotMap,
5446
6490
  createOIMOrderedListCommandStreamDiffDriven,
5447
6491
  createOIMOrderedListMappedCommandStream,