@oimdb/core 2.2.0 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -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
  }
@@ -924,6 +935,12 @@ var OIMCollectionStoreMapDriven = class extends OIMCollectionStore {
924
935
  getSlotByPk(pk) {
925
936
  return this.slots.get(pk);
926
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
+ }
927
944
  getSlotsByPks(pks) {
928
945
  const result = [];
929
946
  result.length = pks.length;
@@ -942,12 +959,20 @@ var OIMCollectionStoreMapDriven = class extends OIMCollectionStore {
942
959
  }
943
960
  removeOneByPk(pk) {
944
961
  const slot = this.slots.get(pk);
945
- if (slot) slot.item = void 0;
946
- 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
+ }
947
970
  const reserved = this.reservedSlots.get(pk);
948
971
  if (reserved) {
949
972
  reserved.item = void 0;
950
- this.reservedSlots.delete(pk);
973
+ if (!this.hasSubscribers(reserved)) {
974
+ this.reservedSlots.delete(pk);
975
+ }
951
976
  }
952
977
  }
953
978
  removeManyByPks(pks) {
@@ -982,20 +1007,31 @@ var OIMCollectionStoreMapDriven = class extends OIMCollectionStore {
982
1007
  return this.slots.size;
983
1008
  }
984
1009
  clear() {
1010
+ const retained = [];
985
1011
  for (const slot of this.slots.values()) {
986
1012
  slot.item = void 0;
1013
+ if (this.hasSubscribers(slot)) retained.push(slot);
987
1014
  }
988
1015
  this.slots.clear();
989
1016
  for (const slot of this.reservedSlots.values()) {
990
1017
  slot.item = void 0;
1018
+ if (this.hasSubscribers(slot)) retained.push(slot);
991
1019
  }
992
1020
  this.reservedSlots.clear();
1021
+ for (const slot of retained) {
1022
+ this.reservedSlots.set(slot.pk, slot);
1023
+ }
993
1024
  }
994
1025
  getAllPks() {
995
1026
  return Array.from(this.slots.keys());
996
1027
  }
997
1028
  destroy() {
998
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();
999
1035
  }
1000
1036
  };
1001
1037
 
@@ -1118,6 +1154,153 @@ var OIMCollection = class {
1118
1154
  }
1119
1155
  };
1120
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
+
1121
1304
  // src/core/OIMReactiveCollection.ts
1122
1305
  var OIMReactiveCollection = class extends OIMCollection {
1123
1306
  constructor(queue, opts) {
@@ -1137,7 +1320,10 @@ var OIMReactiveCollection = class extends OIMCollection {
1137
1320
  for (let i = 0; i < snapshot.length; i++) snapshot[i](pks);
1138
1321
  };
1139
1322
  this.queue = queue;
1140
- 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
+ });
1141
1327
  }
1142
1328
  subscribeOnKey(pk, handler) {
1143
1329
  return this.updateEmitter.subscribeOnKey(pk, handler);
@@ -1169,17 +1355,31 @@ var OIMReactiveCollection = class extends OIMCollection {
1169
1355
  this.anyUpdateHandlers.delete(handler);
1170
1356
  };
1171
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
+ }
1172
1372
  upsertOneByPk(pk, entity) {
1173
1373
  const slot = this.upsertOneWithoutNotificationsByPk(pk, entity);
1174
- this.emitter.emit(0 /* UPDATE */, { pks: [pk] });
1175
- this.updateEmitter.markUpdatedKey(pk);
1374
+ this.notifyLegacy(pk);
1375
+ this.updateEmitter.markUpdatedCarrier(slot);
1176
1376
  this.trackAnyUpdatePk(pk);
1177
1377
  return slot;
1178
1378
  }
1179
1379
  upsertOne(entity) {
1180
1380
  const slot = this.upsertOneWithoutNotifications(entity);
1181
- this.emitter.emit(0 /* UPDATE */, { pks: [slot.pk] });
1182
- this.updateEmitter.markUpdatedKey(slot.pk);
1381
+ this.notifyLegacy(slot.pk);
1382
+ this.updateEmitter.markUpdatedCarrier(slot);
1183
1383
  this.trackAnyUpdatePk(slot.pk);
1184
1384
  return slot;
1185
1385
  }
