@oimdb/core 2.2.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.js CHANGED
@@ -177,10 +177,18 @@ var OIMIndex = class {
177
177
  emitUpdateOne(key) {
178
178
  this.emitUpdate([key]);
179
179
  }
180
+ /**
181
+ * Maps a bucket's slots to entities, preserving positional holes: a slot
182
+ * whose entity is not present yet (reserved by an index ahead of its data)
183
+ * or was removed yields `undefined` at its position rather than being
184
+ * dropped. This keeps the result aligned with `getPksByKey`, so callers can
185
+ * render a per-item loading/placeholder state instead of a silently shorter
186
+ * list.
187
+ */
180
188
  slotsToEntities(slots) {
181
189
  const entities = [];
182
190
  for (const slot of slots) {
183
- if (slot.item !== void 0) entities.push(slot.item);
191
+ entities.push(slot.item);
184
192
  }
185
193
  return entities;
186
194
  }
@@ -436,12 +444,7 @@ var OIMUpdateEventEmitter = class {
436
444
  this.updatedKeys.clear();
437
445
  return;
438
446
  }
439
- const makeHandlerSnapshot = (handlers) => {
440
- const snapshot = [];
441
- handlers.forEach((h) => snapshot.push(h));
442
- return snapshot;
443
- };
444
- this.runSinglePass(makeHandlerSnapshot);
447
+ this.runSinglePass();
445
448
  };
