@oimdb/core 3.2.0 → 3.4.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/README.md CHANGED
@@ -318,16 +318,20 @@ cardsByDeck.commandsEventEmitter.subscribeOnKey('deck1', () => {
318
318
  for (const command of commands) {
319
319
  switch (command.type) {
320
320
  case 'insert':
321
- // Insert command.pk / command.slot.item at command.index
321
+ // Insert command.item (the slot; entity is command.item.item)
322
+ // at command.index
322
323
  break;
323
324
  case 'remove':
324
- // Remove command.pk from command.index
325
+ // Remove command.count ?? 1 elements starting at command.index
325
326
  break;
326
327
  case 'move':
327
- // Move command.pk from command.fromIndex to command.toIndex
328
+ // Move command.count ?? 1 elements from command.from to command.to
328
329
  break;
329
330
  case 'set':
330
- // Replace the whole list with command.pks or command.slots
331
+ // Replace the single element at command.index with command.item
332
+ break;
333
+ case 'reset':
334
+ // Replace the whole list with command.items (slots)
331
335
  break;
332
336
  }
333
337
  }
@@ -546,6 +550,31 @@ users.upsertOne({ id: 'user1', name: 'John' });
546
550
  users.upsertOne({ id: 'user1', email: 'john@example.com' }); // Merges with existing
547
551
  ```
548
552
 
553
+ #### Built-in updaters
554
+
555
+ Two updater strategies ship as factories — pass the result as `updateEntity`:
556
+
557
+ ```typescript
558
+ import { createMergeEntityUpdater, createInPlaceEntityUpdater } from '@oimdb/core';
559
+
560
+ // Default: immutable shallow merge — `{ ...prev, ...draft }`. Each update produces
561
+ // a NEW entity object (required for React's Object.is / useSyncExternalStore).
562
+ const a = new OIMReactiveCollection<User, string>(queue, {
563
+ selectPk: (u) => u.id,
564
+ updateEntity: createMergeEntityUpdater(),
565
+ });
566
+
567
+ // In-place: `Object.assign(prev, draft)` — mutates the existing object, no
568
+ // per-update allocation. The entity reference is STABLE across changes, so it
569
+ // only works with subscription-driven readers (e.g. the `*Signal` hooks in
570
+ // @oimdb/react), not with Object.is/React.memo diffing. Fastest on the data
571
+ // layer for update-heavy workloads.
572
+ const b = new OIMReactiveCollection<User, string>(queue, {
573
+ selectPk: (u) => u.id,
574
+ updateEntity: createInPlaceEntityUpdater(),
575
+ });
576
+ ```
577
+
549
578
  ### Event Coalescing and Update Subscriptions
550
579
 
551
580
  ```typescript
@@ -1509,7 +1538,7 @@ new OIMIndexManualOrderedArrayBased<TKey, TPk>()
1509
1538
  **Query:**
1510
1539
  - `getPksByKey(key: TKey): TPk[]` - Returns the ordered primary-key array
1511
1540
  - `getSlotsByKey(key: TKey): readonly TOIMAnyEntitySlot<TPk>[]` - Returns the stored ordered slots
1512
- - `getEntitiesByKey<TEntity>(key: TKey): TEntity[]` - Returns existing entities from stored slots
1541
+ - `getEntitiesByKey<TEntity>(key: TKey): (TEntity | undefined)[]` - Returns entities from stored slots, aligned 1:1 with the pks (holes are `undefined`)
1513
1542
 
1514
1543
  #### `OIMCollectionIndexManualOrderedArrayBased<TKey, TPk, TEntity>`
1515
1544
  Collection-bound ordered Array-based index. Use this when ordered PK writes should resolve to canonical slots from a collection.
@@ -1518,10 +1547,19 @@ Collection-bound ordered Array-based index. Use this when ordered PK writes shou
1518
1547
  - `push(key: TKey, pk: TPk): number` - Append a PK as its canonical slot
1519
1548
  - `insertAt(key: TKey, index: number, pk: TPk): number` - Insert a PK as its canonical slot
1520
1549
  - `reset(key: TKey, pks: readonly TPk[]): void` - Replace the whole ordered list from PKs
1521
- - `getEntitiesByKey(key: TKey): TEntity[]` - Returns entities from canonical slots
1550
+ - `getEntitiesByKey(key: TKey): (TEntity | undefined)[]` - Returns entities from canonical slots (holes are `undefined`)
1522
1551
 
1523
1552
  #### `OIMOrderedListCommandStream<TKey, TPk, TEntity>`
1524
- Slot-first ordered per-key list with a command stream for imperative consumers. It stores data in an `OIMIndexManualOrderedArrayBased` and emits `TOIMOrderedListCommand` batches through `commandsEventEmitter`.
1553
+ Slot-first ordered per-key list with a command stream for imperative consumers. It stores data in an `OIMIndexManualOrderedArrayBased` and emits position-addressed `TOIMOrderedListCommand<Slot>` batches through `commandsEventEmitter`. Each command's `item` is the entity slot (read the entity via `item.item`):
1554
+
1555
+ ```typescript
1556
+ type TOIMOrderedListCommand<TItem> =
1557
+ | { type: 'insert'; index: number; item: TItem }
1558
+ | { type: 'remove'; index: number; count?: number } // count may be > 1
1559
+ | { type: 'move'; from: number; to: number; count?: number }
1560
+ | { type: 'set'; index: number; item: TItem } // replace one element
1561
+ | { type: 'reset'; items: readonly TItem[] }; // replace whole list
1562
+ ```
1525
1563
 
1526
1564
  **Constructor:**
1527
1565
  ```typescript
@@ -1535,25 +1573,29 @@ new OIMOrderedListCommandStream(
1535
1573
  - `index: OIMIndexManualOrderedArrayBased<TKey, TPk>` - Underlying ordered index and source of truth
1536
1574
  - `commandsEventEmitter: OIMUpdateEventEmitter<TKey>` - Key-specific command notifications delivered after queue flush
1537
1575
 
1538
- **Methods:**
1539
- - `setSlots(key: TKey, slots: readonly TOIMEntitySlot<TEntity, TPk>[]): void` - Replace the whole ordered list and emit a `set` command
1540
- - `pushSlot(key: TKey, slot: TOIMEntitySlot<TEntity, TPk>): void` - Append a slot and emit an `insert` command
1541
- - `insertSlotAt(key: TKey, index: number, slot: TOIMEntitySlot<TEntity, TPk>): void` - Insert a slot and emit an `insert` command
1576
+ **Methods:** (`Slot` = `TOIMEntitySlot<TEntity, TPk>`)
1577
+ - `setSlots(key: TKey, slots: readonly Slot[]): void` - Replace the whole ordered list and emit a `reset` command
1578
+ - `pushSlot(key: TKey, slot: Slot): void` - Append a slot and emit an `insert` command
1579
+ - `insertSlotAt(key: TKey, index: number, slot: Slot): void` - Insert a slot and emit an `insert` command
1580
+ - `setSlotAt(key: TKey, index: number, slot: Slot): void` - Replace the slot at `index` in place and emit a `set` command
1542
1581
  - `removeAt(key: TKey, index: number): void` - Remove by index and emit a `remove` command
1582
+ - `removeRange(key: TKey, index: number, count: number): void` - Remove `count` consecutive elements from `index` and emit a `remove` command with `count`
1543
1583
  - `move(key: TKey, fromIndex: number, toIndex: number): void` - Move within the list and emit a `move` command
1544
- - `consumeCommands(key: TKey): TOIMOrderedListCommand<TPk, TEntity>[]` - Read buffered commands for a key inside the notification handler
1545
- - `getBufferedCommands(key: TKey): readonly TOIMOrderedListCommand<TPk, TEntity>[]` - Peek at buffered commands without clearing them
1584
+ - `moveRange(key: TKey, from: number, to: number, count: number): void` - Move `count` consecutive elements and emit a `move` command with `count` (`to` is post-extraction space)
1585
+ - `consumeCommands(key: TKey): TOIMOrderedListCommand<Slot>[]` - Read buffered commands for a key inside the notification handler
1586
+ - `getBufferedCommands(key: TKey): readonly TOIMOrderedListCommand<Slot>[]` - Peek at buffered commands without clearing them
1546
1587
  - `getPksByKey(key: TKey): readonly TPk[]` - Read the current ordered list
1547
1588
  - `getSlotsByKey(key: TKey): readonly TOIMEntitySlot<TEntity, TPk>[]` - Read current ordered slots
1548
- - `getEntitiesByKey(key: TKey): TEntity[]` - Read current ordered entities
1589
+ - `getEntitiesByKey(key: TKey): (TEntity | undefined)[]` - Read current ordered entities (holes are `undefined`)
1549
1590
  - `clear(key?: TKey): void` - Clear all keys or a specific key
1550
1591
  - `destroy(): void` - Dispose subscriptions and clear state
1551
1592
 
1552
1593
  #### `OIMCollectionOrderedListCommandStream<TKey, TPk, TEntity>`
1553
- Collection-bound ordered-list command stream. Public writes use PKs, commands include both `pk`/`pks` and canonical `slot`/`slots`.
1594
+ Collection-bound ordered-list command stream. Public writes use PKs that resolve to canonical collection slots; emitted commands carry the slot as `item` (entity via `item.item`).
1554
1595
 
1555
1596
  **Methods:**
1556
- - `set(key: TKey, pks: readonly TPk[]): void` - Replace the whole ordered list from PKs
1597
+ - `set(key: TKey, pks: readonly TPk[]): void` - Replace the whole ordered list from PKs (emits `reset`)
1598
+ - `setAt(key: TKey, index: number, pk: TPk): void` - Replace the element at `index` with the slot for `pk` (emits `set`)
1557
1599
  - `push(key: TKey, pk: TPk): void` - Append a PK
1558
1600
  - `insertAt(key: TKey, index: number, pk: TPk): void` - Insert a PK
1559
1601
  - all read/command methods from `OIMOrderedListCommandStream`
package/dist/index.cjs CHANGED
@@ -95,6 +95,7 @@ __export(index_exports, {
95
95
  OIMReactiveObject: () => OIMReactiveObject,
96
96
  OIMSelector: () => OIMSelector,
97
97
  createInPlaceEntityUpdater: () => createInPlaceEntityUpdater,
98
+ createMergeEntityUpdater: () => createMergeEntityUpdater,
98
99
  createOIMCollectionIndexFactory: () => createOIMCollectionIndexFactory,
99
100
  createOIMCollectionKit: () => createOIMCollectionKit
100
101
  });
@@ -647,7 +648,7 @@ var OIMCarrierKeyedEmitter = class {
647
648
  }
648
649
  destroy() {
649
650
  if (this.isFlushEnqueued) {
650
- this.queue.cancelTask(this.onFlush);
651
+ this.queue.cancel(this.onFlush);
651
652
  this.isFlushEnqueued = false;
652
653
  }
653
654
  this.subscribedCarriers.forEach((carrier) => {
@@ -671,7 +672,7 @@ var OIMCarrierKeyedEmitter = class {
671
672
  scheduleFlush() {
672
673
  if (this.isFlushEnqueued) return;
673
674
  this.isFlushEnqueued = true;
674
- this.queue.enqueueTask(this.onFlush);
675
+ this.queue.enqueue(this.onFlush);
675
676
  }
676
677
  notify(carrier) {
677
678
  const subscribers = carrier.subscribers;
@@ -967,12 +968,15 @@ var OIMCollectionStoreMapDriven = class extends OIMCollectionStore {
967
968
  }
968
969
  };
969
970
 
971
+ // src/core/createMergeEntityUpdater.ts
972
+ function createMergeEntityUpdater() {
973
+ return (draft, prev) => ({ ...prev, ...draft });
974
+ }
975
+
970
976
  // src/core/OIMEntityUpdaterFactory.ts
971
977
  var OIMEntityUpdaterFactory = class {
972
978
  createMergeEntityUpdater() {
973
- return (draft, prev) => {
974
- return { ...prev, ...draft };
975
- };
979
+ return createMergeEntityUpdater();
976
980
  }
977
981
  };
978
982
 
@@ -1236,12 +1240,12 @@ var OIMReactiveCollection = class extends OIMCollection {
1236
1240
  ensureAnyUpdateEnqueued() {
1237
1241
  if (this.isAnyUpdateScheduled) return;
1238
1242
  this.isAnyUpdateScheduled = true;
1239
- this.queue.enqueueTask(this.runAnyUpdate);
1243
+ this.queue.enqueue(this.runAnyUpdate);
1240
1244
  }
1241
1245
  destroy() {
1242
1246
  this.updateEmitter.destroy();
1243
1247
  if (this.isAnyUpdateScheduled) {
1244
- this.queue.cancelTask(this.runAnyUpdate);
1248
+ this.queue.cancel(this.runAnyUpdate);
1245
1249
  this.isAnyUpdateScheduled = false;
1246
1250
  }
1247
1251
  this.anyUpdateHandlers.clear();
@@ -1291,6 +1295,22 @@ var OIMIndexManualOrderedArrayBased = class extends OIMIndexArrayBased {
1291
1295
  this.emitUpdate([key]);
1292
1296
  return removed;
1293
1297
  }
1298
+ /**
1299
+ * Remove up to `count` consecutive slots starting at `index`.
1300
+ * Returns the number actually removed (clamped to bounds; 0 if nothing).
1301
+ */
1302
+ removeRange(key, index, count) {
1303
+ const list = this.store.getOneByKey(key);
1304
+ if (!list) return 0;
1305
+ if (index < 0 || index >= list.length || count <= 0) return 0;
1306
+ const actual = Math.min(count, list.length - index);
1307
+ list.splice(index, actual);
1308
+ if (list.length === 0) {
1309
+ this.store.removeOneByKey(key);
1310
+ }
1311
+ this.emitUpdate([key]);
1312
+ return actual;
1313
+ }
1294
1314
  move(key, fromIndex, toIndex) {
1295
1315
  const list = this.store.getOneByKey(key);
1296
1316
  if (!list) return void 0;
@@ -1303,6 +1323,32 @@ var OIMIndexManualOrderedArrayBased = class extends OIMIndexArrayBased {
1303
1323
  this.emitUpdate([key]);
1304
1324
  return slot;
1305
1325
  }
1326
+ /** Replace the slot at `index` in place. Returns the safe index, or -1. */
1327
+ setSlotAt(key, index, slot) {
1328
+ const list = this.store.getOneByKey(key);
1329
+ if (!list) return -1;
1330
+ if (index < 0 || index >= list.length) return -1;
1331
+ list[index] = slot;
1332
+ this.emitUpdate([key]);
1333
+ return index;
1334
+ }
1335
+ /**
1336
+ * Move up to `count` consecutive slots from `from` to `to`. `to` is in the
1337
+ * coordinate space *after* the block is extracted (same as single `move`).
1338
+ * Returns the number actually moved (0 if a no-op / out of bounds).
1339
+ */
1340
+ moveRange(key, from, to, count) {
1341
+ const list = this.store.getOneByKey(key);
1342
+ if (!list) return 0;
1343
+ if (from < 0 || from >= list.length || count <= 0) return 0;
1344
+ const actual = Math.min(count, list.length - from);
1345
+ const insertAt = Math.max(0, Math.min(to, list.length - actual));
1346
+ if (insertAt === from) return 0;
1347
+ const block = list.splice(from, actual);
1348
+ list.splice(insertAt, 0, ...block);
1349
+ this.emitUpdate([key]);
1350
+ return actual;
1351
+ }
1306
1352
  resetSlots(key, slots) {
1307
1353
  if (slots.length === 0) {
1308
1354
  if (this.store.getOneByKey(key) !== void 0) {
@@ -1436,7 +1482,7 @@ var OIMUpdateEventEmitter = class {
1436
1482
  }
1437
1483
  if (this.isFlushEnqueued) return;
1438
1484
  this.isFlushEnqueued = true;
1439
- this.queue.enqueueTask(this.onFlush);
1485
+ this.queue.enqueue(this.onFlush);
1440
1486
  }
1441
1487
  markUpdatedKey(key) {
1442
1488
  if (this.deliveryMode === "queue" && this.queue.isInFlush) {
@@ -1458,7 +1504,7 @@ var OIMUpdateEventEmitter = class {
1458
1504
  }
1459
1505
  if (this.isFlushEnqueued) return;
1460
1506
  this.isFlushEnqueued = true;
1461
- this.queue.enqueueTask(this.onFlush);
1507
+ this.queue.enqueue(this.onFlush);
1462
1508
  }
1463
1509
  markAllUpdated() {
1464
1510
  if (this.deliveryMode === "queue" && this.queue.isInFlush) {
@@ -1480,7 +1526,7 @@ var OIMUpdateEventEmitter = class {
1480
1526
  }
1481
1527
  if (this.isFlushEnqueued) return;
1482
1528
  this.isFlushEnqueued = true;
1483
- this.queue.enqueueTask(this.onFlush);
1529
+ this.queue.enqueue(this.onFlush);
1484
1530
  }
1485
1531
  deliverImmediate() {
1486
1532
  this.runSinglePass();
@@ -1513,15 +1559,15 @@ var OIMUpdateEventEmitter = class {
1513
1559
  const flushingKeys = this.updatedKeys;
1514
1560
  this.updatedKeys = /* @__PURE__ */ new Set();
1515
1561
  if (this.keyHandlers.size === 0 || flushingKeys.size === 0) return;
1516
- if (flushingKeys.size * 2 < this.keyHandlers.size) {
1517
- flushingKeys.forEach((key) => {
1518
- const handlers = this.keyHandlers.get(key);
1562
+ if (flushingKeys.size === this.keyHandlers.size) {
1563
+ this.keyHandlers.forEach((handlers, key) => {
1564
+ if (!flushingKeys.has(key)) return;
1519
1565
  if (!handlers || handlers.size === 0) return;
1520
1566
  this.notifyHandlers(handlers);
1521
1567
  });
1522
1568
  } else {
1523
- this.keyHandlers.forEach((handlers, key) => {
1524
- if (!flushingKeys.has(key)) return;
1569
+ flushingKeys.forEach((key) => {
1570
+ const handlers = this.keyHandlers.get(key);
1525
1571
  if (!handlers || handlers.size === 0) return;
1526
1572
  this.notifyHandlers(handlers);
1527
1573
  });
@@ -1529,7 +1575,7 @@ var OIMUpdateEventEmitter = class {
1529
1575
  }
1530
1576
  destroy() {
1531
1577
  if (this.isFlushEnqueued) {
1532
- this.queue.cancelTask(this.onFlush);
1578
+ this.queue.cancel(this.onFlush);
1533
1579
  this.isFlushEnqueued = false;
1534
1580
  }
1535
1581
  if (this.unsubscribeAfterFlushDelivery) {
@@ -1589,7 +1635,7 @@ var OIMUpdateEventEmitter = class {
1589
1635
  if (this.keyHandlers.size === 0) {
1590
1636
  this.updatedKeys.clear();
1591
1637
  if (this.isFlushEnqueued) {
1592
- this.queue.cancelTask(this.onFlush);
1638
+ this.queue.cancel(this.onFlush);
1593
1639
  }
1594
1640
  this.isFlushEnqueued = false;
1595
1641
  }
@@ -1633,7 +1679,7 @@ var OIMOrderedListCommandStream = class {
1633
1679
  if (payload.keys.length === 0) return;
1634
1680
  for (let i = 0; i < payload.keys.length; i++) {
1635
1681
  const key = payload.keys[i];
1636
- this.appendSetCommand(key, this.getSlotsByKey(key));
1682
+ this.appendResetCommand(key, this.getSlotsByKey(key));
1637
1683
  }
1638
1684
  };
1639
1685
  this.commandsEventEmitter = new OIMUpdateEventEmitter(
@@ -1670,12 +1716,7 @@ var OIMOrderedListCommandStream = class {
1670
1716
  pushSlot(key, slot) {
1671
1717
  this.withWrite(() => {
1672
1718
  const index = this.index.pushSlot(key, slot);
1673
- this.appendCommand(key, {
1674
- type: "insert",
1675
- pk: slot.pk,
1676
- slot,
1677
- index
1678
- });
1719
+ this.appendCommand(key, { type: "insert", index, item: slot });
1679
1720
  });
1680
1721
  }
1681
1722
  insertSlotAt(key, index, slot) {
@@ -1683,9 +1724,20 @@ var OIMOrderedListCommandStream = class {
1683
1724
  const safeIndex = this.index.insertSlotAt(key, index, slot);
1684
1725
  this.appendCommand(key, {
1685
1726
  type: "insert",
1686
- pk: slot.pk,
1687
- slot,
1688
- index: safeIndex
1727
+ index: safeIndex,
1728
+ item: slot
1729
+ });
1730
+ });
1731
+ }
1732
+ /** Replace the slot at `index` in place. */
1733
+ setSlotAt(key, index, slot) {
1734
+ this.withWrite(() => {
1735
+ const safeIndex = this.index.setSlotAt(key, index, slot);
1736
+ if (safeIndex < 0) return;
1737
+ this.appendCommand(key, {
1738
+ type: "set",
1739
+ index: safeIndex,
1740
+ item: slot
1689
1741
  });
1690
1742
  });
1691
1743
  }
@@ -1693,12 +1745,15 @@ var OIMOrderedListCommandStream = class {
1693
1745
  this.withWrite(() => {
1694
1746
  const slot = this.index.removeAt(key, index);
1695
1747
  if (slot === void 0) return;
1696
- this.appendCommand(key, {
1697
- type: "remove",
1698
- pk: slot.pk,
1699
- slot,
1700
- index
1701
- });
1748
+ this.appendCommand(key, { type: "remove", index });
1749
+ });
1750
+ }
1751
+ /** Remove `count` consecutive elements from `index`; emits `remove` with `count`. */
1752
+ removeRange(key, index, count) {
1753
+ this.withWrite(() => {
1754
+ const removed = this.index.removeRange(key, index, count);
1755
+ if (removed <= 0) return;
1756
+ this.appendCommand(key, { type: "remove", index, count: removed });
1702
1757
  });
1703
1758
  }
1704
1759
  move(key, fromIndex, toIndex) {
@@ -1707,17 +1762,26 @@ var OIMOrderedListCommandStream = class {
1707
1762
  if (slot === void 0) return;
1708
1763
  this.appendCommand(key, {
1709
1764
  type: "move",
1710
- pk: slot.pk,
1711
- slot,
1712
- fromIndex,
1713
- toIndex
1765
+ from: fromIndex,
1766
+ to: toIndex
1714
1767
  });
1715
1768
  });
1716
1769
  }
1770
+ /**
1771
+ * Move `count` consecutive elements from `from` to `to`; emits `move` with
1772
+ * `count`. `to` is in the post-extraction coordinate space (as single `move`).
1773
+ */
1774
+ moveRange(key, from, to, count) {
1775
+ this.withWrite(() => {
1776
+ const moved = this.index.moveRange(key, from, to, count);
1777
+ if (moved <= 0) return;
1778
+ this.appendCommand(key, { type: "move", from, to, count: moved });
1779
+ });
1780
+ }
1717
1781
  setSlots(key, slots) {
1718
1782
  this.withWrite(() => {
1719
1783
  this.index.resetSlots(key, slots);
1720
- this.appendSetCommand(key, slots);
1784
+ this.appendResetCommand(key, slots);
1721
1785
  });
1722
1786
  }
1723
1787
  getBufferedCommands(key) {
@@ -1761,26 +1825,21 @@ var OIMOrderedListCommandStream = class {
1761
1825
  cmds = [];
1762
1826
  this.commandsByKey.set(key, cmds);
1763
1827
  }
1764
- if (cmd.type === "set") {
1828
+ if (cmd.type === "reset") {
1765
1829
  cmds.length = 0;
1766
1830
  cmds.push(cmd);
1767
- } else if (cmds.length > 0 && cmds[0].type === "set") {
1768
- const slots = this.getSlotsByKey(key);
1769
- cmds[0] = this.createSetCommand(slots);
1831
+ } else if (cmds.length > 0 && cmds[0].type === "reset") {
1832
+ cmds[0] = this.createResetCommand(this.getSlotsByKey(key));
1770
1833
  } else {
1771
1834
  cmds.push(cmd);
1772
1835
  }
1773
1836
  this.commandsEventEmitter.markUpdatedKeys([key]);
1774
1837
  }
1775
- appendSetCommand(key, slots) {
1776
- this.appendCommand(key, this.createSetCommand(slots));
1838
+ appendResetCommand(key, slots) {
1839
+ this.appendCommand(key, this.createResetCommand(slots));
1777
1840
  }
1778
- createSetCommand(slots) {
1779
- return {
1780
- type: "set",
1781
- pks: slots.map((slot) => slot.pk),
1782
- slots
1783
- };
1841
+ createResetCommand(slots) {
1842
+ return { type: "reset", items: slots };
1784
1843
  }
1785
1844
  };
1786
1845
 
@@ -1797,12 +1856,7 @@ var OIMCollectionOrderedListCommandStream = class extends OIMOrderedListCommandS
1797
1856
  this.withWrite(() => {
1798
1857
  const slot = this.resolvePk(pk);
1799
1858
  const index = this.index.pushSlot(key, slot);
1800
- this.appendCommand(key, {
1801
- type: "insert",
1802
- pk,
1803
- slot,
1804
- index
1805
- });
1859
+ this.appendCommand(key, { type: "insert", index, item: slot });
1806
1860
  });
1807
1861
  }
1808
1862
  insertAt(key, index, pk) {
@@ -1811,9 +1865,21 @@ var OIMCollectionOrderedListCommandStream = class extends OIMOrderedListCommandS
1811
1865
  const safeIndex = this.index.insertSlotAt(key, index, slot);
1812
1866
  this.appendCommand(key, {
1813
1867
  type: "insert",
1814
- pk,
1815
- slot,
1816
- index: safeIndex
1868
+ index: safeIndex,
1869
+ item: slot
1870
+ });
1871
+ });
1872
+ }
1873
+ /** Replace the element at `index` with the slot for `pk`, in place. */
1874
+ setAt(key, index, pk) {
1875
+ this.withWrite(() => {
1876
+ const slot = this.resolvePk(pk);
1877
+ const safeIndex = this.index.setSlotAt(key, index, slot);
1878
+ if (safeIndex < 0) return;
1879
+ this.appendCommand(key, {
1880
+ type: "set",
1881
+ index: safeIndex,
1882
+ item: slot
1817
1883
  });
1818
1884
  });
1819
1885
  }
@@ -1821,7 +1887,7 @@ var OIMCollectionOrderedListCommandStream = class extends OIMOrderedListCommandS
1821
1887
  this.withWrite(() => {
1822
1888
  const slots = pks.map((pk) => this.resolvePk(pk));
1823
1889
  this.index.resetSlots(key, slots);
1824
- this.appendSetCommand(key, slots);
1890
+ this.appendResetCommand(key, slots);
1825
1891
  });
1826
1892
  }
1827
1893
  getSlotsByKey(key) {
@@ -2737,48 +2803,23 @@ var OIMEventQueue = class {
2737
2803
  }
2738
2804
  }
2739
2805
  /**
2740
- * Enqueue a one-shot task to be executed on the next flush.
2741
- * Returns a dequeue function that can be called to cancel the task before it runs.
2806
+ * Enqueue a one-shot task to run on the next flush. The function is stored
2807
+ * directly (no wrapper, no returned closure) and cancelled via {@link cancel}
2808
+ * by passing the same reference. Enqueuing the same reference twice is
2809
+ * idempotent — it is Set-deduped and runs once per flush.
2742
2810
  */
2743
2811
  enqueue(fn) {
2744
- let isActive = true;
2745
- const task = () => {
2746
- if (!isActive) return;
2747
- fn();
2748
- };
2749
- this.tasks.add(task);
2750
- this.ensureScheduled();
2751
- return () => {
2752
- if (!isActive) return;
2753
- isActive = false;
2754
- this.tasks.delete(task);
2755
- };
2756
- }
2757
- /**
2758
- * Allocation-free enqueue for a **stable** function reference (e.g. a bound
2759
- * `onFlush` method). The function is stored directly — no wrapper, no
2760
- * returned closure — and cancelled via {@link cancelTask}. Enqueuing the
2761
- * same reference twice is idempotent (Set-deduped); callers that may run a
2762
- * task more than once per flush should use {@link enqueue} instead.
2763
- */
2764
- enqueueTask(fn) {
2765
2812
  this.tasks.add(fn);
2766
2813
  this.ensureScheduled();
2767
2814
  }
2768
- /** Cancel a task scheduled via {@link enqueueTask}; safe to call mid-flush. */
2769
- cancelTask(fn) {
2815
+ /** Cancel a previously enqueued task by its reference; safe to call mid-flush. */
2816
+ cancel(fn) {
2770
2817
  this.tasks.delete(fn);
2771
2818
  this.flushing?.delete(fn);
2772
2819
  }
2773
2820
  get isInFlush() {
2774
2821
  return this.isFlushing;
2775
2822
  }
2776
- /**
2777
- * Dequeue (cancel) a previously enqueued task by its dequeue function.
2778
- */
2779
- dequeue(dequeueFn) {
2780
- dequeueFn();
2781
- }
2782
2823
  /**
2783
2824
  * Execute all pending tasks.
2784
2825
  *
@@ -3425,20 +3466,27 @@ var OIMCollectionChangedFields = class {
3425
3466
  constructor(queue, collection, opts) {
3426
3467
  this.isInWrite = false;
3427
3468
  this.lastEntitySnapshotByPk = /* @__PURE__ */ new Map();
3469
+ // PKs written through this wrapper during the current flush. Their fields are
3470
+ // already tracked accurately via trackPatch, so the flush-time external-write
3471
+ // handler skips them — no redundant diff + snapshot re-clone. Cleared per flush.
3472
+ this.writtenThroughThisFlush = /* @__PURE__ */ new Set();
3428
3473
  this.changedFieldsByPk = /* @__PURE__ */ new Map();
3429
3474
  this.changedPksByField = /* @__PURE__ */ new Map();
3430
3475
  this.onAfterFlush = () => {
3431
3476
  this.clear();
3477
+ this.writtenThroughThisFlush.clear();
3432
3478
  };
3433
3479
  this.onCollectionUpdate = (payload) => {
3434
3480
  if (this.isInWrite) return;
3435
3481
  if (payload.pks.length === 0) return;
3436
3482
  for (let i = 0; i < payload.pks.length; i++) {
3437
3483
  const pk = payload.pks[i];
3484
+ if (this.writtenThroughThisFlush.has(pk)) continue;
3438
3485
  this.trackExternalMutationByPk(pk);
3439
3486
  }
3440
3487
  };
3441
3488
  this.collection = collection;
3489
+ this.detectExternalMutations = opts?.detectExternalMutations !== false;
3442
3490
  this.selectPk = opts?.selectPk ?? (() => {
3443
3491
  throw new Error(
3444
3492
  `[OIMCollectionChangedFields]: selectPk was not provided. Use upsertOneByPk(...) or pass opts.selectPk.`
@@ -3453,15 +3501,17 @@ var OIMCollectionChangedFields = class {
3453
3501
  0 /* AFTER_FLUSH */,
3454
3502
  this.onAfterFlush
3455
3503
  );
3456
- const maybeReactive = collection;
3457
- this.unsubscribeFromCollectionEmitter = maybeReactive.subscribeOnAnyUpdate ? maybeReactive.subscribeOnAnyUpdate((pks) => {
3458
- this.onCollectionUpdate({
3459
- pks
3460
- });
3461
- }) : collection.emitter.on(
3462
- 0 /* UPDATE */,
3463
- this.onCollectionUpdate
3464
- );
3504
+ if (this.detectExternalMutations) {
3505
+ const maybeReactive = collection;
3506
+ this.unsubscribeFromCollectionEmitter = maybeReactive.subscribeOnAnyUpdate ? maybeReactive.subscribeOnAnyUpdate((pks) => {
3507
+ this.onCollectionUpdate({
3508
+ pks
3509
+ });
3510
+ }) : collection.emitter.on(
3511
+ 0 /* UPDATE */,
3512
+ this.onCollectionUpdate
3513
+ );
3514
+ }
3465
3515
  }
3466
3516
  upsertOneByPk(pk, patch) {
3467
3517
  const slot = this.withWrite(() => {
@@ -3517,7 +3567,7 @@ var OIMCollectionChangedFields = class {
3517
3567
  this.changedPksEventEmitter.destroy();
3518
3568
  this.changedFieldsEventEmitter.destroy();
3519
3569
  this.unsubscribeAfterFlush();
3520
- this.unsubscribeFromCollectionEmitter();
3570
+ this.unsubscribeFromCollectionEmitter?.();
3521
3571
  this.clear();
3522
3572
  }
3523
3573
  withWrite(fn) {
@@ -3559,6 +3609,8 @@ var OIMCollectionChangedFields = class {
3559
3609
  return changed;
3560
3610
  }
3561
3611
  updateEntitySnapshot(pk) {
3612
+ if (!this.detectExternalMutations) return;
3613
+ this.writtenThroughThisFlush.add(pk);
3562
3614
  const entity = this.collection.getOneByPk(pk);
3563
3615
  if (!entity) {
3564
3616
  this.lastEntitySnapshotByPk.delete(pk);
@@ -4243,6 +4295,7 @@ var OIMIndexPksByKeysArrayBasedSelector = class extends OIMSelector {
4243
4295
  OIMReactiveObject,
4244
4296
  OIMSelector,
4245
4297
  createInPlaceEntityUpdater,
4298
+ createMergeEntityUpdater,
4246
4299
  createOIMCollectionIndexFactory,
4247
4300
  createOIMCollectionKit
4248
4301
  });