@@ -1189,15 +1389,17 @@ var OIMReactiveCollection = class extends OIMCollection {
1189
1389
  (entity) => this.upsertOneWithoutNotifications(entity)
1190
1390
  );
1191
1391
  const pks = slots.map((slot) => slot.pk);
1192
- this.emitter.emit(0 /* UPDATE */, { pks });
1193
- this.updateEmitter.markUpdatedKeys(pks);
1392
+ this.notifyLegacyMany(pks);
1393
+ for (let i = 0; i < slots.length; i++) {
1394
+ this.updateEmitter.markUpdatedCarrier(slots[i]);
1395
+ }
1194
1396
  this.trackAnyUpdatePks(pks);
1195
1397
  return slots;
1196
1398
  }
1197
1399
  removeOne(entity) {
1198
1400
  const pk = this.selectPk(entity);
1199
1401
  this.store.removeOneByPk(pk);
1200
- this.emitter.emit(0 /* UPDATE */, { pks: [pk] });
1402
+ this.notifyLegacy(pk);
1201
1403
  this.updateEmitter.markUpdatedKey(pk);
1202
1404
  this.trackAnyUpdatePk(pk);
1203
1405
  }
@@ -1205,26 +1407,28 @@ var OIMReactiveCollection = class extends OIMCollection {
1205
1407
  if (entities.length === 0) return;
1206
1408
  const pks = entities.map(this.selectPk);
1207
1409
  this.store.removeManyByPks(pks);
1208
- this.emitter.emit(0 /* UPDATE */, { pks });
1410
+ this.notifyLegacyMany(pks);
1209
1411
  this.updateEmitter.markUpdatedKeys(pks);
1210
1412
  this.trackAnyUpdatePks(pks);
1211
1413
  }
1212
1414
  removeOneByPk(pk) {
1213
1415
  this.store.removeOneByPk(pk);
1214
- this.emitter.emit(0 /* UPDATE */, { pks: [pk] });
1416
+ this.notifyLegacy(pk);
1215
1417
  this.updateEmitter.markUpdatedKey(pk);
1216
1418
  this.trackAnyUpdatePk(pk);
1217
1419
  }
1218
1420
  removeManyByPks(pks) {
1219
1421
  if (pks.length === 0) return;
1220
1422
  this.store.removeManyByPks(pks);
1221
- this.emitter.emit(0 /* UPDATE */, { pks });
1423
+ this.notifyLegacyMany(pks);
1222
1424
  this.updateEmitter.markUpdatedKeys(pks);
1223
1425
  this.trackAnyUpdatePks(pks);
1224
1426
  }
1225
1427
  clear() {
1226
1428
  this.store.clear();
1227
- this.emitter.emit(0 /* UPDATE */, { pks: [] });
1429
+ if (this.emitter.hasHandlers(0 /* UPDATE */)) {
1430
+ this.emitter.emit(0 /* UPDATE */, { pks: [] });
1431
+ }
1228
1432
  this.updateEmitter.markAllUpdated();
1229
1433
  this.trackAnyUpdateClear();
1230
1434
  }
@@ -1607,6 +1811,21 @@ var OIMIndexManualArrayBased = class extends OIMIndexArrayBased {
1607
1811
  this.emitUpdateOne(key);
1608
1812
  }
1609
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
+ }
1610
1829
  /**
1611
1830
  * Clear all primary keys for a specific index key, or all keys if no key specified
1612
1831
  */
@@ -1651,6 +1870,9 @@ var OIMReactiveIndexManualArrayBased = class extends OIMReactiveIndexArrayBased
1651
1870
  setSlots(key, slots) {
1652
1871
  this.index.setSlots(key, slots);
1653
1872
  }