446
449
  this.onAfterFlush = () => {
447
450
  if (!this.isAfterFlushDeliveryScheduled) return;
@@ -536,18 +539,32 @@ var OIMUpdateEventEmitter = class {
536
539
  this.dequeueFlush = this.queue.enqueue(this.onFlush);
537
540
  }
538
541
  deliverImmediate() {
539
- const makeHandlerSnapshot = (handlers) => {
540
- const snapshot = [];
541
- handlers.forEach((h) => snapshot.push(h));
542
- return snapshot;
543
- };
544
- this.runSinglePass(makeHandlerSnapshot);
542
+ this.runSinglePass();
543
+ }
544
+ /**
545
+ * Notifies all handlers for a key. The overwhelmingly common case is a
546
+ * single subscriber per key (one component per pk), handled without any
547
+ * allocation. For multiple subscribers a snapshot is taken so a handler may
548
+ * (un)subscribe during iteration without corrupting the walk.
549
+ */
550
+ notifyHandlers(handlers) {
551
+ if (handlers.size === 1) {
552
+ const only = handlers.values().next().value;
553
+ if (only) only();
554
+ return;
555
+ }
556
+ const snapshot = [];
557
+ handlers.forEach((h) => snapshot.push(h));
558
+ for (let i = 0; i < snapshot.length; i++) {
559
+ const handler = snapshot[i];
560
+ if (handlers.has(handler)) handler();
561
+ }
545
562
  }
546
563
  /**
547
564
  * Deliver updated keys and notify handlers once.
548
565
  * Re-entrant updates during delivery are forbidden and will throw in markUpdatedKeys().
549
566
  */
550
- runSinglePass(makeHandlerSnapshot) {
567
+ runSinglePass() {
551
568
  if (this.updatedKeys.size === 0) return;
552
569
  const flushingKeys = this.updatedKeys;
553
570
  this.updatedKeys = /* @__PURE__ */ new Set();
@@ -556,21 +573,13 @@ var OIMUpdateEventEmitter = class {
556
573
  flushingKeys.forEach((key) => {
557
574
  const handlers = this.keyHandlers.get(key);
558
575
  if (!handlers || handlers.size === 0) return;
559
- const handlerSnapshot = makeHandlerSnapshot(handlers);
560
- for (let i = 0; i < handlerSnapshot.length; i++) {
561
- const handler = handlerSnapshot[i];
562
- if (handlers.has(handler)) handler();
563
- }
576
+ this.notifyHandlers(handlers);
564
577
  });
565
578
  } else {
566
579
  this.keyHandlers.forEach((handlers, key) => {
567
580
  if (!flushingKeys.has(key)) return;
568
581
  if (!handlers || handlers.size === 0) return;
569
- const handlerSnapshot = makeHandlerSnapshot(handlers);
570
- for (let i = 0; i < handlerSnapshot.length; i++) {
571
- const handler = handlerSnapshot[i];
572
- if (handlers.has(handler)) handler();
573
- }
582
+ this.notifyHandlers(handlers);
574
583
  });
575
584
  }
576
585
  }
@@ -824,6 +833,12 @@ var OIMCollectionStoreMapDriven = class extends OIMCollectionStore {
824
833
  getSlotByPk(pk) {
825
834
  return this.slots.get(pk);
826
835
  }
836
+ findSlotByPk(pk) {
837
+ return this.slots.get(pk) ?? this.reservedSlots.get(pk);
838
+ }
839
+ hasSubscribers(slot) {
840
+ return slot.subscribers !== void 0 && slot.subscribers.size > 0;
841
+ }
827
842
  getSlotsByPks(pks) {
828
843
  const result = [];
829
844
  result.length = pks.length;
@@ -842,12 +857,20 @@ var OIMCollectionStoreMapDriven = class extends OIMCollectionStore {
842
857
  }
843
858
  removeOneByPk(pk) {
844
859
  const slot = this.slots.get(pk);
845
- if (slot) slot.item = void 0;
846
- this.slots.delete(pk);
860
+ if (slot) {
861
+ slot.item = void 0;
862
+ this.slots.delete(pk);
863
+ if (this.hasSubscribers(slot)) {
864
+ this.reservedSlots.set(pk, slot);
865
+ }
866
+ return;
867
+ }
847
868
  const reserved = this.reservedSlots.get(pk);
848
869
  if (reserved) {
849
870
  reserved.item = void 0;
850
- this.reservedSlots.delete(pk);
871
+ if (!this.hasSubscribers(reserved)) {
872
+ this.reservedSlots.delete(pk);
873
+ }
851
874
  }
852
875
  }
853
876
  removeManyByPks(pks) {
@@ -882,20 +905,31 @@ var OIMCollectionStoreMapDriven = class extends OIMCollectionStore {
882
905
  return this.slots.size;
883
906
  }
884
907
  clear() {
908
+ const retained = [];
885
909
  for (const slot of this.slots.values()) {
886
910
  slot.item = void 0;
911
+ if (this.hasSubscribers(slot)) retained.push(slot);
887
912
  }
888
913
  this.slots.clear();
889
914
  for (const slot of this.reservedSlots.values()) {
890
915
  slot.item = void 0;
916
+ if (this.hasSubscribers(slot)) retained.push(slot);
891
917
  }
892
918
  this.reservedSlots.clear();
919
+ for (const slot of retained) {
920
+ this.reservedSlots.set(slot.pk, slot);
921
+ }
893
922
  }
894
923
  getAllPks() {
895
924
  return Array.from(this.slots.keys());
896
925
  }
897
926
  destroy() {
898
927
  this.clear();
928
+ for (const slot of this.reservedSlots.values()) {
929
+ slot.subscribers?.clear();
930
+ slot.subscribers = void 0;
931
+ }
932
+ this.reservedSlots.clear();
899
933
  }
900
934
  };
901
935
 
@@ -1018,6 +1052,153 @@ var OIMCollection = class {
1018
1052
  }
1019
1053
  };
1020
1054
 
1055
+ // src/core/OIMCarrierKeyedEmitter.ts
1056
+ var OIMCarrierKeyedEmitter = class {
1057
+ constructor(queue, resolver) {
1058
+ // Carriers with at least one subscriber — for markAllUpdated / metrics.
1059
+ this.subscribedCarriers = /* @__PURE__ */ new Set();
1060
+ this.dirtyCarriers = /* @__PURE__ */ new Set();
1061
+ this.isFlushEnqueued = false;
1062
+ this.onFlush = () => {
1063
+ this.isFlushEnqueued = false;
1064
+ this.dequeueFlush = void 0;
1065
+ if (this.dirtyCarriers.size === 0) return;
1066
+ const flushing = this.dirtyCarriers;
1067
+ this.dirtyCarriers = /* @__PURE__ */ new Set();
1068
+ flushing.forEach((carrier) => this.notify(carrier));
1069
+ };
1070
+ this.queue = queue;
1071
+ this.resolver = resolver;
1072
+ }
1073
+ subscribeOnKey(key, handler) {
1074
+ const carrier = this.resolver.getOrReserveCarrier(key);
1075
+ this.addHandler(carrier, handler);
1076
+ return () => this.removeHandler(carrier, handler);
1077
+ }
1078
+ subscribeOnKeys(keys, handler) {
1079
+ const carriers = [];
1080
+ for (let i = 0; i < keys.length; i++) {
1081
+ const carrier = this.resolver.getOrReserveCarrier(keys[i]);
1082
+ this.addHandler(carrier, handler);
1083
+ carriers.push(carrier);
1084
+ }
1085
+ return () => {
1086
+ for (let i = 0; i < carriers.length; i++) {
1087
+ this.removeHandler(carriers[i], handler);
1088
+ }
1089
+ };
1090
+ }
1091
+ unsubscribeFromKey(key, handler) {
1092
+ const carrier = this.resolver.findCarrier(key);
1093
+ if (carrier) this.removeHandler(carrier, handler);
1094
+ }
1095
+ unsubscribeFromKeys(keys, handler) {
1096
+ for (let i = 0; i < keys.length; i++) {
1097
+ this.unsubscribeFromKey(keys[i], handler);
1098
+ }
1099
+ }
1100
+ addHandler(carrier, handler) {
1101
+ let subscribers = carrier.subscribers;
1102
+ if (!subscribers) {
1103
+ subscribers = /* @__PURE__ */ new Set();
1104
+ carrier.subscribers = subscribers;
1105
+ }
1106
+ subscribers.add(handler);
1107
+ this.subscribedCarriers.add(carrier);
1108
+ }
1109
+ removeHandler(carrier, handler) {
1110
+ const subscribers = carrier.subscribers;
1111
+ if (!subscribers) return;
1112
+ subscribers.delete(handler);
1113
+ if (subscribers.size === 0) {
1114
+ this.subscribedCarriers.delete(carrier);
1115
+ }
1116
+ }
1117
+ /** Fast path: the writer already holds the carrier. */
1118
+ markUpdatedCarrier(carrier) {
1119
+ this.assertNotInFlush();
1120
+ const subscribers = carrier.subscribers;
1121
+ if (!subscribers || subscribers.size === 0) return;
1122
+ this.dirtyCarriers.add(carrier);
1123
+ this.scheduleFlush();
1124
+ }
1125
+ markUpdatedKey(key) {
1126
+ const carrier = this.resolver.findCarrier(key);
1127
+ if (carrier) this.markUpdatedCarrier(carrier);
1128
+ else this.assertNotInFlush();
1129
+ }
1130
+ markUpdatedKeys(keys) {
1131
+ for (let i = 0; i < keys.length; i++) this.markUpdatedKey(keys[i]);
1132
+ }
1133
+ markAllUpdated() {
1134
+ this.assertNotInFlush();
1135
+ if (this.subscribedCarriers.size === 0) return;
1136
+ this.subscribedCarriers.forEach(
1137
+ (carrier) => this.dirtyCarriers.add(carrier)
1138
+ );
1139
+ this.scheduleFlush();
1140
+ }
1141
+ hasSubscriptions() {
1142
+ return this.subscribedCarriers.size > 0;
1143
+ }
1144
+ getHandlerCount(key) {
1145
+ return this.resolver.findCarrier(key)?.subscribers?.size ?? 0;
1146
+ }
1147
+ getMetrics() {
1148
+ let totalHandlers = 0;
1149
+ this.subscribedCarriers.forEach((carrier) => {
1150
+ totalHandlers += carrier.subscribers?.size ?? 0;
1151
+ });
1152
+ const totalKeys = this.subscribedCarriers.size;
1153
+ return {
1154
+ totalKeys,
1155
+ totalHandlers,
1156
+ averageHandlersPerKey: totalKeys > 0 ? totalHandlers / totalKeys : 0,
1157
+ queueLength: this.dirtyCarriers.size
1158
+ };
1159
+ }
1160
+ destroy() {
1161
+ if (this.dequeueFlush) {
1162
+ this.dequeueFlush();
1163
+ this.dequeueFlush = void 0;
1164
+ }
1165
+ this.subscribedCarriers.forEach((carrier) => {
1166
+ carrier.subscribers?.clear();
1167
+ carrier.subscribers = void 0;
1168
+ });
1169
+ this.subscribedCarriers.clear();
1170
+ this.dirtyCarriers.clear();
1171
+ this.isFlushEnqueued = false;
1172
+ }
1173
+ assertNotInFlush() {
1174
+ if (this.queue.isInFlush) {
1175
+ throw new Error(
1176
+ "OIMCarrierKeyedEmitter: updates during queue.flush() are not allowed. Finish all writes before calling queue.flush()."
1177
+ );
1178
+ }
1179
+ }
1180
+ scheduleFlush() {
1181
+ if (this.isFlushEnqueued) return;
1182
+ this.isFlushEnqueued = true;
1183
+ this.dequeueFlush = this.queue.enqueue(this.onFlush);
1184
+ }
1185
+ notify(carrier) {
1186
+ const subscribers = carrier.subscribers;
1187
+ if (!subscribers || subscribers.size === 0) return;
1188
+ if (subscribers.size === 1) {
1189
+ const only = subscribers.values().next().value;
1190
+ if (only) only();
1191
+ return;
1192
+ }
1193
+ const snapshot = [];
1194
+ subscribers.forEach((h) => snapshot.push(h));
1195
+ for (let i = 0; i < snapshot.length; i++) {
1196
+ const handler = snapshot[i];
1197
+ if (subscribers.has(handler)) handler();
1198
+ }
1199
+ }
1200
+ };
1201
+
1021
1202
  // src/core/OIMReactiveCollection.ts
1022
1203
  var OIMReactiveCollection = class extends OIMCollection {
1023
1204
  constructor(queue, opts) {
@@ -1037,7 +1218,10 @@ var OIMReactiveCollection = class extends OIMCollection {
1037
1218
  for (let i = 0; i < snapshot.length; i++) snapshot[i](pks);
1038
1219
  };
1039
1220
  this.queue = queue;
1040
- this.updateEmitter = new OIMUpdateEventEmitter(queue, "queue");
1221
+ this.updateEmitter = new OIMCarrierKeyedEmitter(queue, {
1222
+ getOrReserveCarrier: (pk) => this.store.getOrReserveSlotByPk(pk),
1223
+ findCarrier: (pk) => this.store.findSlotByPk(pk)
1224
+ });
1041
1225
  }
1042
1226
  subscribeOnKey(pk, handler) {
1043
1227
  return this.updateEmitter.subscribeOnKey(pk, handler);
@@ -1069,17 +1253,31 @@ var OIMReactiveCollection = class extends OIMCollection {
1069
1253
  this.anyUpdateHandlers.delete(handler);
1070
1254
  };
1071
1255
  }
1256
+ // The legacy `emitter` (EOIMCollectionEventType.UPDATE) coexists with the
1257
+ // keyed `updateEmitter`. Build its payload only when something is actually
1258
+ // subscribed — otherwise every upsert/remove allocates a throwaway
1259
+ // `{ pks }` object (and array) that nobody reads.
1260
+ notifyLegacy(pk) {
1261
+ if (this.emitter.hasHandlers(0 /* UPDATE */)) {
1262
+ this.emitter.emit(0 /* UPDATE */, { pks: [pk] });
1263
+ }
1264
+ }
1265
+ notifyLegacyMany(pks) {
1266
+ if (this.emitter.hasHandlers(0 /* UPDATE */)) {
1267
+ this.emitter.emit(0 /* UPDATE */, { pks });
1268
+ }
1269
+ }
1072
1270
  upsertOneByPk(pk, entity) {
1073
1271
  const slot = this.upsertOneWithoutNotificationsByPk(pk, entity);
1074
- this.emitter.emit(0 /* UPDATE */, { pks: [pk] });
1075
- this.updateEmitter.markUpdatedKey(pk);
1272
+ this.notifyLegacy(pk);
1273
+ this.updateEmitter.markUpdatedCarrier(slot);
1076
1274
  this.trackAnyUpdatePk(pk);
1077
1275
  return slot;
1078
1276
  }
1079
1277
  upsertOne(entity) {
1080
1278
  const slot = this.upsertOneWithoutNotifications(entity);
1081
- this.emitter.emit(0 /* UPDATE */, { pks: [slot.pk] });
1082
- this.updateEmitter.markUpdatedKey(slot.pk);
1279
+ this.notifyLegacy(slot.pk);
1280
+ this.updateEmitter.markUpdatedCarrier(slot);
1083
1281
  this.trackAnyUpdatePk(slot.pk);
1084
1282
  return slot;
1085
1283
  }
@@ -1089,15 +1287,17 @@ var OIMReactiveCollection = class extends OIMCollection {
1089
1287
  (entity) => this.upsertOneWithoutNotifications(entity)
1090
1288
  );
1091
1289
  const pks = slots.map((slot) => slot.pk);
1092
- this.emitter.emit(0 /* UPDATE */, { pks });
1093
- this.updateEmitter.markUpdatedKeys(pks);
1290
+ this.notifyLegacyMany(pks);
1291
+ for (let i = 0; i < slots.length; i++) {
1292
+ this.updateEmitter.markUpdatedCarrier(slots[i]);
1293
+ }
1094
1294
  this.trackAnyUpdatePks(pks);
1095
1295
  return slots;
1096
1296
  }
1097
1297
  removeOne(entity) {
1098
1298
  const pk = this.selectPk(entity);
1099
1299
  this.store.removeOneByPk(pk);
1100
- this.emitter.emit(0 /* UPDATE */, { pks: [pk] });
1300
+ this.notifyLegacy(pk);
1101
1301
  this.updateEmitter.markUpdatedKey(pk);
1102
1302
  this.trackAnyUpdatePk(pk);
1103
1303
  }
@@ -1105,26 +1305,28 @@ var OIMReactiveCollection = class extends OIMCollection {
1105
1305
  if (entities.length === 0) return;
1106
1306
  const pks = entities.map(this.selectPk);
1107
1307
  this.store.removeManyByPks(pks);
1108
- this.emitter.emit(0 /* UPDATE */, { pks });
1308
+ this.notifyLegacyMany(pks);
1109
1309
  this.updateEmitter.markUpdatedKeys(pks);
1110
1310
  this.trackAnyUpdatePks(pks);
1111
1311
  }
1112
1312
  removeOneByPk(pk) {
1113
1313
  this.store.removeOneByPk(pk);
1114
- this.emitter.emit(0 /* UPDATE */, { pks: [pk] });
1314
+ this.notifyLegacy(pk);
1115
1315
  this.updateEmitter.markUpdatedKey(pk);
1116
1316
  this.trackAnyUpdatePk(pk);
1117
1317
  }
1118
1318
  removeManyByPks(pks) {
1119
1319
  if (pks.length === 0) return;
1120
1320
  this.store.removeManyByPks(pks);
1121
- this.emitter.emit(0 /* UPDATE */, { pks });
1321
+ this.notifyLegacyMany(pks);
1122
1322
  this.updateEmitter.markUpdatedKeys(pks);
1123
1323
  this.trackAnyUpdatePks(pks);
1124
1324
  }
1125
1325
  clear() {
1126
1326
  this.store.clear();
1127
- this.emitter.emit(0 /* UPDATE */, { pks: [] });
1327
+ if (this.emitter.hasHandlers(0 /* UPDATE */)) {
1328
+ this.emitter.emit(0 /* UPDATE */, { pks: [] });
1329
+ }
1128
1330
  this.updateEmitter.markAllUpdated();
1129
1331
  this.trackAnyUpdateClear();
1130
1332
  }
@@ -1507,6 +1709,21 @@ var OIMIndexManualArrayBased = class extends OIMIndexArrayBased {
1507
1709
  this.emitUpdateOne(key);
1508
1710
  }
1509
1711
  }
1712
+ /**
1713
+ * Appends pre-deduplicated slots to a key's bucket IN PLACE (no copy of the
1714
+ * existing bucket) and emits once. Callers own dedup (so this stays O(added),
1715
+ * not O(bucket)); used by collection-bound indexes for fast `addPks`.
1716
+ */
1717
+ appendSlots(key, slots) {
1718
+ if (slots.length === 0) return;
1719
+ let bucket = this.store.getOneByKey(key);
1720
+ if (!bucket) {
1721
+ bucket = [];
1722
+ this.store.setOneByKey(key, bucket);
1723
+ }
1724
+ for (let i = 0; i < slots.length; i++) bucket.push(slots[i]);
1725
+ this.emitUpdateOne(key);
1726
+ }
1510
1727
  /**
1511
1728
  * Clear all primary keys for a specific index key, or all keys if no key specified
1512
1729
  */
@@ -1551,6 +1768,9 @@ var OIMReactiveIndexManualArrayBased = class extends OIMReactiveIndexArrayBased
1551
1768
  setSlots(key, slots) {
1552
1769
  this.index.setSlots(key, slots);
1553
1770
  }
1771
+ appendSlots(key, slots) {
1772
+ this.index.appendSlots(key, slots);
1773
+ }
1554
1774
  clear(key) {
1555
1775
  this.index.clear(key);
1556
1776
  }
@@ -1563,24 +1783,27 @@ var OIMReactiveCollectionIndexManualArrayBased = class extends OIMReactiveIndexM
1563
1783
  super(queue, {
1564
1784
  indexOptions: opts.indexOptions
1565
1785
  });
1786
+ // Persistent per-key pk membership for O(1) `addPks` dedup (avoids rebuilding
1787
+ // a Set from the whole bucket on every call). Kept in sync by setSlots/clear.
1788
+ this.pksByKey = /* @__PURE__ */ new Map();
1566
1789
  this.resolveSlot = resolveSlot;
1567
1790
  }
1568
1791
  setPks(key, pks) {
1569
- this.setSlots(key, this.resolveSlots(pks));
1792
+ const pkSet = /* @__PURE__ */ new Set();
1793
+ for (let i = 0; i < pks.length; i++) pkSet.add(pks[i]);
1794
+ this.pksByKey.set(key, pkSet);
1795
+ super.setSlots(key, this.resolveSlots(pks));
1570
1796
  }
1571
1797
  addPks(key, pks) {
1572
1798
  if (pks.length === 0) return;
1573
- const existingSlots = this.index.getSlotsByKey(key);
1574
- const existingPks = new Set(existingSlots.map((slot) => slot.pk));
1575
- const nextSlots = existingSlots.slice();
1576
- let hasChanges = false;
1799
+ const pkSet = this.membershipOf(key);
1800
+ const newSlots = [];
1577
1801
  for (const pk of pks) {
1578
- if (existingPks.has(pk)) continue;
1579
- nextSlots.push(this.resolveRequiredSlot(pk));
1580
- existingPks.add(pk);
1581
- hasChanges = true;
1802
+ if (pkSet.has(pk)) continue;
1803
+ pkSet.add(pk);
1804
+ newSlots.push(this.resolveRequiredSlot(pk));
1582
1805
  }
1583
- if (hasChanges) this.setSlots(key, nextSlots);
1806
+ if (newSlots.length > 0) this.appendSlots(key, newSlots);
1584
1807
  }
1585
1808
  removePks(key, pks) {
1586
1809
  if (pks.length === 0) return;
@@ -1590,8 +1813,33 @@ var OIMReactiveCollectionIndexManualArrayBased = class extends OIMReactiveIndexM
1590
1813
  (slot) => !pksToRemove.has(slot.pk)
1591
1814
  );
1592
1815
  if (nextSlots.length === existingSlots.length) return;
1816
+ const pkSet = this.pksByKey.get(key);
1817
+ if (pkSet) {
1818
+ for (let i = 0; i < pks.length; i++) pkSet.delete(pks[i]);
1819
+ }
1593
1820
  if (nextSlots.length === 0) this.clear(key);
1594
- else this.setSlots(key, nextSlots);
1821
+ else super.setSlots(key, nextSlots);
1822
+ }
1823
+ clear(key) {
1824
+ if (key === void 0) this.pksByKey.clear();
1825
+ else this.pksByKey.delete(key);
1826
+ super.clear(key);
1827
+ }
1828
+ /**
1829
+ * The membership set for a key, lazily seeded from the current bucket so it
1830
+ * stays correct even if slots were set through a lower-level path.
1831
+ */
1832
+ membershipOf(key) {
1833
+ let pkSet = this.pksByKey.get(key);
1834
+ if (!pkSet) {
1835
+ pkSet = /* @__PURE__ */ new Set();
1836
+ const existing = this.index.getSlotsByKey(key);
1837
+ for (let i = 0; i < existing.length; i++) {
1838
+ pkSet.add(existing[i].pk);
1839
+ }
1840
+ this.pksByKey.set(key, pkSet);
1841
+ }
1842
+ return pkSet;
1595
1843
  }
