@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.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
  }
@@ -779,17 +788,39 @@ var OIMCollectionStoreMapDriven = class extends OIMCollectionStore {
779
788
  constructor() {
780
789
  super(...arguments);
781
790
  this.slots = /* @__PURE__ */ new Map();
791
+ // Slots reserved by indexes for PKs whose entity has not arrived yet.
792
+ // They carry `item: undefined` and live apart from `slots` so they do not
793
+ // leak into entity enumeration (getAll/getAllPks/countAll). When the entity
794
+ // is finally written, the reserved slot is promoted into `slots` — keeping
795
+ // the reference that indexes already hold, so it fills in live.
796
+ this.reservedSlots = /* @__PURE__ */ new Map();
782
797
  }
783
798
  setOneByPk(pk, entity) {
784
799
  const slot = this.slots.get(pk);
785
800
  if (slot) {
786
801
  slot.item = entity;
787
802
  return slot;
788
- } else {
789
- const nextSlot = { pk, item: entity };
790
- this.slots.set(pk, nextSlot);
791
- return nextSlot;
792
803
  }
804
+ const reserved = this.reservedSlots.get(pk);
805
+ if (reserved) {
806
+ reserved.item = entity;
807
+ this.reservedSlots.delete(pk);
808
+ this.slots.set(pk, reserved);
809
+ return reserved;
810
+ }
811
+ const nextSlot = { pk, item: entity };
812
+ this.slots.set(pk, nextSlot);
813
+ return nextSlot;
814
+ }
815
+ getOrReserveSlotByPk(pk) {
816
+ const slot = this.slots.get(pk);
817
+ if (slot) return slot;
818
+ let reserved = this.reservedSlots.get(pk);
819
+ if (!reserved) {
820
+ reserved = { pk, item: void 0 };
821
+ this.reservedSlots.set(pk, reserved);
822
+ }
823
+ return reserved;
793
824
  }
794
825
  setManyByPks(pks, entities) {
795
826
  const slots = [];
@@ -802,6 +833,12 @@ var OIMCollectionStoreMapDriven = class extends OIMCollectionStore {
802
833
  getSlotByPk(pk) {
803
834
  return this.slots.get(pk);
804
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
+ }
805
842
  getSlotsByPks(pks) {
806
843
  const result = [];
807
844
  result.length = pks.length;
@@ -820,8 +857,21 @@ var OIMCollectionStoreMapDriven = class extends OIMCollectionStore {
820
857
  }
821
858
  removeOneByPk(pk) {
822
859
  const slot = this.slots.get(pk);
823
- if (slot) slot.item = void 0;
824
- 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
+ }
868
+ const reserved = this.reservedSlots.get(pk);
869
+ if (reserved) {
870
+ reserved.item = void 0;
871
+ if (!this.hasSubscribers(reserved)) {
872
+ this.reservedSlots.delete(pk);
873
+ }
874
+ }
825
875
  }
826
876
  removeManyByPks(pks) {
827
877
  for (const pk of pks) {
@@ -855,16 +905,31 @@ var OIMCollectionStoreMapDriven = class extends OIMCollectionStore {
855
905
  return this.slots.size;
856
906
  }
857
907
  clear() {
908
+ const retained = [];
858
909
  for (const slot of this.slots.values()) {
859
910
  slot.item = void 0;
911
+ if (this.hasSubscribers(slot)) retained.push(slot);
860
912
  }
861
913
  this.slots.clear();
914
+ for (const slot of this.reservedSlots.values()) {
915
+ slot.item = void 0;
916
+ if (this.hasSubscribers(slot)) retained.push(slot);
917
+ }
918
+ this.reservedSlots.clear();
919
+ for (const slot of retained) {
920
+ this.reservedSlots.set(slot.pk, slot);
921
+ }
862
922
  }
863
923
  getAllPks() {
864
924
  return Array.from(this.slots.keys());
865
925
  }
866
926
  destroy() {
867
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();
868
933
  }
869
934
  };
870
935
 
@@ -900,6 +965,9 @@ var OIMCollection = class {
900
965
  getSlotByPk(pk) {
901
966
  return this.store.getSlotByPk(pk);
902
967
  }
968
+ getOrReserveSlotByPk(pk) {
969
+ return this.store.getOrReserveSlotByPk(pk);
970
+ }
903
971
  getSlotsByPks(pks) {
904
972
  return this.store.getSlotsByPks(pks);
905
973
  }
@@ -984,6 +1052,153 @@ var OIMCollection = class {
984
1052
  }
985
1053
  };
986
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
+
987
1202
  // src/core/OIMReactiveCollection.ts
988
1203
  var OIMReactiveCollection = class extends OIMCollection {
989
1204
  constructor(queue, opts) {
@@ -1003,7 +1218,10 @@ var OIMReactiveCollection = class extends OIMCollection {
1003
1218
  for (let i = 0; i < snapshot.length; i++) snapshot[i](pks);
1004
1219
  };
1005
1220
  this.queue = queue;
1006
- 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
+ });
1007
1225
  }
1008
1226
  subscribeOnKey(pk, handler) {
1009
1227
  return this.updateEmitter.subscribeOnKey(pk, handler);
@@ -1035,17 +1253,31 @@ var OIMReactiveCollection = class extends OIMCollection {
1035
1253
  this.anyUpdateHandlers.delete(handler);
1036
1254
  };
1037
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
+ }
1038
1270
  upsertOneByPk(pk, entity) {
1039
1271
  const slot = this.upsertOneWithoutNotificationsByPk(pk, entity);
1040
- this.emitter.emit(0 /* UPDATE */, { pks: [pk] });
1041
- this.updateEmitter.markUpdatedKey(pk);
1272
+ this.notifyLegacy(pk);
1273
+ this.updateEmitter.markUpdatedCarrier(slot);
1042
1274
  this.trackAnyUpdatePk(pk);
1043
1275
  return slot;
1044
1276
  }
1045
1277
  upsertOne(entity) {
1046
1278
  const slot = this.upsertOneWithoutNotifications(entity);
1047
- this.emitter.emit(0 /* UPDATE */, { pks: [slot.pk] });
1048
- this.updateEmitter.markUpdatedKey(slot.pk);
1279
+ this.notifyLegacy(slot.pk);
1280
+ this.updateEmitter.markUpdatedCarrier(slot);
1049
1281
  this.trackAnyUpdatePk(slot.pk);
1050
1282
  return slot;
1051
1283
  }
@@ -1055,15 +1287,17 @@ var OIMReactiveCollection = class extends OIMCollection {
1055
1287
  (entity) => this.upsertOneWithoutNotifications(entity)
1056
1288
  );
1057
1289
  const pks = slots.map((slot) => slot.pk);
1058
- this.emitter.emit(0 /* UPDATE */, { pks });
1059
- this.updateEmitter.markUpdatedKeys(pks);
1290
+ this.notifyLegacyMany(pks);
1291
+ for (let i = 0; i < slots.length; i++) {
1292
+ this.updateEmitter.markUpdatedCarrier(slots[i]);
1293
+ }
1060
1294
  this.trackAnyUpdatePks(pks);
1061
1295
  return slots;
1062
1296
  }
1063
1297
  removeOne(entity) {
1064
1298
  const pk = this.selectPk(entity);
1065
1299
  this.store.removeOneByPk(pk);
1066
- this.emitter.emit(0 /* UPDATE */, { pks: [pk] });
1300
+ this.notifyLegacy(pk);
1067
1301
  this.updateEmitter.markUpdatedKey(pk);
1068
1302
  this.trackAnyUpdatePk(pk);
1069
1303
  }
@@ -1071,26 +1305,28 @@ var OIMReactiveCollection = class extends OIMCollection {
1071
1305
  if (entities.length === 0) return;
1072
1306
  const pks = entities.map(this.selectPk);
1073
1307
  this.store.removeManyByPks(pks);
1074
- this.emitter.emit(0 /* UPDATE */, { pks });
1308
+ this.notifyLegacyMany(pks);
1075
1309
  this.updateEmitter.markUpdatedKeys(pks);
1076
1310
  this.trackAnyUpdatePks(pks);
1077
1311
  }
1078
1312
  removeOneByPk(pk) {
1079
1313
  this.store.removeOneByPk(pk);
1080
- this.emitter.emit(0 /* UPDATE */, { pks: [pk] });
1314
+ this.notifyLegacy(pk);
1081
1315
  this.updateEmitter.markUpdatedKey(pk);
1082
1316
  this.trackAnyUpdatePk(pk);
1083
1317
  }
1084
1318
  removeManyByPks(pks) {
1085
1319
  if (pks.length === 0) return;
1086
1320
  this.store.removeManyByPks(pks);
1087
- this.emitter.emit(0 /* UPDATE */, { pks });
1321
+ this.notifyLegacyMany(pks);
1088
1322
  this.updateEmitter.markUpdatedKeys(pks);
1089
1323
  this.trackAnyUpdatePks(pks);
1090
1324
  }
1091
1325
  clear() {
1092
1326
  this.store.clear();
1093
- this.emitter.emit(0 /* UPDATE */, { pks: [] });
1327
+ if (this.emitter.hasHandlers(0 /* UPDATE */)) {
1328
+ this.emitter.emit(0 /* UPDATE */, { pks: [] });
1329
+ }
1094
1330
  this.updateEmitter.markAllUpdated();
1095
1331
  this.trackAnyUpdateClear();
1096
1332
  }
@@ -1473,6 +1709,21 @@ var OIMIndexManualArrayBased = class extends OIMIndexArrayBased {
1473
1709
  this.emitUpdateOne(key);
1474
1710
  }
1475
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
+ }
1476
1727
  /**
1477
1728
  * Clear all primary keys for a specific index key, or all keys if no key specified
1478
1729
  */
@@ -1517,6 +1768,9 @@ var OIMReactiveIndexManualArrayBased = class extends OIMReactiveIndexArrayBased
1517
1768
  setSlots(key, slots) {
1518
1769
  this.index.setSlots(key, slots);
1519
1770
  }
1771
+ appendSlots(key, slots) {
1772
+ this.index.appendSlots(key, slots);
1773
+ }
1520
1774
  clear(key) {
1521
1775
  this.index.clear(key);
1522
1776
  }
@@ -1525,28 +1779,31 @@ var OIMReactiveIndexManualArrayBased = class extends OIMReactiveIndexArrayBased
1525
1779
  // src/core/OIMReactiveCollectionIndexManualArrayBased.ts
1526
1780
  var OIMReactiveCollectionIndexManualArrayBased = class extends OIMReactiveIndexManualArrayBased {
1527
1781
  constructor(queue, opts) {
1528
- const resolveSlot = opts.collection !== void 0 ? (pk) => opts.collection.getSlotByPk(pk) : opts.resolveSlot;
1782
+ const resolveSlot = opts.collection !== void 0 ? (pk) => opts.collection.getOrReserveSlotByPk(pk) : opts.resolveSlot;
1529
1783
  super(queue, {
1530
1784
  indexOptions: opts.indexOptions
1531
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();
1532
1789
  this.resolveSlot = resolveSlot;
1533
1790
  }
1534
1791
  setPks(key, pks) {
1535
- 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));
1536
1796
  }
1537
1797
  addPks(key, pks) {
1538
1798
  if (pks.length === 0) return;
1539
- const existingSlots = this.index.getSlotsByKey(key);
1540
- const existingPks = new Set(existingSlots.map((slot) => slot.pk));
1541
- const nextSlots = existingSlots.slice();
1542
- let hasChanges = false;
1799
+ const pkSet = this.membershipOf(key);
1800
+ const newSlots = [];
1543
1801
  for (const pk of pks) {
1544
- if (existingPks.has(pk)) continue;
1545
- nextSlots.push(this.resolveRequiredSlot(pk));
1546
- existingPks.add(pk);
1547
- hasChanges = true;
1802
+ if (pkSet.has(pk)) continue;
1803
+ pkSet.add(pk);
1804
+ newSlots.push(this.resolveRequiredSlot(pk));
1548
1805
  }
1549
- if (hasChanges) this.setSlots(key, nextSlots);
1806
+ if (newSlots.length > 0) this.appendSlots(key, newSlots);
1550
1807
  }
1551
1808
  removePks(key, pks) {
1552
1809
  if (pks.length === 0) return;
@@ -1556,8 +1813,33 @@ var OIMReactiveCollectionIndexManualArrayBased = class extends OIMReactiveIndexM
1556
1813
  (slot) => !pksToRemove.has(slot.pk)
1557
1814
  );
1558
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
+ }
1559
1820
  if (nextSlots.length === 0) this.clear(key);
1560
- 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;
1561
1843
  }
