@oimdb/core 2.1.0 → 3.0.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
+ OIMCarrierKeyedEmitter: () => OIMCarrierKeyedEmitter,
29
30
  OIMCollection: () => OIMCollection,
30
31
  OIMCollectionByPkSelector: () => OIMCollectionByPkSelector,
31
32
  OIMCollectionByPksSelector: () => OIMCollectionByPksSelector,
@@ -93,6 +94,7 @@ __export(index_exports, {
93
94
  OIMReactiveIndexSetBased: () => OIMReactiveIndexSetBased,
94
95
  OIMReactiveObject: () => OIMReactiveObject,
95
96
  OIMSelector: () => OIMSelector,
97
+ createInPlaceEntityUpdater: () => createInPlaceEntityUpdater,
96
98
  createOIMCollectionIndexFactory: () => createOIMCollectionIndexFactory,
97
99
  createOIMCollectionKit: () => createOIMCollectionKit
98
100
  });
@@ -277,10 +279,18 @@ var OIMIndex = class {
277
279
  emitUpdateOne(key) {
278
280
  this.emitUpdate([key]);
279
281
  }
282
+ /**
283
+ * Maps a bucket's slots to entities, preserving positional holes: a slot
284
+ * whose entity is not present yet (reserved by an index ahead of its data)
285
+ * or was removed yields `undefined` at its position rather than being
286
+ * dropped. This keeps the result aligned with `getPksByKey`, so callers can
287
+ * render a per-item loading/placeholder state instead of a silently shorter
288
+ * list.
289
+ */
280
290
  slotsToEntities(slots) {
281
291
  const entities = [];
282
292
  for (const slot of slots) {
283
- if (slot.item !== void 0) entities.push(slot.item);
293
+ entities.push(slot.item);
284
294
  }
285
295
  return entities;
286
296
  }
@@ -536,12 +546,7 @@ var OIMUpdateEventEmitter = class {
536
546
  this.updatedKeys.clear();
537
547
  return;
538
548
  }
539
- const makeHandlerSnapshot = (handlers) => {
540
- const snapshot = [];
541
- handlers.forEach((h) => snapshot.push(h));
542
- return snapshot;
543
- };
544
- this.runSinglePass(makeHandlerSnapshot);
549
+ this.runSinglePass();
545
550
  };