1596
1844
  getEntitiesByKey(key) {
1597
1845
  return super.getEntitiesByKey(key);
@@ -1625,6 +1873,41 @@ var OIMIndexManualSetBased = class extends OIMIndexSetBased {
1625
1873
  this.emitUpdateOne(key);
1626
1874
  }
1627
1875
  }
1876
+ /**
1877
+ * Adds slots to a key's bucket IN PLACE (no copy of the existing Set) and
1878
+ * emits once. O(added). Used by collection-bound indexes for fast `addPks`.
1879
+ */
1880
+ addSlots(key, slots) {
1881
+ if (slots.length === 0) return;
1882
+ let bucket = this.store.getOneByKey(key);
1883
+ if (!bucket) {
1884
+ bucket = /* @__PURE__ */ new Set();
1885
+ this.store.setOneByKey(key, bucket);
1886
+ }
1887
+ let changed = false;
1888
+ for (let i = 0; i < slots.length; i++) {
1889
+ if (!bucket.has(slots[i])) {
1890
+ bucket.add(slots[i]);
1891
+ changed = true;
1892
+ }
1893
+ }
1894
+ if (changed) this.emitUpdateOne(key);
1895
+ }
1896
+ /**
1897
+ * Removes slots from a key's bucket IN PLACE (O(removed)) and emits once.
1898
+ */
1899
+ removeSlots(key, slots) {
1900
+ if (slots.length === 0) return;
1901
+ const bucket = this.store.getOneByKey(key);
1902
+ if (!bucket) return;
1903
+ let changed = false;
1904
+ for (let i = 0; i < slots.length; i++) {
1905
+ if (bucket.delete(slots[i])) changed = true;
1906
+ }
1907
+ if (!changed) return;
1908
+ if (bucket.size === 0) this.store.removeOneByKey(key);
1909
+ this.emitUpdateOne(key);
1910
+ }
1628
1911
  /**
1629
1912
  * Clear all primary keys for a specific index key, or all keys if no key specified
1630
1913
  */