1562
1844
  getEntitiesByKey(key) {
1563
1845
  return super.getEntitiesByKey(key);
@@ -1575,11 +1857,7 @@ var OIMReactiveCollectionIndexManualArrayBased = class extends OIMReactiveIndexM
1575
1857
  }
1576
1858
  resolveRequiredSlot(pk) {
1577
1859
  const slot = this.resolveSlot(pk);
1578
- if (!slot) {
1579
- throw new Error(
1580
- `[OIMReactiveCollectionIndexManualArrayBased]: Unable to resolve slot for PK "${String(pk)}".`
1581
- );
1582
- }
1860
+ if (!slot) return { pk, item: void 0 };
1583
1861
  return slot;
1584
1862
  }
1585
1863
  };
@@ -1595,6 +1873,41 @@ var OIMIndexManualSetBased = class extends OIMIndexSetBased {
1595
1873
  this.emitUpdateOne(key);
1596
1874
  }
1597
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
+ }
1598
1911
  /**
1599
1912
  * Clear all primary keys for a specific index key, or all keys if no key specified
1600
1913
  */
@@ -1639,6 +1952,12 @@ var OIMReactiveIndexManualSetBased = class extends OIMReactiveIndexSetBased {
1639
1952
  setSlots(key, slots) {
1640
1953
  this.index.setSlots(key, slots);
1641
1954
  }
1955
+ addSlots(key, slots) {
1956
+ this.index.addSlots(key, slots);
1957
+ }
1958
+ removeSlots(key, slots) {
1959
+ this.index.removeSlots(key, slots);
1960
+ }
1642
1961
  clear(key) {
1643
1962
  this.index.clear(key);
1644
1963
  }
@@ -1647,46 +1966,73 @@ var OIMReactiveIndexManualSetBased = class extends OIMReactiveIndexSetBased {
1647
1966
  // src/core/OIMReactiveCollectionIndexManualSetBased.ts
1648
1967
  var OIMReactiveCollectionIndexManualSetBased = class extends OIMReactiveIndexManualSetBased {
1649
1968
  constructor(queue, opts) {
1650
- const resolveSlot = opts.collection !== void 0 ? (pk) => opts.collection.getSlotByPk(pk) : opts.resolveSlot;
1969
+ const resolveSlot = opts.collection !== void 0 ? (pk) => opts.collection.getOrReserveSlotByPk(pk) : opts.resolveSlot;
1651
1970
  super(queue, {
1652
1971
  indexOptions: opts.indexOptions
1653
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();
1654
1976
  this.resolveSlot = resolveSlot;
1655
1977
  }
1656
1978
  setPks(key, pks) {
1657
- 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);
1658
1989
  }
1659
1990
  addPks(key, pks) {
1660
1991
  if (pks.length === 0) return;
1661
- const existingSlots = this.index.getSlotsByKey(key);
1662
- const existingPks = this.index.getPksByKey(key);
1663
- const nextSlots = new Set(existingSlots);
1664
- let hasChanges = false;
1992
+ const map = this.membershipOf(key);
1993
+ const newSlots = [];
1665
1994
  for (const pk of pks) {
1666
- if (existingPks.has(pk)) continue;
1995
+ if (map.has(pk)) continue;
1667
1996
  const slot = this.resolveRequiredSlot(pk);
1668
- nextSlots.add(slot);
1669
- existingPks.add(pk);
1670
- hasChanges = true;
1997
+ map.set(pk, slot);
1998
+ newSlots.push(slot);
1671
1999
  }
1672
- if (hasChanges) this.setSlots(key, nextSlots);
2000
+ if (newSlots.length > 0) this.addSlots(key, newSlots);
1673
2001
  }
1674
2002
  removePks(key, pks) {
1675
2003
  if (pks.length === 0) return;
1676
- const pksToRemove = new Set(pks);
1677
- const existingSlots = this.index.getSlotsByKey(key);
1678
- const nextSlots = /* @__PURE__ */ new Set();
1679
- let hasChanges = false;
1680
- for (const slot of existingSlots) {
1681
- if (pksToRemove.has(slot.pk)) {
1682
- hasChanges = true;
1683
- } else {
1684
- 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);
1685
2012
  }
1686
2013
  }
1687
- if (!hasChanges) return;
1688
- if (nextSlots.size === 0) this.clear(key);
1689
- 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;
1690
2036
  }
1691
2037
  getEntitiesByKey(key) {
1692
2038
  return super.getEntitiesByKey(key);
@@ -1694,18 +2040,9 @@ var OIMReactiveCollectionIndexManualSetBased = class extends OIMReactiveIndexMan
1694
2040
  getEntitiesByKeys(keys) {
1695
2041
  return super.getEntitiesByKeys(keys);
1696
2042
  }
1697
- resolveSlots(pks) {
1698
- const slots = /* @__PURE__ */ new Set();
1699
- for (const pk of pks) slots.add(this.resolveRequiredSlot(pk));
1700
- return slots;
1701
- }
1702
2043
  resolveRequiredSlot(pk) {
1703
2044
  const slot = this.resolveSlot(pk);
1704
- if (!slot) {
1705
- throw new Error(
1706
- `[OIMReactiveCollectionIndexManualSetBased]: Unable to resolve slot for PK "${String(pk)}".`
1707
- );
1708
- }
2045
+ if (!slot) return { pk, item: void 0 };
1709
2046
  return slot;
1710
2047
  }
1711
2048
  };
@@ -1820,17 +2157,56 @@ var OIMDerivedCollectionIndexArrayBased = class extends OIMReactiveCollectionInd
1820
2157
  indexOptions: opts.indexOptions
1821
2158
  });
1822
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();
1823
2163
  this.onCollectionUpdate = (pks) => {
1824
2164
  if (pks.length === 0) {
1825
2165
  this.rebuildFromCollection();
1826
2166
  return;
1827
2167
  }
1828
- const affectedKeys = /* @__PURE__ */ new Set();
2168
+ const ordered = !!(this.compareEntities || this.orderBy);
2169
+ const keysToSort = ordered ? /* @__PURE__ */ new Set() : null;
1829
2170
  for (const pk of pks) {
1830
- 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);
1831
2202
  }
1832
- for (const key of affectedKeys) {
1833
- 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
+ }
1834
2210
  }