1873
+ appendSlots(key, slots) {
1874
+ this.index.appendSlots(key, slots);
1875
+ }
1654
1876
  clear(key) {
1655
1877
  this.index.clear(key);
1656
1878
  }
@@ -1663,24 +1885,27 @@ var OIMReactiveCollectionIndexManualArrayBased = class extends OIMReactiveIndexM
1663
1885
  super(queue, {
1664
1886
  indexOptions: opts.indexOptions
1665
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();
1666
1891
  this.resolveSlot = resolveSlot;
1667
1892
  }
1668
1893
  setPks(key, pks) {
1669
- 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));
1670
1898
  }
1671
1899
  addPks(key, pks) {
1672
1900
  if (pks.length === 0) return;
1673
- const existingSlots = this.index.getSlotsByKey(key);
1674
- const existingPks = new Set(existingSlots.map((slot) => slot.pk));
1675
- const nextSlots = existingSlots.slice();
1676
- let hasChanges = false;
1901
+ const pkSet = this.membershipOf(key);
1902
+ const newSlots = [];
1677
1903
  for (const pk of pks) {
1678
- if (existingPks.has(pk)) continue;
1679
- nextSlots.push(this.resolveRequiredSlot(pk));
1680
- existingPks.add(pk);
1681
- hasChanges = true;
1904
+ if (pkSet.has(pk)) continue;
1905
+ pkSet.add(pk);
1906
+ newSlots.push(this.resolveRequiredSlot(pk));
1682
1907
  }
1683
- if (hasChanges) this.setSlots(key, nextSlots);
1908
+ if (newSlots.length > 0) this.appendSlots(key, newSlots);
1684
1909
  }
1685
1910
  removePks(key, pks) {
1686
1911
  if (pks.length === 0) return;
@@ -1690,8 +1915,33 @@ var OIMReactiveCollectionIndexManualArrayBased = class extends OIMReactiveIndexM
1690
1915
  (slot) => !pksToRemove.has(slot.pk)
1691
1916
  );
1692
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
+ }
1693
1922
  if (nextSlots.length === 0) this.clear(key);
1694
- 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;
1695
1945
  }
1696
1946
  getEntitiesByKey(key) {
1697
1947
  return super.getEntitiesByKey(key);
@@ -1725,6 +1975,41 @@ var OIMIndexManualSetBased = class extends OIMIndexSetBased {
1725
1975
  this.emitUpdateOne(key);
1726
1976
  }
1727
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
+ }
1728
2013
  /**
1729
2014
  * Clear all primary keys for a specific index key, or all keys if no key specified
1730
2015
  */
@@ -1769,6 +2054,12 @@ var OIMReactiveIndexManualSetBased = class extends OIMReactiveIndexSetBased {
1769
2054
  setSlots(key, slots) {
1770
2055
  this.index.setSlots(key, slots);
1771
2056
  }
2057
+ addSlots(key, slots) {
2058
+ this.index.addSlots(key, slots);
2059
+ }
2060
+ removeSlots(key, slots) {
2061
+ this.index.removeSlots(key, slots);
2062
+ }
1772
2063
  clear(key) {
1773
2064
  this.index.clear(key);
1774
2065
  }
@@ -1781,42 +2072,69 @@ var OIMReactiveCollectionIndexManualSetBased = class extends OIMReactiveIndexMan
1781
2072
  super(queue, {
1782
2073
  indexOptions: opts.indexOptions
1783
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();
1784
2078
  this.resolveSlot = resolveSlot;
1785
2079
  }
1786
2080
  setPks(key, pks) {
1787
- 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);
1788
2091
  }
1789
2092
  addPks(key, pks) {
1790
2093
  if (pks.length === 0) return;
1791
- const existingSlots = this.index.getSlotsByKey(key);
1792
- const existingPks = this.index.getPksByKey(key);
1793
- const nextSlots = new Set(existingSlots);
1794
- let hasChanges = false;
2094
+ const map = this.membershipOf(key);
2095
+ const newSlots = [];
1795
2096
  for (const pk of pks) {
1796
- if (existingPks.has(pk)) continue;
2097
+ if (map.has(pk)) continue;
1797
2098
  const slot = this.resolveRequiredSlot(pk);
1798
- nextSlots.add(slot);
1799
- existingPks.add(pk);
1800
- hasChanges = true;
2099
+ map.set(pk, slot);
2100
+ newSlots.push(slot);
1801
2101
  }
1802
- if (hasChanges) this.setSlots(key, nextSlots);
2102
+ if (newSlots.length > 0) this.addSlots(key, newSlots);
1803
2103
  }
1804
2104
  removePks(key, pks) {
1805
2105
  if (pks.length === 0) return;
1806
- const pksToRemove = new Set(pks);
1807
- const existingSlots = this.index.getSlotsByKey(key);
1808
- const nextSlots = /* @__PURE__ */ new Set();
1809
- let hasChanges = false;
1810
- for (const slot of existingSlots) {
1811
- if (pksToRemove.has(slot.pk)) {
1812
- hasChanges = true;
1813
- } else {
1814
- 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);
1815
2114
  }
1816
2115
  }
1817
- if (!hasChanges) return;
1818
- if (nextSlots.size === 0) this.clear(key);
1819
- 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;
1820
2138
  }