@@ -1669,6 +1952,12 @@ var OIMReactiveIndexManualSetBased = class extends OIMReactiveIndexSetBased {
1669
1952
  setSlots(key, slots) {
1670
1953
  this.index.setSlots(key, slots);
1671
1954
  }
1955
+ addSlots(key, slots) {
1956
+ this.index.addSlots(key, slots);
1957
+ }
1958
+ removeSlots(key, slots) {
1959
+ this.index.removeSlots(key, slots);
1960
+ }
1672
1961
  clear(key) {
1673
1962
  this.index.clear(key);
1674
1963
  }
@@ -1681,42 +1970,69 @@ var OIMReactiveCollectionIndexManualSetBased = class extends OIMReactiveIndexMan
1681
1970
  super(queue, {
1682
1971
  indexOptions: opts.indexOptions
1683
1972
  });
1973
+ // Persistent per-key pk → slot map for O(1) add (dedup) and O(1) remove
1974
+ // (slot lookup by pk). Kept in sync by setPks/addPks/removePks/clear.
1975
+ this.slotByPk = /* @__PURE__ */ new Map();
1684
1976
  this.resolveSlot = resolveSlot;
1685
1977
  }
1686
1978
  setPks(key, pks) {
1687
- this.setSlots(key, this.resolveSlots(pks));
1979
+ const map = /* @__PURE__ */ new Map();
1980
+ const slots = [];
1981
+ for (let i = 0; i < pks.length; i++) {
1982
+ const pk = pks[i];
1983
+ const slot = this.resolveRequiredSlot(pk);
1984
+ if (!map.has(pk)) map.set(pk, slot);
1985
+ slots.push(slot);
1986
+ }
1987
+ this.slotByPk.set(key, map);
1988
+ super.setSlots(key, slots);
1688
1989
  }