1835
2211
  };
1836
2212
  this.collection = collection;
@@ -1847,6 +2223,7 @@ var OIMDerivedCollectionIndexArrayBased = class extends OIMReactiveCollectionInd
1847
2223
  }
1848
2224
  rebuildFromCollection() {
1849
2225
  this.keysByPk.clear();
2226
+ this.orderValueByPk.clear();
1850
2227
  super.clear();
1851
2228
  const slotsByKey = /* @__PURE__ */ new Map();
1852
2229
  const slots = this.collection.getAllSlots();
@@ -1855,6 +2232,9 @@ var OIMDerivedCollectionIndexArrayBased = class extends OIMReactiveCollectionInd
1855
2232
  const keys = this.normalizeKeys(this.selectIndexKeys(slot.item));
1856
2233
  if (keys.length === 0) continue;
1857
2234
  this.keysByPk.set(slot.pk, new Set(keys));
2235
+ if (this.orderBy) {
2236
+ this.orderValueByPk.set(slot.pk, this.orderBy(slot.item));
2237
+ }
1858
2238
  for (const key of keys) {
1859
2239
  let slotsForKey = slotsByKey.get(key);
1860
2240
  if (!slotsForKey) {
@@ -1871,6 +2251,7 @@ var OIMDerivedCollectionIndexArrayBased = class extends OIMReactiveCollectionInd
1871
2251
  clear(key) {
1872
2252
  if (key === void 0) {
1873
2253
  this.keysByPk.clear();
2254
+ this.orderValueByPk.clear();
1874
2255
  } else {
1875
2256
  for (const keys of this.keysByPk.values()) {
1876
2257
  keys.delete(key);
@@ -1881,37 +2262,11 @@ var OIMDerivedCollectionIndexArrayBased = class extends OIMReactiveCollectionInd
1881
2262
  destroy() {
1882
2263
  this.unsubscribeFromCollection();
1883
2264
  this.keysByPk.clear();
2265
+ this.orderValueByPk.clear();
1884
2266
  super.destroy();
1885
2267
  }
1886
- trackPkKeys(pk, affectedKeys) {
1887
- const prevKeys = this.keysByPk.get(pk) ?? /* @__PURE__ */ new Set();
1888
- for (const key of prevKeys) affectedKeys.add(key);
1889
- const slot = this.collection.getSlotByPk(pk);
1890
- const entity = slot?.item;
1891
- const nextKeys = entity === void 0 ? /* @__PURE__ */ new Set() : new Set(this.normalizeKeys(this.selectIndexKeys(entity)));
1892
- for (const key of nextKeys) affectedKeys.add(key);
1893
- if (nextKeys.size === 0) {
1894
- this.keysByPk.delete(pk);
1895
- } else {
1896
- this.keysByPk.set(pk, nextKeys);
1897
- }
1898
- }
1899
- rebuildKey(key) {
1900
- const keySlots = [];
1901
- const slots = this.collection.getAllSlots();
1902
- for (const slot of slots) {
1903
- if (slot.item === void 0) continue;
1904
- const keys = this.keysByPk.get(slot.pk);
1905
- if (keys?.has(key)) keySlots.push(slot);
1906
- }
1907
- if (keySlots.length === 0) {
1908
- super.clear(key);
1909
- } else {
1910
- this.setSlots(key, this.sortSlots(keySlots));
1911
- }
1912
- }
1913
2268
  sortSlots(slots) {
1914
- if (!this.compareEntities && !this.orderBy) return slots;
2269
+ if (!this.compareEntities && !this.orderBy) return slots.slice();
1915
2270
  return slots.slice().sort((a, b) => {
1916
2271
  const aItem = a.item;
1917
2272
  const bItem = b.item;
@@ -2194,6 +2549,11 @@ var OIMComparatorFactory = class {
2194
2549
  }
2195
2550
  };
2196
2551
 
2552
+ // src/core/createInPlaceEntityUpdater.ts
2553
+ function createInPlaceEntityUpdater() {
2554
+ return (draft, prev) => Object.assign(prev, draft);
2555
+ }
2556
+
2197
2557
  // src/core/OIMEventQueue.ts
2198
2558
  var OIMEventQueue = class {
2199
2559
  constructor(options = {}) {
@@ -3628,6 +3988,7 @@ export {
3628
3988
  EOIMEventQueueSchedulerEventType,
3629
3989
  EOIMIndexEventType,
3630
3990
  EOIMObjectEventType,
3991
+ OIMCarrierKeyedEmitter,
3631
3992
  OIMCollection,
3632
3993
  OIMCollectionByPkSelector,
3633
3994
  OIMCollectionByPksSelector,
@@ -3695,6 +4056,7 @@ export {
3695
4056
  OIMReactiveIndexSetBased,
3696
4057
  OIMReactiveObject,
3697
4058
  OIMSelector,
4059
+ createInPlaceEntityUpdater,
3698
4060
  createOIMCollectionIndexFactory,
3699
4061
  createOIMCollectionKit
3700
4062
  };