1821
2139
  getEntitiesByKey(key) {
1822
2140
  return super.getEntitiesByKey(key);
@@ -1824,11 +2142,6 @@ var OIMReactiveCollectionIndexManualSetBased = class extends OIMReactiveIndexMan
1824
2142
  getEntitiesByKeys(keys) {
1825
2143
  return super.getEntitiesByKeys(keys);
1826
2144
  }
1827
- resolveSlots(pks) {
1828
- const slots = /* @__PURE__ */ new Set();
1829
- for (const pk of pks) slots.add(this.resolveRequiredSlot(pk));
1830
- return slots;
1831
- }
1832
2145
  resolveRequiredSlot(pk) {
1833
2146
  const slot = this.resolveSlot(pk);
1834
2147
  if (!slot) return { pk, item: void 0 };
@@ -1946,17 +2259,56 @@ var OIMDerivedCollectionIndexArrayBased = class extends OIMReactiveCollectionInd
1946
2259
  indexOptions: opts.indexOptions
1947
2260
  });
1948
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();
1949
2265
  this.onCollectionUpdate = (pks) => {
1950
2266
  if (pks.length === 0) {
1951
2267
  this.rebuildFromCollection();
1952
2268
  return;
1953
2269
  }
1954
- const affectedKeys = /* @__PURE__ */ new Set();
2270
+ const ordered = !!(this.compareEntities || this.orderBy);
2271
+ const keysToSort = ordered ? /* @__PURE__ */ new Set() : null;
1955
2272
  for (const pk of pks) {
1956
- 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);
1957
2304
  }
1958
- for (const key of affectedKeys) {
1959
- 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
+ }
1960
2312
  }
1961
2313
  };
1962
2314
  this.collection = collection;
@@ -1973,6 +2325,7 @@ var OIMDerivedCollectionIndexArrayBased = class extends OIMReactiveCollectionInd
1973
2325
  }