546
551
  this.onAfterFlush = () => {
547
552
  if (!this.isAfterFlushDeliveryScheduled) return;
@@ -636,18 +641,32 @@ var OIMUpdateEventEmitter = class {
636
641
  this.dequeueFlush = this.queue.enqueue(this.onFlush);
637
642
  }
638
643
  deliverImmediate() {
639
- const makeHandlerSnapshot = (handlers) => {
640
- const snapshot = [];
641
- handlers.forEach((h) => snapshot.push(h));
642
- return snapshot;
643
- };
644
- this.runSinglePass(makeHandlerSnapshot);
644
+ this.runSinglePass();
645
+ }
646
+ /**
647
+ * Notifies all handlers for a key. The overwhelmingly common case is a
648
+ * single subscriber per key (one component per pk), handled without any
649
+ * allocation. For multiple subscribers a snapshot is taken so a handler may
650
+ * (un)subscribe during iteration without corrupting the walk.
651
+ */
652
+ notifyHandlers(handlers) {
653
+ if (handlers.size === 1) {
654
+ const only = handlers.values().next().value;
655
+ if (only) only();
656
+ return;
657
+ }
658
+ const snapshot = [];
659
+ handlers.forEach((h) => snapshot.push(h));
660
+ for (let i = 0; i < snapshot.length; i++) {
661
+ const handler = snapshot[i];
662
+ if (handlers.has(handler)) handler();
663
+ }
645
664
  }
646
665
  /**
647
666
  * Deliver updated keys and notify handlers once.
648
667
  * Re-entrant updates during delivery are forbidden and will throw in markUpdatedKeys().
649
668
  */
650
- runSinglePass(makeHandlerSnapshot) {
669
+ runSinglePass() {
651
670
  if (this.updatedKeys.size === 0) return;
652
671
  const flushingKeys = this.updatedKeys;
653
672
  this.updatedKeys = /* @__PURE__ */ new Set();
@@ -656,21 +675,13 @@ var OIMUpdateEventEmitter = class {
656
675
  flushingKeys.forEach((key) => {
657
676
  const handlers = this.keyHandlers.get(key);
658
677
  if (!handlers || handlers.size === 0) return;
659
- const handlerSnapshot = makeHandlerSnapshot(handlers);
660
- for (let i = 0; i < handlerSnapshot.length; i++) {
661
- const handler = handlerSnapshot[i];
662
- if (handlers.has(handler)) handler();
663
- }
678
+ this.notifyHandlers(handlers);
664
679
  });
665
680
  } else {
666
681
  this.keyHandlers.forEach((handlers, key) => {
667
682
  if (!flushingKeys.has(key)) return;
668
683
  if (!handlers || handlers.size === 0) return;
669
- const handlerSnapshot = makeHandlerSnapshot(handlers);
670
- for (let i = 0; i < handlerSnapshot.length; i++) {
671
- const handler = handlerSnapshot[i];
672
- if (handlers.has(handler)) handler();
673
- }
684
+ this.notifyHandlers(handlers);
674
685
  });
675
686
  }
676
687
  }
@@ -879,17 +890,39 @@ var OIMCollectionStoreMapDriven = class extends OIMCollectionStore {
879
890
  constructor() {
880
891
  super(...arguments);
881
892
  this.slots = /* @__PURE__ */ new Map();
893
+ // Slots reserved by indexes for PKs whose entity has not arrived yet.
894
+ // They carry `item: undefined` and live apart from `slots` so they do not
895
+ // leak into entity enumeration (getAll/getAllPks/countAll). When the entity
896
+ // is finally written, the reserved slot is promoted into `slots` — keeping
897
+ // the reference that indexes already hold, so it fills in live.
898
+ this.reservedSlots = /* @__PURE__ */ new Map();
882
899
  }
883
900
  setOneByPk(pk, entity) {
884
901
  const slot = this.slots.get(pk);
885
902
  if (slot) {
886
903
  slot.item = entity;
887
904
  return slot;
888
- } else {
889
- const nextSlot = { pk, item: entity };
890
- this.slots.set(pk, nextSlot);
891
- return nextSlot;
892
905
  }
906
+ const reserved = this.reservedSlots.get(pk);
907
+ if (reserved) {
908
+ reserved.item = entity;
909
+ this.reservedSlots.delete(pk);
910
+ this.slots.set(pk, reserved);
911
+ return reserved;
912
+ }
913
+ const nextSlot = { pk, item: entity };
914
+ this.slots.set(pk, nextSlot);
915
+ return nextSlot;
916
+ }
917
+ getOrReserveSlotByPk(pk) {
918
+ const slot = this.slots.get(pk);
919
+ if (slot) return slot;
920
+ let reserved = this.reservedSlots.get(pk);
921
+ if (!reserved) {
922
+ reserved = { pk, item: void 0 };
923
+ this.reservedSlots.set(pk, reserved);
924
+ }
925
+ return reserved;
893
926
  }
894
927
  setManyByPks(pks, entities) {
895
928
  const slots = [];
@@ -902,6 +935,12 @@ var OIMCollectionStoreMapDriven = class extends OIMCollectionStore {
902
935
  getSlotByPk(pk) {
903
936
  return this.slots.get(pk);
904
937
  }
938
+ findSlotByPk(pk) {
939
+ return this.slots.get(pk) ?? this.reservedSlots.get(pk);
940
+ }
941
+ hasSubscribers(slot) {
942
+ return slot.subscribers !== void 0 && slot.subscribers.size > 0;
943
+ }
905
944
  getSlotsByPks(pks) {
906
945
  const result = [];
907
946
  result.length = pks.length;
@@ -920,8 +959,21 @@ var OIMCollectionStoreMapDriven = class extends OIMCollectionStore {
920
959
  }
921
960
  removeOneByPk(pk) {
922
961
  const slot = this.slots.get(pk);
923
- if (slot) slot.item = void 0;
924
- this.slots.delete(pk);
962
+ if (slot) {
963
+ slot.item = void 0;
964
+ this.slots.delete(pk);
965
+ if (this.hasSubscribers(slot)) {
966
+ this.reservedSlots.set(pk, slot);
967
+ }
968
+ return;
969
+ }
970
+ const reserved = this.reservedSlots.get(pk);
971
+ if (reserved) {
972
+ reserved.item = void 0;
973
+ if (!this.hasSubscribers(reserved)) {
974
+ this.reservedSlots.delete(pk);
975
+ }
976
+ }
925
977
  }
926
978
  removeManyByPks(pks) {
927
979
  for (const pk of pks) {
@@ -955,16 +1007,31 @@ var OIMCollectionStoreMapDriven = class extends OIMCollectionStore {
955
1007
  return this.slots.size;
956
1008
  }
957
1009
  clear() {
1010
+ const retained = [];
958
1011
  for (const slot of this.slots.values()) {
959
1012
  slot.item = void 0;
1013
+ if (this.hasSubscribers(slot)) retained.push(slot);
960
1014
  }
961
1015
  this.slots.clear();
1016
+ for (const slot of this.reservedSlots.values()) {
1017
+ slot.item = void 0;
1018
+ if (this.hasSubscribers(slot)) retained.push(slot);
1019
+ }
1020
+ this.reservedSlots.clear();
1021
+ for (const slot of retained) {
1022
+ this.reservedSlots.set(slot.pk, slot);
1023
+ }
962
1024
  }
963
1025
  getAllPks() {
964
1026
  return Array.from(this.slots.keys());
965
1027
  }
966
1028
  destroy() {
967
1029
  this.clear();
1030
+ for (const slot of this.reservedSlots.values()) {
1031
+ slot.subscribers?.clear();
1032
+ slot.subscribers = void 0;
1033
+ }
1034
+ this.reservedSlots.clear();
968
1035
  }
969
1036
  };
970
1037
 
@@ -1000,6 +1067,9 @@ var OIMCollection = class {
1000
1067
  getSlotByPk(pk) {
1001
1068
  return this.store.getSlotByPk(pk);
1002
1069
  }
1070
+ getOrReserveSlotByPk(pk) {
1071
+ return this.store.getOrReserveSlotByPk(pk);
1072
+ }
1003
1073
  getSlotsByPks(pks) {
1004
1074
  return this.store.getSlotsByPks(pks);
1005
1075
  }
@@ -1084,6 +1154,153 @@ var OIMCollection = class {
1084
1154
  }
1085
1155
  };
1086
1156
 
1157
+ // src/core/OIMCarrierKeyedEmitter.ts
1158
+ var OIMCarrierKeyedEmitter = class {
1159
+ constructor(queue, resolver) {
1160
+ // Carriers with at least one subscriber — for markAllUpdated / metrics.
1161
+ this.subscribedCarriers = /* @__PURE__ */ new Set();
1162
+ this.dirtyCarriers = /* @__PURE__ */ new Set();
1163
+ this.isFlushEnqueued = false;
1164
+ this.onFlush = () => {
1165
+ this.isFlushEnqueued = false;
1166
+ this.dequeueFlush = void 0;
1167
+ if (this.dirtyCarriers.size === 0) return;
1168
+ const flushing = this.dirtyCarriers;
1169
+ this.dirtyCarriers = /* @__PURE__ */ new Set();
1170
+ flushing.forEach((carrier) => this.notify(carrier));
1171
+ };
1172
+ this.queue = queue;
1173
+ this.resolver = resolver;
1174
+ }
1175
+ subscribeOnKey(key, handler) {
1176
+ const carrier = this.resolver.getOrReserveCarrier(key);
1177
+ this.addHandler(carrier, handler);
1178
+ return () => this.removeHandler(carrier, handler);
1179
+ }
1180
+ subscribeOnKeys(keys, handler) {
1181
+ const carriers = [];
1182
+ for (let i = 0; i < keys.length; i++) {
1183
+ const carrier = this.resolver.getOrReserveCarrier(keys[i]);
1184
+ this.addHandler(carrier, handler);
1185
+ carriers.push(carrier);
1186
+ }
1187
+ return () => {
1188
+ for (let i = 0; i < carriers.length; i++) {
1189
+ this.removeHandler(carriers[i], handler);
1190
+ }
1191
+ };
1192
+ }
1193
+ unsubscribeFromKey(key, handler) {
1194
+ const carrier = this.resolver.findCarrier(key);
1195
+ if (carrier) this.removeHandler(carrier, handler);
1196
+ }
1197
+ unsubscribeFromKeys(keys, handler) {
1198
+ for (let i = 0; i < keys.length; i++) {
1199
+ this.unsubscribeFromKey(keys[i], handler);
1200
+ }
1201
+ }
1202
+ addHandler(carrier, handler) {
1203
+ let subscribers = carrier.subscribers;
1204
+ if (!subscribers) {
1205
+ subscribers = /* @__PURE__ */ new Set();
1206
+ carrier.subscribers = subscribers;
1207
+ }
1208
+ subscribers.add(handler);
1209
+ this.subscribedCarriers.add(carrier);
1210
+ }
1211
+ removeHandler(carrier, handler) {
1212
+ const subscribers = carrier.subscribers;
1213
+ if (!subscribers) return;
1214
+ subscribers.delete(handler);
1215
+ if (subscribers.size === 0) {
1216
+ this.subscribedCarriers.delete(carrier);
1217
+ }
1218
+ }
1219
+ /** Fast path: the writer already holds the carrier. */
1220
+ markUpdatedCarrier(carrier) {
1221
+ this.assertNotInFlush();
1222
+ const subscribers = carrier.subscribers;
1223
+ if (!subscribers || subscribers.size === 0) return;
1224
+ this.dirtyCarriers.add(carrier);
1225
+ this.scheduleFlush();
1226
+ }
1227
+ markUpdatedKey(key) {
1228
+ const carrier = this.resolver.findCarrier(key);
1229
+ if (carrier) this.markUpdatedCarrier(carrier);
1230
+ else this.assertNotInFlush();
1231
+ }
1232
+ markUpdatedKeys(keys) {
1233
+ for (let i = 0; i < keys.length; i++) this.markUpdatedKey(keys[i]);
1234
+ }
1235
+ markAllUpdated() {
1236
+ this.assertNotInFlush();
1237
+ if (this.subscribedCarriers.size === 0) return;
1238
+ this.subscribedCarriers.forEach(
1239
+ (carrier) => this.dirtyCarriers.add(carrier)
1240
+ );
1241
+ this.scheduleFlush();
1242
+ }
1243
+ hasSubscriptions() {
1244
+ return this.subscribedCarriers.size > 0;
1245
+ }
1246
+ getHandlerCount(key) {
1247
+ return this.resolver.findCarrier(key)?.subscribers?.size ?? 0;
1248
+ }
1249
+ getMetrics() {
1250
+ let totalHandlers = 0;
1251
+ this.subscribedCarriers.forEach((carrier) => {
1252
+ totalHandlers += carrier.subscribers?.size ?? 0;
1253
+ });
1254
+ const totalKeys = this.subscribedCarriers.size;
1255
+ return {
1256
+ totalKeys,
1257
+ totalHandlers,
1258
+ averageHandlersPerKey: totalKeys > 0 ? totalHandlers / totalKeys : 0,
1259
+ queueLength: this.dirtyCarriers.size
1260
+ };
1261
+ }
1262
+ destroy() {
1263
+ if (this.dequeueFlush) {
1264
+ this.dequeueFlush();
1265
+ this.dequeueFlush = void 0;
1266
+ }
1267
+ this.subscribedCarriers.forEach((carrier) => {
1268
+ carrier.subscribers?.clear();
1269
+ carrier.subscribers = void 0;
1270
+ });
1271
+ this.subscribedCarriers.clear();
1272
+ this.dirtyCarriers.clear();
1273
+ this.isFlushEnqueued = false;
1274
+ }
1275
+ assertNotInFlush() {
1276
+ if (this.queue.isInFlush) {
1277
+ throw new Error(
1278
+ "OIMCarrierKeyedEmitter: updates during queue.flush() are not allowed. Finish all writes before calling queue.flush()."
1279
+ );
1280
+ }
1281
+ }
1282
+ scheduleFlush() {
1283
+ if (this.isFlushEnqueued) return;
1284
+ this.isFlushEnqueued = true;
1285
+ this.dequeueFlush = this.queue.enqueue(this.onFlush);
1286
+ }
1287
+ notify(carrier) {
1288
+ const subscribers = carrier.subscribers;
1289
+ if (!subscribers || subscribers.size === 0) return;
1290
+ if (subscribers.size === 1) {
1291
+ const only = subscribers.values().next().value;
1292
+ if (only) only();
1293
+ return;
1294
+ }
1295
+ const snapshot = [];
1296
+ subscribers.forEach((h) => snapshot.push(h));
1297
+ for (let i = 0; i < snapshot.length; i++) {
1298
+ const handler = snapshot[i];
1299
+ if (subscribers.has(handler)) handler();
1300
+ }
1301
+ }
1302
+ };
1303
+
1087
1304
  // src/core/OIMReactiveCollection.ts
1088
1305
  var OIMReactiveCollection = class extends OIMCollection {
1089
1306
  constructor(queue, opts) {
@@ -1103,7 +1320,10 @@ var OIMReactiveCollection = class extends OIMCollection {
1103
1320
  for (let i = 0; i < snapshot.length; i++) snapshot[i](pks);
1104
1321
  };
1105
1322
  this.queue = queue;
1106
- this.updateEmitter = new OIMUpdateEventEmitter(queue, "queue");
1323
+ this.updateEmitter = new OIMCarrierKeyedEmitter(queue, {
1324
+ getOrReserveCarrier: (pk) => this.store.getOrReserveSlotByPk(pk),
1325
+ findCarrier: (pk) => this.store.findSlotByPk(pk)
1326
+ });
1107
1327
  }
1108
1328
  subscribeOnKey(pk, handler) {
1109
1329
  return this.updateEmitter.subscribeOnKey(pk, handler);
@@ -1135,17 +1355,31 @@ var OIMReactiveCollection = class extends OIMCollection {
1135
1355
  this.anyUpdateHandlers.delete(handler);
1136
1356
  };
1137
1357
  }
1358
+ // The legacy `emitter` (EOIMCollectionEventType.UPDATE) coexists with the
1359
+ // keyed `updateEmitter`. Build its payload only when something is actually
1360
+ // subscribed — otherwise every upsert/remove allocates a throwaway
1361
+ // `{ pks }` object (and array) that nobody reads.
1362
+ notifyLegacy(pk) {
1363
+ if (this.emitter.hasHandlers(0 /* UPDATE */)) {
1364
+ this.emitter.emit(0 /* UPDATE */, { pks: [pk] });
1365
+ }
1366
+ }
1367
+ notifyLegacyMany(pks) {
1368
+ if (this.emitter.hasHandlers(0 /* UPDATE */)) {
1369
+ this.emitter.emit(0 /* UPDATE */, { pks });
1370
+ }
1371
+ }
1138
1372
  upsertOneByPk(pk, entity) {
1139
1373
  const slot = this.upsertOneWithoutNotificationsByPk(pk, entity);
1140
- this.emitter.emit(0 /* UPDATE */, { pks: [pk] });
1141
- this.updateEmitter.markUpdatedKey(pk);
1374
+ this.notifyLegacy(pk);
1375
+ this.updateEmitter.markUpdatedCarrier(slot);
1142
1376
  this.trackAnyUpdatePk(pk);
1143
1377
  return slot;
1144
1378
  }
1145
1379
  upsertOne(entity) {
1146
1380
  const slot = this.upsertOneWithoutNotifications(entity);
1147
- this.emitter.emit(0 /* UPDATE */, { pks: [slot.pk] });
1148
- this.updateEmitter.markUpdatedKey(slot.pk);
1381
+ this.notifyLegacy(slot.pk);
1382
+ this.updateEmitter.markUpdatedCarrier(slot);
1149
1383
  this.trackAnyUpdatePk(slot.pk);
1150
1384
  return slot;
1151
1385
  }
@@ -1155,15 +1389,17 @@ var OIMReactiveCollection = class extends OIMCollection {
1155
1389
  (entity) => this.upsertOneWithoutNotifications(entity)
1156
1390
  );
1157
1391
  const pks = slots.map((slot) => slot.pk);
1158
- this.emitter.emit(0 /* UPDATE */, { pks });
1159
- this.updateEmitter.markUpdatedKeys(pks);
1392
+ this.notifyLegacyMany(pks);
1393
+ for (let i = 0; i < slots.length; i++) {
1394
+ this.updateEmitter.markUpdatedCarrier(slots[i]);
1395
+ }
1160
1396
  this.trackAnyUpdatePks(pks);
1161
1397
  return slots;
1162
1398
  }
1163
1399
  removeOne(entity) {
1164
1400
  const pk = this.selectPk(entity);
1165
1401
  this.store.removeOneByPk(pk);
1166
- this.emitter.emit(0 /* UPDATE */, { pks: [pk] });
1402
+ this.notifyLegacy(pk);
1167
1403
  this.updateEmitter.markUpdatedKey(pk);
1168
1404
  this.trackAnyUpdatePk(pk);
1169
1405
  }
@@ -1171,26 +1407,28 @@ var OIMReactiveCollection = class extends OIMCollection {
1171
1407
  if (entities.length === 0) return;
1172
1408
  const pks = entities.map(this.selectPk);
1173
1409
  this.store.removeManyByPks(pks);
1174
- this.emitter.emit(0 /* UPDATE */, { pks });
1410
+ this.notifyLegacyMany(pks);
1175
1411
  this.updateEmitter.markUpdatedKeys(pks);
1176
1412
  this.trackAnyUpdatePks(pks);
1177
1413
  }
1178
1414
  removeOneByPk(pk) {
1179
1415
  this.store.removeOneByPk(pk);
1180
- this.emitter.emit(0 /* UPDATE */, { pks: [pk] });
1416
+ this.notifyLegacy(pk);
1181
1417
  this.updateEmitter.markUpdatedKey(pk);
1182
1418
  this.trackAnyUpdatePk(pk);
1183
1419
  }
1184
1420
  removeManyByPks(pks) {
1185
1421
  if (pks.length === 0) return;
1186
1422
  this.store.removeManyByPks(pks);
1187
- this.emitter.emit(0 /* UPDATE */, { pks });
1423
+ this.notifyLegacyMany(pks);
1188
1424
  this.updateEmitter.markUpdatedKeys(pks);
1189
1425
  this.trackAnyUpdatePks(pks);
1190
1426
  }
1191
1427
  clear() {
1192
1428
  this.store.clear();
1193
- this.emitter.emit(0 /* UPDATE */, { pks: [] });
1429
+ if (this.emitter.hasHandlers(0 /* UPDATE */)) {
1430
+ this.emitter.emit(0 /* UPDATE */, { pks: [] });
1431
+ }
1194
1432
  this.updateEmitter.markAllUpdated();
1195
1433
  this.trackAnyUpdateClear();
1196
1434
  }
@@ -1573,6 +1811,21 @@ var OIMIndexManualArrayBased = class extends OIMIndexArrayBased {
1573
1811
  this.emitUpdateOne(key);
1574
1812
  }
1575
1813
  }
1814
+ /**
1815
+ * Appends pre-deduplicated slots to a key's bucket IN PLACE (no copy of the
1816
+ * existing bucket) and emits once. Callers own dedup (so this stays O(added),
1817
+ * not O(bucket)); used by collection-bound indexes for fast `addPks`.
1818
+ */
1819
+ appendSlots(key, slots) {
1820
+ if (slots.length === 0) return;
1821
+ let bucket = this.store.getOneByKey(key);
1822
+ if (!bucket) {
1823
+ bucket = [];
1824
+ this.store.setOneByKey(key, bucket);
1825
+ }
1826
+ for (let i = 0; i < slots.length; i++) bucket.push(slots[i]);
1827
+ this.emitUpdateOne(key);
1828
+ }
1576
1829
  /**
1577
1830
  * Clear all primary keys for a specific index key, or all keys if no key specified
1578
1831
  */
@@ -1617,6 +1870,9 @@ var OIMReactiveIndexManualArrayBased = class extends OIMReactiveIndexArrayBased
1617
1870
  setSlots(key, slots) {
1618
1871
  this.index.setSlots(key, slots);
1619
1872
  }
1873
+ appendSlots(key, slots) {
1874
+ this.index.appendSlots(key, slots);
1875
+ }
1620
1876
  clear(key) {
1621
1877
  this.index.clear(key);
1622
1878
  }
@@ -1625,28 +1881,31 @@ var OIMReactiveIndexManualArrayBased = class extends OIMReactiveIndexArrayBased
1625
1881
  // src/core/OIMReactiveCollectionIndexManualArrayBased.ts
1626
1882
  var OIMReactiveCollectionIndexManualArrayBased = class extends OIMReactiveIndexManualArrayBased {
1627
1883
  constructor(queue, opts) {
1628
- const resolveSlot = opts.collection !== void 0 ? (pk) => opts.collection.getSlotByPk(pk) : opts.resolveSlot;
1884
+ const resolveSlot = opts.collection !== void 0 ? (pk) => opts.collection.getOrReserveSlotByPk(pk) : opts.resolveSlot;
1629
1885
  super(queue, {
1630
1886
  indexOptions: opts.indexOptions
1631
1887
  });
1888
+ // Persistent per-key pk membership for O(1) `addPks` dedup (avoids rebuilding
1889
+ // a Set from the whole bucket on every call). Kept in sync by setSlots/clear.
1890
+ this.pksByKey = /* @__PURE__ */ new Map();
1632
1891
  this.resolveSlot = resolveSlot;
1633
1892
  }
1634
1893
  setPks(key, pks) {
1635
- this.setSlots(key, this.resolveSlots(pks));
1894
+ const pkSet = /* @__PURE__ */ new Set();
1895
+ for (let i = 0; i < pks.length; i++) pkSet.add(pks[i]);
1896
+ this.pksByKey.set(key, pkSet);
1897
+ super.setSlots(key, this.resolveSlots(pks));
1636
1898
  }
1637
1899
  addPks(key, pks) {
1638
1900
  if (pks.length === 0) return;
1639
- const existingSlots = this.index.getSlotsByKey(key);
1640
- const existingPks = new Set(existingSlots.map((slot) => slot.pk));
1641
- const nextSlots = existingSlots.slice();
1642
- let hasChanges = false;
1901
+ const pkSet = this.membershipOf(key);
1902
+ const newSlots = [];
1643
1903
  for (const pk of pks) {
1644
- if (existingPks.has(pk)) continue;
1645
- nextSlots.push(this.resolveRequiredSlot(pk));
1646
- existingPks.add(pk);
1647
- hasChanges = true;
1904
+ if (pkSet.has(pk)) continue;
1905
+ pkSet.add(pk);
1906
+ newSlots.push(this.resolveRequiredSlot(pk));
1648
1907
  }
1649
- if (hasChanges) this.setSlots(key, nextSlots);
1908
+ if (newSlots.length > 0) this.appendSlots(key, newSlots);
1650
1909
  }
1651
1910
  removePks(key, pks) {
1652
1911
  if (pks.length === 0) return;
@@ -1656,8 +1915,33 @@ var OIMReactiveCollectionIndexManualArrayBased = class extends OIMReactiveIndexM
1656
1915
  (slot) => !pksToRemove.has(slot.pk)
1657
1916
  );
1658
1917
  if (nextSlots.length === existingSlots.length) return;
1918
+ const pkSet = this.pksByKey.get(key);
1919
+ if (pkSet) {
1920
+ for (let i = 0; i < pks.length; i++) pkSet.delete(pks[i]);
1921
+ }
1659
1922
  if (nextSlots.length === 0) this.clear(key);
1660
- else this.setSlots(key, nextSlots);
1923
+ else super.setSlots(key, nextSlots);
1924
+ }
1925
+ clear(key) {
1926
+ if (key === void 0) this.pksByKey.clear();
1927
+ else this.pksByKey.delete(key);
1928
+ super.clear(key);
1929
+ }
1930
+ /**
1931
+ * The membership set for a key, lazily seeded from the current bucket so it
1932
+ * stays correct even if slots were set through a lower-level path.
1933
+ */
1934
+ membershipOf(key) {
1935
+ let pkSet = this.pksByKey.get(key);
1936
+ if (!pkSet) {
1937
+ pkSet = /* @__PURE__ */ new Set();
1938
+ const existing = this.index.getSlotsByKey(key);
1939
+ for (let i = 0; i < existing.length; i++) {
1940
+ pkSet.add(existing[i].pk);
1941
+ }
1942
+ this.pksByKey.set(key, pkSet);
1943
+ }
1944
+ return pkSet;
1661
1945
  }
1662
1946
  getEntitiesByKey(key) {
1663
1947
  return super.getEntitiesByKey(key);
@@ -1675,11 +1959,7 @@ var OIMReactiveCollectionIndexManualArrayBased = class extends OIMReactiveIndexM
1675
1959
  }
1676
1960
  resolveRequiredSlot(pk) {
1677
1961
  const slot = this.resolveSlot(pk);
1678
- if (!slot) {
1679
- throw new Error(
1680
- `[OIMReactiveCollectionIndexManualArrayBased]: Unable to resolve slot for PK "${String(pk)}".`
1681
- );
1682
- }
1962
+ if (!slot) return { pk, item: void 0 };
1683
1963
  return slot;
1684
1964
  }
1685
1965
  };
@@ -1695,6 +1975,41 @@ var OIMIndexManualSetBased = class extends OIMIndexSetBased {
1695
1975
  this.emitUpdateOne(key);
1696
1976
  }
1697
1977
  }
1978
+ /**
1979
+ * Adds slots to a key's bucket IN PLACE (no copy of the existing Set) and
1980
+ * emits once. O(added). Used by collection-bound indexes for fast `addPks`.
1981
+ */
1982
+ addSlots(key, slots) {
1983
+ if (slots.length === 0) return;
1984
+ let bucket = this.store.getOneByKey(key);
1985
+ if (!bucket) {
1986
+ bucket = /* @__PURE__ */ new Set();
1987
+ this.store.setOneByKey(key, bucket);
1988
+ }
1989
+ let changed = false;
1990
+ for (let i = 0; i < slots.length; i++) {
1991
+ if (!bucket.has(slots[i])) {
1992
+ bucket.add(slots[i]);
1993
+ changed = true;
1994
+ }
1995
+ }
1996
+ if (changed) this.emitUpdateOne(key);
1997
+ }
1998
+ /**
1999
+ * Removes slots from a key's bucket IN PLACE (O(removed)) and emits once.
2000
+ */
2001
+ removeSlots(key, slots) {
2002
+ if (slots.length === 0) return;
2003
+ const bucket = this.store.getOneByKey(key);
2004
+ if (!bucket) return;
2005
+ let changed = false;
2006
+ for (let i = 0; i < slots.length; i++) {
2007
+ if (bucket.delete(slots[i])) changed = true;
2008
+ }
2009
+ if (!changed) return;
2010
+ if (bucket.size === 0) this.store.removeOneByKey(key);
2011
+ this.emitUpdateOne(key);
2012
+ }
1698
2013
  /**
1699
2014
  * Clear all primary keys for a specific index key, or all keys if no key specified
1700
2015
  */
@@ -1739,6 +2054,12 @@ var OIMReactiveIndexManualSetBased = class extends OIMReactiveIndexSetBased {
1739
2054
  setSlots(key, slots) {
1740
2055
  this.index.setSlots(key, slots);
1741
2056
  }
2057
+ addSlots(key, slots) {
2058
+ this.index.addSlots(key, slots);
2059
+ }
2060
+ removeSlots(key, slots) {
2061
+ this.index.removeSlots(key, slots);
2062
+ }
1742
2063
  clear(key) {
1743
2064
  this.index.clear(key);
1744
2065
  }
@@ -1747,46 +2068,73 @@ var OIMReactiveIndexManualSetBased = class extends OIMReactiveIndexSetBased {
1747
2068
  // src/core/OIMReactiveCollectionIndexManualSetBased.ts
1748
2069
  var OIMReactiveCollectionIndexManualSetBased = class extends OIMReactiveIndexManualSetBased {
1749
2070
  constructor(queue, opts) {
1750
- const resolveSlot = opts.collection !== void 0 ? (pk) => opts.collection.getSlotByPk(pk) : opts.resolveSlot;
2071
+ const resolveSlot = opts.collection !== void 0 ? (pk) => opts.collection.getOrReserveSlotByPk(pk) : opts.resolveSlot;
1751
2072
  super(queue, {
1752
2073
  indexOptions: opts.indexOptions
1753
2074
  });
2075
+ // Persistent per-key pk → slot map for O(1) add (dedup) and O(1) remove
2076
+ // (slot lookup by pk). Kept in sync by setPks/addPks/removePks/clear.
2077
+ this.slotByPk = /* @__PURE__ */ new Map();
1754
2078
  this.resolveSlot = resolveSlot;
1755
2079
  }
1756
2080
  setPks(key, pks) {
1757
- this.setSlots(key, this.resolveSlots(pks));
2081
+ const map = /* @__PURE__ */ new Map();
2082
+ const slots = [];
2083
+ for (let i = 0; i < pks.length; i++) {
2084
+ const pk = pks[i];
2085
+ const slot = this.resolveRequiredSlot(pk);
2086
+ if (!map.has(pk)) map.set(pk, slot);
2087
+ slots.push(slot);
2088
+ }
2089
+ this.slotByPk.set(key, map);
2090
+ super.setSlots(key, slots);
1758
2091
  }
1759
2092
  addPks(key, pks) {
1760
2093
  if (pks.length === 0) return;
1761
- const existingSlots = this.index.getSlotsByKey(key);
1762
- const existingPks = this.index.getPksByKey(key);
1763
- const nextSlots = new Set(existingSlots);
1764
- let hasChanges = false;
2094
+ const map = this.membershipOf(key);
2095
+ const newSlots = [];
1765
2096
  for (const pk of pks) {
1766
- if (existingPks.has(pk)) continue;
2097
+ if (map.has(pk)) continue;
1767
2098
  const slot = this.resolveRequiredSlot(pk);
1768
- nextSlots.add(slot);
1769
- existingPks.add(pk);
1770
- hasChanges = true;
2099
+ map.set(pk, slot);
2100
+ newSlots.push(slot);
1771
2101
  }
1772
- if (hasChanges) this.setSlots(key, nextSlots);
2102
+ if (newSlots.length > 0) this.addSlots(key, newSlots);
1773
2103
  }
1774
2104
  removePks(key, pks) {
1775
2105
  if (pks.length === 0) return;
1776
- const pksToRemove = new Set(pks);
1777
- const existingSlots = this.index.getSlotsByKey(key);
1778
- const nextSlots = /* @__PURE__ */ new Set();
1779
- let hasChanges = false;
1780
- for (const slot of existingSlots) {
1781
- if (pksToRemove.has(slot.pk)) {
1782
- hasChanges = true;
1783
- } else {
1784
- nextSlots.add(slot);
2106
+ const map = this.membershipOf(key);
2107
+ if (map.size === 0) return;
2108
+ const removed = [];
2109
+ for (const pk of pks) {
2110
+ const slot = map.get(pk);
2111
+ if (slot) {
2112
+ map.delete(pk);
2113
+ removed.push(slot);
1785
2114
  }
1786
2115
  }
1787
- if (!hasChanges) return;
1788
- if (nextSlots.size === 0) this.clear(key);
1789
- else this.setSlots(key, nextSlots);
2116
+ if (removed.length === 0) return;
2117
+ if (map.size === 0) this.slotByPk.delete(key);
2118
+ this.removeSlots(key, removed);
2119
+ }
2120
+ clear(key) {
2121
+ if (key === void 0) this.slotByPk.clear();
2122
+ else this.slotByPk.delete(key);
2123
+ super.clear(key);
2124
+ }
2125
+ /**
2126
+ * The pk→slot map for a key, lazily seeded from the current bucket so it
2127
+ * stays correct even if slots were set through a lower-level path.
2128
+ */
2129
+ membershipOf(key) {
2130
+ let map = this.slotByPk.get(key);
2131
+ if (!map) {
2132
+ map = /* @__PURE__ */ new Map();
2133
+ const existing = this.index.getSlotsByKey(key);
2134
+ for (const slot of existing) map.set(slot.pk, slot);
2135
+ this.slotByPk.set(key, map);
2136
+ }
2137
+ return map;
1790
2138
  }
1791
2139
  getEntitiesByKey(key) {
1792
2140
  return super.getEntitiesByKey(key);
@@ -1794,18 +2142,9 @@ var OIMReactiveCollectionIndexManualSetBased = class extends OIMReactiveIndexMan
1794
2142
  getEntitiesByKeys(keys) {
1795
2143
  return super.getEntitiesByKeys(keys);
1796
2144
  }
1797
- resolveSlots(pks) {
1798
- const slots = /* @__PURE__ */ new Set();
1799
- for (const pk of pks) slots.add(this.resolveRequiredSlot(pk));
1800
- return slots;
1801
- }
1802
2145
  resolveRequiredSlot(pk) {
1803
2146
  const slot = this.resolveSlot(pk);
1804
- if (!slot) {
1805
- throw new Error(
1806
- `[OIMReactiveCollectionIndexManualSetBased]: Unable to resolve slot for PK "${String(pk)}".`
1807
- );
1808
- }
2147
+ if (!slot) return { pk, item: void 0 };
1809
2148
  return slot;
1810
2149
  }
1811
2150
  };
@@ -1920,17 +2259,56 @@ var OIMDerivedCollectionIndexArrayBased = class extends OIMReactiveCollectionInd
1920
2259
  indexOptions: opts.indexOptions
1921
2260
  });
1922
2261
  this.keysByPk = /* @__PURE__ */ new Map();
2262
+ // Last known sort value per pk (only when `orderBy` is used), so an update
2263
+ // touching an unrelated field can skip re-sorting when the value is unchanged.
2264
+ this.orderValueByPk = /* @__PURE__ */ new Map();
1923
2265
  this.onCollectionUpdate = (pks) => {
1924
2266
  if (pks.length === 0) {
1925
2267
  this.rebuildFromCollection();
1926
2268
  return;
1927
2269
  }
1928
- const affectedKeys = /* @__PURE__ */ new Set();
2270
+ const ordered = !!(this.compareEntities || this.orderBy);
2271
+ const keysToSort = ordered ? /* @__PURE__ */ new Set() : null;
1929
2272
  for (const pk of pks) {
1930
- this.trackPkKeys(pk, affectedKeys);
2273
+ const prevKeys = this.keysByPk.get(pk) ?? /* @__PURE__ */ new Set();
2274
+ const slot = this.collection.getSlotByPk(pk);
2275
+ const entity = slot?.item;
2276
+ const nextKeys = entity === void 0 ? /* @__PURE__ */ new Set() : new Set(this.normalizeKeys(this.selectIndexKeys(entity)));
2277
+ let orderChanged = false;
2278
+ if (this.orderBy) {
2279
+ if (entity === void 0 || nextKeys.size === 0) {
2280
+ this.orderValueByPk.delete(pk);
2281
+ } else {
2282
+ const value = this.orderBy(entity);
2283
+ orderChanged = !this.orderValueByPk.has(pk) || this.orderValueByPk.get(pk) !== value;
2284
+ this.orderValueByPk.set(pk, value);
2285
+ }
2286
+ } else if (this.compareEntities && entity !== void 0) {
2287
+ orderChanged = true;
2288
+ }
2289
+ for (const key of prevKeys) {
2290
+ if (!nextKeys.has(key)) this.removePks(key, [pk]);
2291
+ }
2292
+ if (entity !== void 0) {
2293
+ for (const key of nextKeys) {
2294
+ if (!prevKeys.has(key)) {
2295
+ this.addPks(key, [pk]);
2296
+ keysToSort?.add(key);
2297
+ } else if (orderChanged) {
2298
+ keysToSort?.add(key);
2299
+ }
2300
+ }
2301
+ }
2302
+ if (nextKeys.size === 0) this.keysByPk.delete(pk);
2303
+ else this.keysByPk.set(pk, nextKeys);
1931
2304
  }
1932
- for (const key of affectedKeys) {
1933
- this.rebuildKey(key);
2305
+ if (keysToSort) {
2306
+ for (const key of keysToSort) {
2307
+ this.setSlots(
2308
+ key,
2309
+ this.sortSlots(this.index.getSlotsByKey(key))
2310
+ );
2311
+ }
1934
2312
  }
1935
2313
  };
1936
2314
  this.collection = collection;
@@ -1947,6 +2325,7 @@ var OIMDerivedCollectionIndexArrayBased = class extends OIMReactiveCollectionInd
1947
2325
  }
1948
2326
  rebuildFromCollection() {
1949
2327
  this.keysByPk.clear();
2328
+ this.orderValueByPk.clear();
1950
2329
  super.clear();
1951
2330
  const slotsByKey = /* @__PURE__ */ new Map();
1952
2331
  const slots = this.collection.getAllSlots();
@@ -1955,6 +2334,9 @@ var OIMDerivedCollectionIndexArrayBased = class extends OIMReactiveCollectionInd
1955
2334
  const keys = this.normalizeKeys(this.selectIndexKeys(slot.item));
1956
2335
  if (keys.length === 0) continue;
1957
2336
  this.keysByPk.set(slot.pk, new Set(keys));
2337
+ if (this.orderBy) {
2338
+ this.orderValueByPk.set(slot.pk, this.orderBy(slot.item));
2339
+ }
1958
2340
  for (const key of keys) {
1959
2341
  let slotsForKey = slotsByKey.get(key);
1960
2342
  if (!slotsForKey) {
@@ -1971,6 +2353,7 @@ var OIMDerivedCollectionIndexArrayBased = class extends OIMReactiveCollectionInd
1971
2353
  clear(key) {
1972
2354
  if (key === void 0) {
1973
2355
  this.keysByPk.clear();
2356
+ this.orderValueByPk.clear();
1974
2357
  } else {
1975
2358
  for (const keys of this.keysByPk.values()) {
1976
2359
  keys.delete(key);
@@ -1981,37 +2364,11 @@ var OIMDerivedCollectionIndexArrayBased = class extends OIMReactiveCollectionInd
1981
2364
  destroy() {
1982
2365
  this.unsubscribeFromCollection();
1983
2366
  this.keysByPk.clear();
2367
+ this.orderValueByPk.clear();
1984
2368
  super.destroy();
1985
2369
  }
1986
- trackPkKeys(pk, affectedKeys) {
1987
- const prevKeys = this.keysByPk.get(pk) ?? /* @__PURE__ */ new Set();
1988
- for (const key of prevKeys) affectedKeys.add(key);
1989
- const slot = this.collection.getSlotByPk(pk);
1990
- const entity = slot?.item;
1991
- const nextKeys = entity === void 0 ? /* @__PURE__ */ new Set() : new Set(this.normalizeKeys(this.selectIndexKeys(entity)));
1992
- for (const key of nextKeys) affectedKeys.add(key);
1993
- if (nextKeys.size === 0) {
1994
- this.keysByPk.delete(pk);
1995
- } else {
1996
- this.keysByPk.set(pk, nextKeys);
1997
- }
1998
- }
1999
- rebuildKey(key) {
2000
- const keySlots = [];
2001
- const slots = this.collection.getAllSlots();
2002
- for (const slot of slots) {
2003
- if (slot.item === void 0) continue;
2004
- const keys = this.keysByPk.get(slot.pk);
2005
- if (keys?.has(key)) keySlots.push(slot);
2006
- }
2007
- if (keySlots.length === 0) {
2008
- super.clear(key);
2009
- } else {
2010
- this.setSlots(key, this.sortSlots(keySlots));
2011
- }
2012
- }
2013
2370
  sortSlots(slots) {
2014
- if (!this.compareEntities && !this.orderBy) return slots;
2371
+ if (!this.compareEntities && !this.orderBy) return slots.slice();
2015
2372
  return slots.slice().sort((a, b) => {
2016
2373
  const aItem = a.item;
2017
2374
  const bItem = b.item;
@@ -2294,6 +2651,11 @@ var OIMComparatorFactory = class {
2294
2651
  }
2295
2652
  };
2296
2653
 
2654
+ // src/core/createInPlaceEntityUpdater.ts
2655
+ function createInPlaceEntityUpdater() {
2656
+ return (draft, prev) => Object.assign(prev, draft);
2657
+ }
2658
+
2297
2659
  // src/core/OIMEventQueue.ts
2298
2660
  var OIMEventQueue = class {
2299
2661
  constructor(options = {}) {
@@ -3729,6 +4091,7 @@ var OIMIndexPksByKeysArrayBasedSelector = class extends OIMSelector {
3729
4091
  EOIMEventQueueSchedulerEventType,
3730
4092
  EOIMIndexEventType,
3731
4093
  EOIMObjectEventType,
4094
+ OIMCarrierKeyedEmitter,
3732
4095
  OIMCollection,
3733
4096
  OIMCollectionByPkSelector,
3734
4097
  OIMCollectionByPksSelector,
@@ -3796,6 +4159,7 @@ var OIMIndexPksByKeysArrayBasedSelector = class extends OIMSelector {
3796
4159
  OIMReactiveIndexSetBased,
3797
4160
  OIMReactiveObject,
3798
4161
  OIMSelector,
4162
+ createInPlaceEntityUpdater,
3799
4163
  createOIMCollectionIndexFactory,
3800
4164
  createOIMCollectionKit
3801
4165
  });