1689
1990
  addPks(key, pks) {
1690
1991
  if (pks.length === 0) return;
1691
- const existingSlots = this.index.getSlotsByKey(key);
1692
- const existingPks = this.index.getPksByKey(key);
1693
- const nextSlots = new Set(existingSlots);
1694
- let hasChanges = false;
1992
+ const map = this.membershipOf(key);
1993
+ const newSlots = [];
1695
1994
  for (const pk of pks) {
1696
- if (existingPks.has(pk)) continue;
1995
+ if (map.has(pk)) continue;
1697
1996
  const slot = this.resolveRequiredSlot(pk);
1698
- nextSlots.add(slot);
1699
- existingPks.add(pk);
1700
- hasChanges = true;
1997
+ map.set(pk, slot);
1998
+ newSlots.push(slot);
1701
1999
  }
1702
- if (hasChanges) this.setSlots(key, nextSlots);
2000
+ if (newSlots.length > 0) this.addSlots(key, newSlots);
1703
2001
  }
1704
2002
  removePks(key, pks) {
1705
2003
  if (pks.length === 0) return;
1706
- const pksToRemove = new Set(pks);
1707
- const existingSlots = this.index.getSlotsByKey(key);
1708
- const nextSlots = /* @__PURE__ */ new Set();
1709
- let hasChanges = false;
1710
- for (const slot of existingSlots) {
1711
- if (pksToRemove.has(slot.pk)) {
1712
- hasChanges = true;
1713
- } else {
1714
- nextSlots.add(slot);
2004
+ const map = this.membershipOf(key);
2005
+ if (map.size === 0) return;
2006
+ const removed = [];
2007
+ for (const pk of pks) {
2008
+ const slot = map.get(pk);
2009
+ if (slot) {
2010
+ map.delete(pk);
2011
+ removed.push(slot);
1715
2012
  }
1716
2013
  }
1717
- if (!hasChanges) return;
1718
- if (nextSlots.size === 0) this.clear(key);
1719
- else this.setSlots(key, nextSlots);
2014
+ if (removed.length === 0) return;
2015
+ if (map.size === 0) this.slotByPk.delete(key);
2016
+ this.removeSlots(key, removed);
2017
+ }
2018
+ clear(key) {
2019
+ if (key === void 0) this.slotByPk.clear();
2020
+ else this.slotByPk.delete(key);
2021
+ super.clear(key);
2022
+ }
2023
+ /**
2024
+ * The pk→slot map for a key, lazily seeded from the current bucket so it
2025
+ * stays correct even if slots were set through a lower-level path.
2026
+ */
2027
+ membershipOf(key) {
2028
+ let map = this.slotByPk.get(key);
2029
+ if (!map) {
2030
+ map = /* @__PURE__ */ new Map();
2031
+ const existing = this.index.getSlotsByKey(key);
2032
+ for (const slot of existing) map.set(slot.pk, slot);
2033
+ this.slotByPk.set(key, map);
2034
+ }
2035
+ return map;
1720
2036
  }
1721
2037
  getEntitiesByKey(key) {
1722
2038
  return super.getEntitiesByKey(key);
@@ -1724,11 +2040,6 @@ var OIMReactiveCollectionIndexManualSetBased = class extends OIMReactiveIndexMan
1724
2040
  getEntitiesByKeys(keys) {
1725
2041
  return super.getEntitiesByKeys(keys);
1726
2042
  }
1727
- resolveSlots(pks) {
1728
- const slots = /* @__PURE__ */ new Set();
1729
- for (const pk of pks) slots.add(this.resolveRequiredSlot(pk));
1730
- return slots;
1731
- }
1732
2043
  resolveRequiredSlot(pk) {
1733
2044
  const slot = this.resolveSlot(pk);
1734
2045
  if (!slot) return { pk, item: void 0 };
@@ -1846,17 +2157,56 @@ var OIMDerivedCollectionIndexArrayBased = class extends OIMReactiveCollectionInd
1846
2157
  indexOptions: opts.indexOptions
1847
2158
  });
1848
2159
  this.keysByPk = /* @__PURE__ */ new Map();
2160
+ // Last known sort value per pk (only when `orderBy` is used), so an update
2161
+ // touching an unrelated field can skip re-sorting when the value is unchanged.
2162
+ this.orderValueByPk = /* @__PURE__ */ new Map();
1849
2163
  this.onCollectionUpdate = (pks) => {
1850
2164
  if (pks.length === 0) {
1851
2165
  this.rebuildFromCollection();
1852
2166
  return;
1853
2167
  }
1854
- const affectedKeys = /* @__PURE__ */ new Set();
2168
+ const ordered = !!(this.compareEntities || this.orderBy);
2169
+ const keysToSort = ordered ? /* @__PURE__ */ new Set() : null;
1855
2170
  for (const pk of pks) {
1856
- this.trackPkKeys(pk, affectedKeys);
2171
+ const prevKeys = this.keysByPk.get(pk) ?? /* @__PURE__ */ new Set();
2172
+ const slot = this.collection.getSlotByPk(pk);
2173
+ const entity = slot?.item;
2174
+ const nextKeys = entity === void 0 ? /* @__PURE__ */ new Set() : new Set(this.normalizeKeys(this.selectIndexKeys(entity)));
2175
+ let orderChanged = false;
2176
+ if (this.orderBy) {
2177
+ if (entity === void 0 || nextKeys.size === 0) {
2178
+ this.orderValueByPk.delete(pk);
2179
+ } else {
2180
+ const value = this.orderBy(entity);
2181
+ orderChanged = !this.orderValueByPk.has(pk) || this.orderValueByPk.get(pk) !== value;
2182
+ this.orderValueByPk.set(pk, value);
2183
+ }
2184
+ } else if (this.compareEntities && entity !== void 0) {
2185
+ orderChanged = true;
2186
+ }
2187
+ for (const key of prevKeys) {
2188
+ if (!nextKeys.has(key)) this.removePks(key, [pk]);
2189
+ }
2190
+ if (entity !== void 0) {
2191
+ for (const key of nextKeys) {
2192
+ if (!prevKeys.has(key)) {
2193
+ this.addPks(key, [pk]);
2194
+ keysToSort?.add(key);
2195
+ } else if (orderChanged) {
2196
+ keysToSort?.add(key);
2197
+ }
2198
+ }
2199
+ }
2200
+ if (nextKeys.size === 0) this.keysByPk.delete(pk);
2201
+ else this.keysByPk.set(pk, nextKeys);
1857
2202
  }
1858
- for (const key of affectedKeys) {
1859
- this.rebuildKey(key);
2203
+ if (keysToSort) {
2204
+ for (const key of keysToSort) {
2205
+ this.setSlots(
2206
+ key,
2207
+ this.sortSlots(this.index.getSlotsByKey(key))
2208
+ );
2209
+ }
1860
2210
  }
1861
2211
  };
1862
2212
  this.collection = collection;
@@ -1873,6 +2223,7 @@ var OIMDerivedCollectionIndexArrayBased = class extends OIMReactiveCollectionInd
1873
2223
  }
1874
2224
  rebuildFromCollection() {
1875
2225
  this.keysByPk.clear();
2226
+ this.orderValueByPk.clear();
1876
2227
  super.clear();
1877
2228
  const slotsByKey = /* @__PURE__ */ new Map();
1878
2229
  const slots = this.collection.getAllSlots();
@@ -1881,6 +2232,9 @@ var OIMDerivedCollectionIndexArrayBased = class extends OIMReactiveCollectionInd
1881
2232
  const keys = this.normalizeKeys(this.selectIndexKeys(slot.item));
1882
2233
  if (keys.length === 0) continue;
1883
2234
  this.keysByPk.set(slot.pk, new Set(keys));
2235
+ if (this.orderBy) {
2236
+ this.orderValueByPk.set(slot.pk, this.orderBy(slot.item));
2237
+ }
1884
2238
  for (const key of keys) {
1885
2239
  let slotsForKey = slotsByKey.get(key);
1886
2240
  if (!slotsForKey) {
@@ -1897,6 +2251,7 @@ var OIMDerivedCollectionIndexArrayBased = class extends OIMReactiveCollectionInd
1897
2251
  clear(key) {
1898
2252
  if (key === void 0) {
1899
2253
  this.keysByPk.clear();
2254
+ this.orderValueByPk.clear();
1900
2255
  } else {
1901
2256
  for (const keys of this.keysByPk.values()) {
1902
2257
  keys.delete(key);
@@ -1907,37 +2262,11 @@ var OIMDerivedCollectionIndexArrayBased = class extends OIMReactiveCollectionInd
1907
2262
  destroy() {
1908
2263
  this.unsubscribeFromCollection();
1909
2264
  this.keysByPk.clear();
2265
+ this.orderValueByPk.clear();
1910
2266
  super.destroy();
1911
2267
  }
1912
- trackPkKeys(pk, affectedKeys) {
1913
- const prevKeys = this.keysByPk.get(pk) ?? /* @__PURE__ */ new Set();
1914
- for (const key of prevKeys) affectedKeys.add(key);
1915
- const slot = this.collection.getSlotByPk(pk);
1916
- const entity = slot?.item;
1917
- const nextKeys = entity === void 0 ? /* @__PURE__ */ new Set() : new Set(this.normalizeKeys(this.selectIndexKeys(entity)));
1918
- for (const key of nextKeys) affectedKeys.add(key);
1919
- if (nextKeys.size === 0) {
1920
- this.keysByPk.delete(pk);
1921
- } else {
1922
- this.keysByPk.set(pk, nextKeys);
1923
- }
1924
- }
1925
- rebuildKey(key) {
1926
- const keySlots = [];
1927
- const slots = this.collection.getAllSlots();
1928
- for (const slot of slots) {
1929
- if (slot.item === void 0) continue;
1930
- const keys = this.keysByPk.get(slot.pk);
1931
- if (keys?.has(key)) keySlots.push(slot);
1932
- }
1933
- if (keySlots.length === 0) {
1934
- super.clear(key);
1935
- } else {
1936
- this.setSlots(key, this.sortSlots(keySlots));
1937
- }
1938
- }
1939
2268
  sortSlots(slots) {
1940
- if (!this.compareEntities && !this.orderBy) return slots;
2269
+ if (!this.compareEntities && !this.orderBy) return slots.slice();
1941
2270
  return slots.slice().sort((a, b) => {
1942
2271
  const aItem = a.item;
1943
2272
  const bItem = b.item;
@@ -2220,6 +2549,11 @@ var OIMComparatorFactory = class {
2220
2549
  }
2221
2550
  };
2222
2551
 
2552
+ // src/core/createInPlaceEntityUpdater.ts
2553
+ function createInPlaceEntityUpdater() {
2554
+ return (draft, prev) => Object.assign(prev, draft);
2555
+ }
2556
+
2223
2557
  // src/core/OIMEventQueue.ts
2224
2558
  var OIMEventQueue = class {
2225
2559
  constructor(options = {}) {
@@ -3654,6 +3988,7 @@ export {
3654
3988
  EOIMEventQueueSchedulerEventType,
3655
3989
  EOIMIndexEventType,
3656
3990
  EOIMObjectEventType,
3991
+ OIMCarrierKeyedEmitter,
3657
3992
  OIMCollection,
3658
3993
  OIMCollectionByPkSelector,
3659
3994
  OIMCollectionByPksSelector,
@@ -3721,6 +4056,7 @@ export {
3721
4056
  OIMReactiveIndexSetBased,
3722
4057
  OIMReactiveObject,
3723
4058
  OIMSelector,
4059
+ createInPlaceEntityUpdater,
3724
4060
  createOIMCollectionIndexFactory,
3725
4061
  createOIMCollectionKit
3726
4062
  };