1974
2326
  rebuildFromCollection() {
1975
2327
  this.keysByPk.clear();
2328
+ this.orderValueByPk.clear();
1976
2329
  super.clear();
1977
2330
  const slotsByKey = /* @__PURE__ */ new Map();
1978
2331
  const slots = this.collection.getAllSlots();
@@ -1981,6 +2334,9 @@ var OIMDerivedCollectionIndexArrayBased = class extends OIMReactiveCollectionInd
1981
2334
  const keys = this.normalizeKeys(this.selectIndexKeys(slot.item));
1982
2335
  if (keys.length === 0) continue;
1983
2336
  this.keysByPk.set(slot.pk, new Set(keys));
2337
+ if (this.orderBy) {
2338
+ this.orderValueByPk.set(slot.pk, this.orderBy(slot.item));
2339
+ }
1984
2340
  for (const key of keys) {
1985
2341
  let slotsForKey = slotsByKey.get(key);
1986
2342
  if (!slotsForKey) {
@@ -1997,6 +2353,7 @@ var OIMDerivedCollectionIndexArrayBased = class extends OIMReactiveCollectionInd
1997
2353
  clear(key) {
1998
2354
  if (key === void 0) {
1999
2355
  this.keysByPk.clear();
2356
+ this.orderValueByPk.clear();
2000
2357
  } else {
2001
2358
  for (const keys of this.keysByPk.values()) {
2002
2359
  keys.delete(key);
@@ -2007,37 +2364,11 @@ var OIMDerivedCollectionIndexArrayBased = class extends OIMReactiveCollectionInd
2007
2364
  destroy() {
2008
2365
  this.unsubscribeFromCollection();
2009
2366
  this.keysByPk.clear();
2367
+ this.orderValueByPk.clear();
2010
2368
  super.destroy();
2011
2369
  }
2012
- trackPkKeys(pk, affectedKeys) {
2013
- const prevKeys = this.keysByPk.get(pk) ?? /* @__PURE__ */ new Set();
2014
- for (const key of prevKeys) affectedKeys.add(key);
2015
- const slot = this.collection.getSlotByPk(pk);
2016
- const entity = slot?.item;
2017
- const nextKeys = entity === void 0 ? /* @__PURE__ */ new Set() : new Set(this.normalizeKeys(this.selectIndexKeys(entity)));
2018
- for (const key of nextKeys) affectedKeys.add(key);
2019
- if (nextKeys.size === 0) {
2020
- this.keysByPk.delete(pk);
2021
- } else {
2022
- this.keysByPk.set(pk, nextKeys);
2023
- }
2024
- }
2025
- rebuildKey(key) {
2026
- const keySlots = [];
2027
- const slots = this.collection.getAllSlots();
2028
- for (const slot of slots) {
2029
- if (slot.item === void 0) continue;
2030
- const keys = this.keysByPk.get(slot.pk);
2031
- if (keys?.has(key)) keySlots.push(slot);
2032
- }
2033
- if (keySlots.length === 0) {
2034
- super.clear(key);
2035
- } else {
2036
- this.setSlots(key, this.sortSlots(keySlots));
2037
- }
2038
- }
2039
2370
  sortSlots(slots) {
2040
- if (!this.compareEntities && !this.orderBy) return slots;
2371
+ if (!this.compareEntities && !this.orderBy) return slots.slice();
2041
2372
  return slots.slice().sort((a, b) => {
2042
2373
  const aItem = a.item;
2043
2374
  const bItem = b.item;
@@ -2320,6 +2651,11 @@ var OIMComparatorFactory = class {
2320
2651
  }
2321
2652
  };
2322
2653
 
2654
+ // src/core/createInPlaceEntityUpdater.ts
2655
+ function createInPlaceEntityUpdater() {
2656
+ return (draft, prev) => Object.assign(prev, draft);
2657
+ }
2658
+
2323
2659
  // src/core/OIMEventQueue.ts
2324
2660
  var OIMEventQueue = class {
2325
2661
  constructor(options = {}) {
@@ -3755,6 +4091,7 @@ var OIMIndexPksByKeysArrayBasedSelector = class extends OIMSelector {
3755
4091
  EOIMEventQueueSchedulerEventType,
3756
4092
  EOIMIndexEventType,
3757
4093
  EOIMObjectEventType,
4094
+ OIMCarrierKeyedEmitter,
3758
4095
  OIMCollection,
3759
4096
  OIMCollectionByPkSelector,
3760
4097
  OIMCollectionByPksSelector,
@@ -3822,6 +4159,7 @@ var OIMIndexPksByKeysArrayBasedSelector = class extends OIMSelector {
3822
4159
  OIMReactiveIndexSetBased,
3823
4160
  OIMReactiveObject,
3824
4161
  OIMSelector,
4162
+ createInPlaceEntityUpdater,
3825
4163
  createOIMCollectionIndexFactory,
3826
4164
  createOIMCollectionKit
3827
4165
  });