@oimdb/core 3.3.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
  }
@@ -1546,7 +1550,16 @@ Collection-bound ordered Array-based index. Use this when ordered PK writes shou
1546
1550
  - `getEntitiesByKey(key: TKey): (TEntity | undefined)[]` - Returns entities from canonical slots (holes are `undefined`)
1547
1551
 
1548
1552
  #### `OIMOrderedListCommandStream<TKey, TPk, TEntity>`
1549
- 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
+ ```
1550
1563
 
1551
1564
  **Constructor:**
1552
1565
  ```typescript
@@ -1560,14 +1573,17 @@ new OIMOrderedListCommandStream(
1560
1573
  - `index: OIMIndexManualOrderedArrayBased<TKey, TPk>` - Underlying ordered index and source of truth
1561
1574
  - `commandsEventEmitter: OIMUpdateEventEmitter<TKey>` - Key-specific command notifications delivered after queue flush
1562
1575
 
1563
- **Methods:**
1564
- - `setSlots(key: TKey, slots: readonly TOIMEntitySlot<TEntity, TPk>[]): void` - Replace the whole ordered list and emit a `set` command
1565
- - `pushSlot(key: TKey, slot: TOIMEntitySlot<TEntity, TPk>): void` - Append a slot and emit an `insert` command
1566
- - `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
1567
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`
1568
1583
  - `move(key: TKey, fromIndex: number, toIndex: number): void` - Move within the list and emit a `move` command
1569
- - `consumeCommands(key: TKey): TOIMOrderedListCommand<TPk, TEntity>[]` - Read buffered commands for a key inside the notification handler
1570
- - `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
1571
1587
  - `getPksByKey(key: TKey): readonly TPk[]` - Read the current ordered list
1572
1588
  - `getSlotsByKey(key: TKey): readonly TOIMEntitySlot<TEntity, TPk>[]` - Read current ordered slots
1573
1589
  - `getEntitiesByKey(key: TKey): (TEntity | undefined)[]` - Read current ordered entities (holes are `undefined`)
@@ -1575,10 +1591,11 @@ new OIMOrderedListCommandStream(
1575
1591
  - `destroy(): void` - Dispose subscriptions and clear state
1576
1592
 
1577
1593
  #### `OIMCollectionOrderedListCommandStream<TKey, TPk, TEntity>`
1578
- 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`).
1579
1595
 
1580
1596
  **Methods:**
1581
- - `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`)
1582
1599
  - `push(key: TKey, pk: TPk): void` - Append a PK
1583
1600
  - `insertAt(key: TKey, index: number, pk: TPk): void` - Insert a PK
1584
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
  });
@@ -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
 
@@ -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) {
@@ -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
  });
@@ -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) {
@@ -3400,20 +3466,27 @@ var OIMCollectionChangedFields = class {
3400
3466
  constructor(queue, collection, opts) {
3401
3467
  this.isInWrite = false;
3402
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();
3403
3473
  this.changedFieldsByPk = /* @__PURE__ */ new Map();
3404
3474
  this.changedPksByField = /* @__PURE__ */ new Map();
3405
3475
  this.onAfterFlush = () => {
3406
3476
  this.clear();
3477
+ this.writtenThroughThisFlush.clear();
3407
3478
  };
3408
3479
  this.onCollectionUpdate = (payload) => {
3409
3480
  if (this.isInWrite) return;
3410
3481
  if (payload.pks.length === 0) return;
3411
3482
  for (let i = 0; i < payload.pks.length; i++) {
3412
3483
  const pk = payload.pks[i];
3484
+ if (this.writtenThroughThisFlush.has(pk)) continue;
3413
3485
  this.trackExternalMutationByPk(pk);
3414
3486
  }
3415
3487
  };
3416
3488
  this.collection = collection;
3489
+ this.detectExternalMutations = opts?.detectExternalMutations !== false;
3417
3490
  this.selectPk = opts?.selectPk ?? (() => {
3418
3491
  throw new Error(
3419
3492
  `[OIMCollectionChangedFields]: selectPk was not provided. Use upsertOneByPk(...) or pass opts.selectPk.`
@@ -3428,15 +3501,17 @@ var OIMCollectionChangedFields = class {
3428
3501
  0 /* AFTER_FLUSH */,
3429
3502
  this.onAfterFlush
3430
3503
  );
3431
- const maybeReactive = collection;
3432
- this.unsubscribeFromCollectionEmitter = maybeReactive.subscribeOnAnyUpdate ? maybeReactive.subscribeOnAnyUpdate((pks) => {
3433
- this.onCollectionUpdate({
3434
- pks
3435
- });
3436
- }) : collection.emitter.on(
3437
- 0 /* UPDATE */,
3438
- this.onCollectionUpdate
3439
- );
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
+ }
3440
3515
  }
3441
3516
  upsertOneByPk(pk, patch) {
3442
3517
  const slot = this.withWrite(() => {
@@ -3492,7 +3567,7 @@ var OIMCollectionChangedFields = class {
3492
3567
  this.changedPksEventEmitter.destroy();
3493
3568
  this.changedFieldsEventEmitter.destroy();
3494
3569
  this.unsubscribeAfterFlush();
3495
- this.unsubscribeFromCollectionEmitter();
3570
+ this.unsubscribeFromCollectionEmitter?.();
3496
3571
  this.clear();
3497
3572
  }
3498
3573
  withWrite(fn) {
@@ -3534,6 +3609,8 @@ var OIMCollectionChangedFields = class {
3534
3609
  return changed;
3535
3610
  }
3536
3611
  updateEntitySnapshot(pk) {
3612
+ if (!this.detectExternalMutations) return;
3613
+ this.writtenThroughThisFlush.add(pk);
3537
3614
  const entity = this.collection.getOneByPk(pk);
3538
3615
  if (!entity) {
3539
3616
  this.lastEntitySnapshotByPk.delete(pk);
@@ -4218,6 +4295,7 @@ var OIMIndexPksByKeysArrayBasedSelector = class extends OIMSelector {
4218
4295
  OIMReactiveObject,
4219
4296
  OIMSelector,
4220
4297
  createInPlaceEntityUpdater,
4298
+ createMergeEntityUpdater,
4221
4299
  createOIMCollectionIndexFactory,
4222
4300
  createOIMCollectionKit
4223
4301
  });
package/dist/index.d.cts CHANGED
@@ -627,7 +627,20 @@ declare class OIMIndexManualOrderedArrayBased<TKey extends TOIMPk, TItemKey exte
627
627
  pushSlot(key: TKey, slot: TOIMAnyEntitySlot<TItemKey>): number;
628
628
  insertSlotAt(key: TKey, index: number, slot: TOIMAnyEntitySlot<TItemKey>): number;
629
629
  removeAt(key: TKey, index: number): TOIMAnyEntitySlot<TItemKey> | undefined;
630
+ /**
631
+ * Remove up to `count` consecutive slots starting at `index`.
632
+ * Returns the number actually removed (clamped to bounds; 0 if nothing).
633
+ */
634
+ removeRange(key: TKey, index: number, count: number): number;
630
635
  move(key: TKey, fromIndex: number, toIndex: number): TOIMAnyEntitySlot<TItemKey> | undefined;
636
+ /** Replace the slot at `index` in place. Returns the safe index, or -1. */
637
+ setSlotAt(key: TKey, index: number, slot: TOIMAnyEntitySlot<TItemKey>): number;
638
+ /**
639
+ * Move up to `count` consecutive slots from `from` to `to`. `to` is in the
640
+ * coordinate space *after* the block is extracted (same as single `move`).
641
+ * Returns the number actually moved (0 if a no-op / out of bounds).
642
+ */
643
+ moveRange(key: TKey, from: number, to: number, count: number): number;
631
644
  resetSlots(key: TKey, slots: readonly TOIMAnyEntitySlot<TItemKey>[]): void;
632
645
  private getOrCreateList;
633
646
  }
@@ -712,36 +725,46 @@ declare class OIMUpdateEventEmitter<TKey extends TOIMPk> {
712
725
  unsubscribeFromKeys(keys: readonly TKey[], handler: TOIMEventHandler<void>): void;
713
726
  }
714
727
 
715
- type TOIMOrderedListCommand<TPk extends TOIMPk, TEntity extends object = object> = {
728
+ /**
729
+ * Incremental command for an ordered per-key list, addressed by position.
730
+ *
731
+ * `item` is whatever the stream stores per element — the command stream uses the
732
+ * entity slot (`TOIMEntitySlot`), so a consumer reads `item.item` for the entity.
733
+ *
734
+ * - `insert` — a single element appears at `index`.
735
+ * - `remove` — `count` elements (default 1, may be > 1) disappear from `index`.
736
+ * - `move` — `count` elements (default 1) move from `from` to `to`.
737
+ * - `set` — the element at `index` is replaced in place.
738
+ * - `reset` — the whole list for the key is replaced by `items`.
739
+ */
740
+ type TOIMOrderedListCommand<TItem> = {
716
741
  type: 'insert';
717
- pk: TPk;
718
- slot: TOIMEntitySlot<TEntity, TPk>;
719
742
  index: number;
743
+ item: TItem;
720
744
  } | {
721
745
  type: 'remove';
722
- pk: TPk;
723
- slot: TOIMEntitySlot<TEntity, TPk>;
724
746
  index: number;
747
+ count?: number;
725
748
  } | {
726
749
  type: 'move';
727
- pk: TPk;
728
- slot: TOIMEntitySlot<TEntity, TPk>;
729
- fromIndex: number;
730
- toIndex: number;
750
+ from: number;
751
+ to: number;
752
+ count?: number;
731
753
  } | {
732
- /**
733
- * Replace the whole list for this key (preferred name).
734
- */
735
754
  type: 'set';
736
- pks: readonly TPk[];
737
- slots: readonly TOIMEntitySlot<TEntity, TPk>[];
755
+ index: number;
756
+ item: TItem;
757
+ } | {
758
+ type: 'reset';
759
+ items: readonly TItem[];
738
760
  };
739
761
 
740
762
  /**
741
763
  * Slot-first ordered-list command stream.
742
764
  *
743
- * Use this when you already have slots and want incremental commands for an
744
- * imperative renderer. For PK writes bound to a collection, use
765
+ * Emits position-addressed {@link TOIMOrderedListCommand}s whose `item` is the
766
+ * entity slot. Use this when you already have slots and want incremental
767
+ * commands for an imperative renderer. For PK writes bound to a collection, use
745
768
  * `OIMCollectionOrderedListCommandStream`.
746
769
  */
747
770
  declare class OIMOrderedListCommandStream<TKey extends TOIMPk, TPk extends TOIMPk, TEntity extends object = object> {
@@ -760,19 +783,28 @@ declare class OIMOrderedListCommandStream<TKey extends TOIMPk, TPk extends TOIMP
760
783
  getKeys(): readonly TKey[];
761
784
  pushSlot(key: TKey, slot: TOIMEntitySlot<TEntity, TPk>): void;
762
785
  insertSlotAt(key: TKey, index: number, slot: TOIMEntitySlot<TEntity, TPk>): void;
786
+ /** Replace the slot at `index` in place. */
787
+ setSlotAt(key: TKey, index: number, slot: TOIMEntitySlot<TEntity, TPk>): void;
763
788
  removeAt(key: TKey, index: number): void;
789
+ /** Remove `count` consecutive elements from `index`; emits `remove` with `count`. */
790
+ removeRange(key: TKey, index: number, count: number): void;
764
791
  move(key: TKey, fromIndex: number, toIndex: number): void;
792
+ /**
793
+ * Move `count` consecutive elements from `from` to `to`; emits `move` with
794
+ * `count`. `to` is in the post-extraction coordinate space (as single `move`).
795
+ */
796
+ moveRange(key: TKey, from: number, to: number, count: number): void;
765
797
  setSlots(key: TKey, slots: readonly TOIMEntitySlot<TEntity, TPk>[]): void;
766
- getBufferedCommands(key: TKey): readonly TOIMOrderedListCommand<TPk, TEntity>[];
767
- consumeCommands(key: TKey): TOIMOrderedListCommand<TPk, TEntity>[];
798
+ getBufferedCommands(key: TKey): readonly TOIMOrderedListCommand<TOIMEntitySlot<TEntity, TPk>>[];
799
+ consumeCommands(key: TKey): TOIMOrderedListCommand<TOIMEntitySlot<TEntity, TPk>>[];
768
800
  clear(key?: TKey): void;
769
801
  destroy(): void;
770
802
  private readonly onAfterFlush;
771
803
  private readonly onIndexUpdate;
772
804
  protected withWrite(fn: () => void): void;
773
- protected appendCommand(key: TKey, cmd: TOIMOrderedListCommand<TPk, TEntity>): void;
774
- protected appendSetCommand(key: TKey, slots: readonly TOIMEntitySlot<TEntity, TPk>[]): void;
775
- protected createSetCommand(slots: readonly TOIMEntitySlot<TEntity, TPk>[]): TOIMOrderedListCommand<TPk, TEntity>;
805
+ protected appendCommand(key: TKey, cmd: TOIMOrderedListCommand<TOIMEntitySlot<TEntity, TPk>>): void;
806
+ protected appendResetCommand(key: TKey, slots: readonly TOIMEntitySlot<TEntity, TPk>[]): void;
807
+ protected createResetCommand(slots: readonly TOIMEntitySlot<TEntity, TPk>[]): TOIMOrderedListCommand<TOIMEntitySlot<TEntity, TPk>>;
776
808
  }
777
809
 
778
810
  /**
@@ -785,6 +817,8 @@ declare class OIMCollectionOrderedListCommandStream<TKey extends TOIMPk, TPk ext
785
817
  constructor(queue: OIMEventQueue, opts: TOIMCollectionOrderedListCommandStreamOptions<TKey, TPk, TEntity>);
786
818
  push(key: TKey, pk: TPk): void;
787
819
  insertAt(key: TKey, index: number, pk: TPk): void;
820
+ /** Replace the element at `index` with the slot for `pk`, in place. */
821
+ setAt(key: TKey, index: number, pk: TPk): void;
788
822
  set(key: TKey, pks: readonly TPk[]): void;
789
823
  getSlotsByKey(key: TKey): readonly TOIMEntitySlot<TEntity, TPk>[];
790
824
  getEntitiesByKey(key: TKey): (TEntity | undefined)[];
@@ -1149,6 +1183,18 @@ declare class OIMEntityUpdaterFactory<TEntity extends object> {
1149
1183
  */
1150
1184
  declare function createInPlaceEntityUpdater<TEntity extends object>(): TOIMEntityUpdater<TEntity>;
1151
1185
 
1186
+ /**
1187
+ * Default entity updater: shallow-merges the patch into a NEW object
1188
+ * (`{ ...prev, ...draft }`). Each update produces a fresh entity reference, which
1189
+ * reference-comparison bindings (React's `Object.is` / `useSyncExternalStore`,
1190
+ * `React.memo`, prev/next diffing, time-travel) need to detect a change.
1191
+ *
1192
+ * The new-object allocation is the biggest per-update data-layer cost; for
1193
+ * update-heavy, subscription-driven readers use {@link createInPlaceEntityUpdater}
1194
+ * instead.
1195
+ */
1196
+ declare function createMergeEntityUpdater<TEntity extends object>(): TOIMEntityUpdater<TEntity>;
1197
+
1152
1198
  /**
1153
1199
  * Factory for creating index comparators.
1154
1200
  * Provides common comparison strategies for primary key arrays.
@@ -1297,9 +1343,11 @@ declare class OIMCollectionChangedFields<TEntity extends object, TPk extends TOI
1297
1343
  readonly collection: OIMCollection<TEntity, TPk>;
1298
1344
  private readonly selectPk;
1299
1345
  private readonly unsubscribeAfterFlush;
1300
- private readonly unsubscribeFromCollectionEmitter;
1346
+ private readonly unsubscribeFromCollectionEmitter?;
1347
+ private readonly detectExternalMutations;
1301
1348
  private isInWrite;
1302
1349
  private readonly lastEntitySnapshotByPk;
1350
+ private readonly writtenThroughThisFlush;
1303
1351
  /**
1304
1352
  * Emits updates scoped by PK. Subscribe, then read state via `getChangedFieldsByPk(...)`
1305
1353
  * or drain via `consumeChangedFieldsByPk(...)`.
@@ -1318,6 +1366,18 @@ declare class OIMCollectionChangedFields<TEntity extends object, TPk extends TOI
1318
1366
  * If not provided, only `upsertOneByPk(...)` should be used.
1319
1367
  */
1320
1368
  selectPk?: (entity: TEntity | Partial<TEntity>) => TPk;
1369
+ /**
1370
+ * Detect writes that bypass this wrapper (direct collection
1371
+ * mutations) by diffing entity snapshots. Default `true`.
1372
+ *
1373
+ * Set to `false` when you always write through this wrapper: it
1374
+ * drops the per-write snapshot clone and the per-PK snapshot map
1375
+ * (~2x entity memory), since those exist only to power external
1376
+ * detection. The shallow diff also can't see deep in-place
1377
+ * mutations anyway — so if that's your model, opting out costs you
1378
+ * nothing it could have caught.
1379
+ */
1380
+ detectExternalMutations?: boolean;
1321
1381
  });
1322
1382
  upsertOneByPk(pk: TPk, patch: Partial<TEntity>): TOIMEntitySlot<TEntity, TPk>;
1323
1383
  upsertOne(entity: TEntity | Partial<TEntity>): TOIMEntitySlot<TEntity, TPk>;
@@ -1690,4 +1750,4 @@ declare class OIMEntitiesByIndexKeyArrayBasedSelector<TEntity extends object, TP
1690
1750
  protected areEqual(prev: readonly (TEntity | undefined)[], next: readonly (TEntity | undefined)[]): boolean;
1691
1751
  }
1692
1752
 
1693
- export { EOIMCollectionEventType, EOIMComputedEventType, EOIMEventQueueEventType, EOIMEventQueueSchedulerEventType, EOIMIndexEventType, EOIMObjectEventType, type IOIMCarrierResolver, type IOIMComputedEventSource, type IOIMEffectDependency, type IOIMEventQueueSchedulerEvents, type IOIMIndexStore, type IOIMKeyedUpdateEmitter, type IOIMSubscribable, OIMCarrierKeyedEmitter, OIMCollection, OIMCollectionByPkSelector, OIMCollectionByPksSelector, OIMCollectionChangedFields, OIMCollectionIndexFactory, OIMCollectionIndexManualOrderedArrayBased, OIMCollectionOrderedListCommandStream, OIMCollectionSelectors, OIMCollectionStore, OIMCollectionStoreMapDriven, OIMComparatorFactory, OIMComputeRuntime, OIMComputed, OIMDBSettings, OIMDerivedCollectionIndexArrayBased, OIMDerivedCollectionIndexSetBased, OIMEffect, OIMEffectDependencyComputed, OIMEffectDependencyKeyed, OIMEffectDependencyKeyedCollection, OIMEffectDependencyKeyedIndex, OIMEffectDependencyKeyedObject, OIMEntitiesByIndexKeyArrayBasedSelector, OIMEntitiesByIndexKeySetBasedSelector, OIMEntityUpdaterFactory, OIMEventEmitter, OIMEventQueue, OIMEventQueueScheduler, OIMEventQueueSchedulerAnimationFrame, OIMEventQueueSchedulerFactory, OIMEventQueueSchedulerImmediate, OIMEventQueueSchedulerMicrotask, OIMEventQueueSchedulerTimeout, OIMIndex, OIMIndexArrayBased, OIMIndexComparatorFactory, OIMIndexManualArrayBased, OIMIndexManualOrderedArrayBased, OIMIndexManualSetBased, OIMIndexPksByKeyArrayBasedSelector, OIMIndexPksByKeySetBasedSelector, OIMIndexPksByKeysArrayBasedSelector, OIMIndexPksByKeysSetBasedSelector, OIMIndexSetBased, OIMIndexStoreArrayBased, OIMIndexStoreMapDrivenArrayBased, OIMIndexStoreMapDrivenSetBased, OIMIndexStoreSetBased, OIMMap2Keys, OIMObject, OIMObjectStore, OIMObjectStoreMapDriven, OIMObjectStoreRecordDriven, OIMObjectValueByKeySelector, OIMObjectValuesByKeysSelector, OIMOrderedListCommandStream, OIMPkSelectorFactory, OIMReactiveCollection, OIMReactiveCollectionIndexManualArrayBased, OIMReactiveCollectionIndexManualSetBased, OIMReactiveIndex, OIMReactiveIndexArrayBased, OIMReactiveIndexManualArrayBased, OIMReactiveIndexManualSetBased, OIMReactiveIndexSetBased, OIMReactiveObject, OIMSelector, type TOIMAnyEntitySlot, type TOIMCollectionEntitiesSelector, type TOIMCollectionEntitySelector, type TOIMCollectionIndexArrayBasedOptions, type TOIMCollectionIndexSetBasedOptions, type TOIMCollectionKit, type TOIMCollectionOptions, type TOIMCollectionOrderedIndexOptions, type TOIMCollectionOrderedListCommandStreamOptions, type TOIMCollectionUpdatePayload, type TOIMComparator, type TOIMComputedOptions, type TOIMComputedUpdatePayload, type TOIMDerivedCollectionIndexArrayBasedOptions, type TOIMDerivedCollectionIndexSetBasedOptions, type TOIMDerivedEntityComparator, type TOIMDerivedEntityOrderSelector, type TOIMDerivedEntityOrderValue, type TOIMDerivedIndexKeySelector, type TOIMEffectOptions, type TOIMEntitySlot, type TOIMEntitySlotResolver, type TOIMEntityUpdater, type TOIMEventHandler, type TOIMEventQueueOptions, type TOIMIndexComparator, type TOIMIndexKey, type TOIMIndexOptionsArrayBased, type TOIMIndexOptionsSetBased, type TOIMIndexPksUpdatePayload, type TOIMIndexUpdatePayload, type TOIMObjectOptions, type TOIMOrderedListCommand, type TOIMPk, type TOIMPkSelector, type TOIMReactiveCollectionFactoryOptions, type TOIMRelationsArrayIndexOptions, type TOIMRelationsDerivedArrayIndexOptions, type TOIMRelationsDerivedSetIndexOptions, type TOIMRelationsOrderedListOptions, type TOIMRelationsSetIndexOptions, type TOIMSchedulerOptions, type TOIMSchedulerType, type TOIMUpdatePayload, createInPlaceEntityUpdater, createOIMCollectionIndexFactory, createOIMCollectionKit };
1753
+ export { EOIMCollectionEventType, EOIMComputedEventType, EOIMEventQueueEventType, EOIMEventQueueSchedulerEventType, EOIMIndexEventType, EOIMObjectEventType, type IOIMCarrierResolver, type IOIMComputedEventSource, type IOIMEffectDependency, type IOIMEventQueueSchedulerEvents, type IOIMIndexStore, type IOIMKeyedUpdateEmitter, type IOIMSubscribable, OIMCarrierKeyedEmitter, OIMCollection, OIMCollectionByPkSelector, OIMCollectionByPksSelector, OIMCollectionChangedFields, OIMCollectionIndexFactory, OIMCollectionIndexManualOrderedArrayBased, OIMCollectionOrderedListCommandStream, OIMCollectionSelectors, OIMCollectionStore, OIMCollectionStoreMapDriven, OIMComparatorFactory, OIMComputeRuntime, OIMComputed, OIMDBSettings, OIMDerivedCollectionIndexArrayBased, OIMDerivedCollectionIndexSetBased, OIMEffect, OIMEffectDependencyComputed, OIMEffectDependencyKeyed, OIMEffectDependencyKeyedCollection, OIMEffectDependencyKeyedIndex, OIMEffectDependencyKeyedObject, OIMEntitiesByIndexKeyArrayBasedSelector, OIMEntitiesByIndexKeySetBasedSelector, OIMEntityUpdaterFactory, OIMEventEmitter, OIMEventQueue, OIMEventQueueScheduler, OIMEventQueueSchedulerAnimationFrame, OIMEventQueueSchedulerFactory, OIMEventQueueSchedulerImmediate, OIMEventQueueSchedulerMicrotask, OIMEventQueueSchedulerTimeout, OIMIndex, OIMIndexArrayBased, OIMIndexComparatorFactory, OIMIndexManualArrayBased, OIMIndexManualOrderedArrayBased, OIMIndexManualSetBased, OIMIndexPksByKeyArrayBasedSelector, OIMIndexPksByKeySetBasedSelector, OIMIndexPksByKeysArrayBasedSelector, OIMIndexPksByKeysSetBasedSelector, OIMIndexSetBased, OIMIndexStoreArrayBased, OIMIndexStoreMapDrivenArrayBased, OIMIndexStoreMapDrivenSetBased, OIMIndexStoreSetBased, OIMMap2Keys, OIMObject, OIMObjectStore, OIMObjectStoreMapDriven, OIMObjectStoreRecordDriven, OIMObjectValueByKeySelector, OIMObjectValuesByKeysSelector, OIMOrderedListCommandStream, OIMPkSelectorFactory, OIMReactiveCollection, OIMReactiveCollectionIndexManualArrayBased, OIMReactiveCollectionIndexManualSetBased, OIMReactiveIndex, OIMReactiveIndexArrayBased, OIMReactiveIndexManualArrayBased, OIMReactiveIndexManualSetBased, OIMReactiveIndexSetBased, OIMReactiveObject, OIMSelector, type TOIMAnyEntitySlot, type TOIMCollectionEntitiesSelector, type TOIMCollectionEntitySelector, type TOIMCollectionIndexArrayBasedOptions, type TOIMCollectionIndexSetBasedOptions, type TOIMCollectionKit, type TOIMCollectionOptions, type TOIMCollectionOrderedIndexOptions, type TOIMCollectionOrderedListCommandStreamOptions, type TOIMCollectionUpdatePayload, type TOIMComparator, type TOIMComputedOptions, type TOIMComputedUpdatePayload, type TOIMDerivedCollectionIndexArrayBasedOptions, type TOIMDerivedCollectionIndexSetBasedOptions, type TOIMDerivedEntityComparator, type TOIMDerivedEntityOrderSelector, type TOIMDerivedEntityOrderValue, type TOIMDerivedIndexKeySelector, type TOIMEffectOptions, type TOIMEntitySlot, type TOIMEntitySlotResolver, type TOIMEntityUpdater, type TOIMEventHandler, type TOIMEventQueueOptions, type TOIMIndexComparator, type TOIMIndexKey, type TOIMIndexOptionsArrayBased, type TOIMIndexOptionsSetBased, type TOIMIndexPksUpdatePayload, type TOIMIndexUpdatePayload, type TOIMObjectOptions, type TOIMOrderedListCommand, type TOIMPk, type TOIMPkSelector, type TOIMReactiveCollectionFactoryOptions, type TOIMRelationsArrayIndexOptions, type TOIMRelationsDerivedArrayIndexOptions, type TOIMRelationsDerivedSetIndexOptions, type TOIMRelationsOrderedListOptions, type TOIMRelationsSetIndexOptions, type TOIMSchedulerOptions, type TOIMSchedulerType, type TOIMUpdatePayload, createInPlaceEntityUpdater, createMergeEntityUpdater, createOIMCollectionIndexFactory, createOIMCollectionKit };
package/dist/index.d.ts CHANGED
@@ -627,7 +627,20 @@ declare class OIMIndexManualOrderedArrayBased<TKey extends TOIMPk, TItemKey exte
627
627
  pushSlot(key: TKey, slot: TOIMAnyEntitySlot<TItemKey>): number;
628
628
  insertSlotAt(key: TKey, index: number, slot: TOIMAnyEntitySlot<TItemKey>): number;
629
629
  removeAt(key: TKey, index: number): TOIMAnyEntitySlot<TItemKey> | undefined;
630
+ /**
631
+ * Remove up to `count` consecutive slots starting at `index`.
632
+ * Returns the number actually removed (clamped to bounds; 0 if nothing).
633
+ */
634
+ removeRange(key: TKey, index: number, count: number): number;
630
635
  move(key: TKey, fromIndex: number, toIndex: number): TOIMAnyEntitySlot<TItemKey> | undefined;
636
+ /** Replace the slot at `index` in place. Returns the safe index, or -1. */
637
+ setSlotAt(key: TKey, index: number, slot: TOIMAnyEntitySlot<TItemKey>): number;
638
+ /**
639
+ * Move up to `count` consecutive slots from `from` to `to`. `to` is in the
640
+ * coordinate space *after* the block is extracted (same as single `move`).
641
+ * Returns the number actually moved (0 if a no-op / out of bounds).
642
+ */
643
+ moveRange(key: TKey, from: number, to: number, count: number): number;
631
644
  resetSlots(key: TKey, slots: readonly TOIMAnyEntitySlot<TItemKey>[]): void;
632
645
  private getOrCreateList;
633
646
  }
@@ -712,36 +725,46 @@ declare class OIMUpdateEventEmitter<TKey extends TOIMPk> {
712
725
  unsubscribeFromKeys(keys: readonly TKey[], handler: TOIMEventHandler<void>): void;
713
726
  }
714
727
 
715
- type TOIMOrderedListCommand<TPk extends TOIMPk, TEntity extends object = object> = {
728
+ /**
729
+ * Incremental command for an ordered per-key list, addressed by position.
730
+ *
731
+ * `item` is whatever the stream stores per element — the command stream uses the
732
+ * entity slot (`TOIMEntitySlot`), so a consumer reads `item.item` for the entity.
733
+ *
734
+ * - `insert` — a single element appears at `index`.
735
+ * - `remove` — `count` elements (default 1, may be > 1) disappear from `index`.
736
+ * - `move` — `count` elements (default 1) move from `from` to `to`.
737
+ * - `set` — the element at `index` is replaced in place.
738
+ * - `reset` — the whole list for the key is replaced by `items`.
739
+ */
740
+ type TOIMOrderedListCommand<TItem> = {
716
741
  type: 'insert';
717
- pk: TPk;
718
- slot: TOIMEntitySlot<TEntity, TPk>;
719
742
  index: number;
743
+ item: TItem;
720
744
  } | {
721
745
  type: 'remove';
722
- pk: TPk;
723
- slot: TOIMEntitySlot<TEntity, TPk>;
724
746
  index: number;
747
+ count?: number;
725
748
  } | {
726
749
  type: 'move';
727
- pk: TPk;
728
- slot: TOIMEntitySlot<TEntity, TPk>;
729
- fromIndex: number;
730
- toIndex: number;
750
+ from: number;
751
+ to: number;
752
+ count?: number;
731
753
  } | {
732
- /**
733
- * Replace the whole list for this key (preferred name).
734
- */
735
754
  type: 'set';
736
- pks: readonly TPk[];
737
- slots: readonly TOIMEntitySlot<TEntity, TPk>[];
755
+ index: number;
756
+ item: TItem;
757
+ } | {
758
+ type: 'reset';
759
+ items: readonly TItem[];
738
760
  };
739
761
 
740
762
  /**
741
763
  * Slot-first ordered-list command stream.
742
764
  *
743
- * Use this when you already have slots and want incremental commands for an
744
- * imperative renderer. For PK writes bound to a collection, use
765
+ * Emits position-addressed {@link TOIMOrderedListCommand}s whose `item` is the
766
+ * entity slot. Use this when you already have slots and want incremental
767
+ * commands for an imperative renderer. For PK writes bound to a collection, use
745
768
  * `OIMCollectionOrderedListCommandStream`.
746
769
  */
747
770
  declare class OIMOrderedListCommandStream<TKey extends TOIMPk, TPk extends TOIMPk, TEntity extends object = object> {
@@ -760,19 +783,28 @@ declare class OIMOrderedListCommandStream<TKey extends TOIMPk, TPk extends TOIMP
760
783
  getKeys(): readonly TKey[];
761
784
  pushSlot(key: TKey, slot: TOIMEntitySlot<TEntity, TPk>): void;
762
785
  insertSlotAt(key: TKey, index: number, slot: TOIMEntitySlot<TEntity, TPk>): void;
786
+ /** Replace the slot at `index` in place. */
787
+ setSlotAt(key: TKey, index: number, slot: TOIMEntitySlot<TEntity, TPk>): void;
763
788
  removeAt(key: TKey, index: number): void;
789
+ /** Remove `count` consecutive elements from `index`; emits `remove` with `count`. */
790
+ removeRange(key: TKey, index: number, count: number): void;
764
791
  move(key: TKey, fromIndex: number, toIndex: number): void;
792
+ /**
793
+ * Move `count` consecutive elements from `from` to `to`; emits `move` with
794
+ * `count`. `to` is in the post-extraction coordinate space (as single `move`).
795
+ */
796
+ moveRange(key: TKey, from: number, to: number, count: number): void;
765
797
  setSlots(key: TKey, slots: readonly TOIMEntitySlot<TEntity, TPk>[]): void;
766
- getBufferedCommands(key: TKey): readonly TOIMOrderedListCommand<TPk, TEntity>[];
767
- consumeCommands(key: TKey): TOIMOrderedListCommand<TPk, TEntity>[];
798
+ getBufferedCommands(key: TKey): readonly TOIMOrderedListCommand<TOIMEntitySlot<TEntity, TPk>>[];
799
+ consumeCommands(key: TKey): TOIMOrderedListCommand<TOIMEntitySlot<TEntity, TPk>>[];
768
800
  clear(key?: TKey): void;
769
801
  destroy(): void;
770
802
  private readonly onAfterFlush;
771
803
  private readonly onIndexUpdate;
772
804
  protected withWrite(fn: () => void): void;
773
- protected appendCommand(key: TKey, cmd: TOIMOrderedListCommand<TPk, TEntity>): void;
774
- protected appendSetCommand(key: TKey, slots: readonly TOIMEntitySlot<TEntity, TPk>[]): void;
775
- protected createSetCommand(slots: readonly TOIMEntitySlot<TEntity, TPk>[]): TOIMOrderedListCommand<TPk, TEntity>;
805
+ protected appendCommand(key: TKey, cmd: TOIMOrderedListCommand<TOIMEntitySlot<TEntity, TPk>>): void;
806
+ protected appendResetCommand(key: TKey, slots: readonly TOIMEntitySlot<TEntity, TPk>[]): void;
807
+ protected createResetCommand(slots: readonly TOIMEntitySlot<TEntity, TPk>[]): TOIMOrderedListCommand<TOIMEntitySlot<TEntity, TPk>>;
776
808
  }
777
809
 
778
810
  /**
@@ -785,6 +817,8 @@ declare class OIMCollectionOrderedListCommandStream<TKey extends TOIMPk, TPk ext
785
817
  constructor(queue: OIMEventQueue, opts: TOIMCollectionOrderedListCommandStreamOptions<TKey, TPk, TEntity>);
786
818
  push(key: TKey, pk: TPk): void;
787
819
  insertAt(key: TKey, index: number, pk: TPk): void;
820
+ /** Replace the element at `index` with the slot for `pk`, in place. */
821
+ setAt(key: TKey, index: number, pk: TPk): void;
788
822
  set(key: TKey, pks: readonly TPk[]): void;
789
823
  getSlotsByKey(key: TKey): readonly TOIMEntitySlot<TEntity, TPk>[];
790
824
  getEntitiesByKey(key: TKey): (TEntity | undefined)[];
@@ -1149,6 +1183,18 @@ declare class OIMEntityUpdaterFactory<TEntity extends object> {
1149
1183
  */
1150
1184
  declare function createInPlaceEntityUpdater<TEntity extends object>(): TOIMEntityUpdater<TEntity>;
1151
1185
 
1186
+ /**
1187
+ * Default entity updater: shallow-merges the patch into a NEW object
1188
+ * (`{ ...prev, ...draft }`). Each update produces a fresh entity reference, which
1189
+ * reference-comparison bindings (React's `Object.is` / `useSyncExternalStore`,
1190
+ * `React.memo`, prev/next diffing, time-travel) need to detect a change.
1191
+ *
1192
+ * The new-object allocation is the biggest per-update data-layer cost; for
1193
+ * update-heavy, subscription-driven readers use {@link createInPlaceEntityUpdater}
1194
+ * instead.
1195
+ */
1196
+ declare function createMergeEntityUpdater<TEntity extends object>(): TOIMEntityUpdater<TEntity>;
1197
+
1152
1198
  /**
1153
1199
  * Factory for creating index comparators.
1154
1200
  * Provides common comparison strategies for primary key arrays.
@@ -1297,9 +1343,11 @@ declare class OIMCollectionChangedFields<TEntity extends object, TPk extends TOI
1297
1343
  readonly collection: OIMCollection<TEntity, TPk>;
1298
1344
  private readonly selectPk;
1299
1345
  private readonly unsubscribeAfterFlush;
1300
- private readonly unsubscribeFromCollectionEmitter;
1346
+ private readonly unsubscribeFromCollectionEmitter?;
1347
+ private readonly detectExternalMutations;
1301
1348
  private isInWrite;
1302
1349
  private readonly lastEntitySnapshotByPk;
1350
+ private readonly writtenThroughThisFlush;
1303
1351
  /**
1304
1352
  * Emits updates scoped by PK. Subscribe, then read state via `getChangedFieldsByPk(...)`
1305
1353
  * or drain via `consumeChangedFieldsByPk(...)`.
@@ -1318,6 +1366,18 @@ declare class OIMCollectionChangedFields<TEntity extends object, TPk extends TOI
1318
1366
  * If not provided, only `upsertOneByPk(...)` should be used.
1319
1367
  */
1320
1368
  selectPk?: (entity: TEntity | Partial<TEntity>) => TPk;
1369
+ /**
1370
+ * Detect writes that bypass this wrapper (direct collection
1371
+ * mutations) by diffing entity snapshots. Default `true`.
1372
+ *
1373
+ * Set to `false` when you always write through this wrapper: it
1374
+ * drops the per-write snapshot clone and the per-PK snapshot map
1375
+ * (~2x entity memory), since those exist only to power external
1376
+ * detection. The shallow diff also can't see deep in-place
1377
+ * mutations anyway — so if that's your model, opting out costs you
1378
+ * nothing it could have caught.
1379
+ */
1380
+ detectExternalMutations?: boolean;
1321
1381
  });
1322
1382
  upsertOneByPk(pk: TPk, patch: Partial<TEntity>): TOIMEntitySlot<TEntity, TPk>;
1323
1383
  upsertOne(entity: TEntity | Partial<TEntity>): TOIMEntitySlot<TEntity, TPk>;
@@ -1690,4 +1750,4 @@ declare class OIMEntitiesByIndexKeyArrayBasedSelector<TEntity extends object, TP
1690
1750
  protected areEqual(prev: readonly (TEntity | undefined)[], next: readonly (TEntity | undefined)[]): boolean;
1691
1751
  }
1692
1752
 
1693
- export { EOIMCollectionEventType, EOIMComputedEventType, EOIMEventQueueEventType, EOIMEventQueueSchedulerEventType, EOIMIndexEventType, EOIMObjectEventType, type IOIMCarrierResolver, type IOIMComputedEventSource, type IOIMEffectDependency, type IOIMEventQueueSchedulerEvents, type IOIMIndexStore, type IOIMKeyedUpdateEmitter, type IOIMSubscribable, OIMCarrierKeyedEmitter, OIMCollection, OIMCollectionByPkSelector, OIMCollectionByPksSelector, OIMCollectionChangedFields, OIMCollectionIndexFactory, OIMCollectionIndexManualOrderedArrayBased, OIMCollectionOrderedListCommandStream, OIMCollectionSelectors, OIMCollectionStore, OIMCollectionStoreMapDriven, OIMComparatorFactory, OIMComputeRuntime, OIMComputed, OIMDBSettings, OIMDerivedCollectionIndexArrayBased, OIMDerivedCollectionIndexSetBased, OIMEffect, OIMEffectDependencyComputed, OIMEffectDependencyKeyed, OIMEffectDependencyKeyedCollection, OIMEffectDependencyKeyedIndex, OIMEffectDependencyKeyedObject, OIMEntitiesByIndexKeyArrayBasedSelector, OIMEntitiesByIndexKeySetBasedSelector, OIMEntityUpdaterFactory, OIMEventEmitter, OIMEventQueue, OIMEventQueueScheduler, OIMEventQueueSchedulerAnimationFrame, OIMEventQueueSchedulerFactory, OIMEventQueueSchedulerImmediate, OIMEventQueueSchedulerMicrotask, OIMEventQueueSchedulerTimeout, OIMIndex, OIMIndexArrayBased, OIMIndexComparatorFactory, OIMIndexManualArrayBased, OIMIndexManualOrderedArrayBased, OIMIndexManualSetBased, OIMIndexPksByKeyArrayBasedSelector, OIMIndexPksByKeySetBasedSelector, OIMIndexPksByKeysArrayBasedSelector, OIMIndexPksByKeysSetBasedSelector, OIMIndexSetBased, OIMIndexStoreArrayBased, OIMIndexStoreMapDrivenArrayBased, OIMIndexStoreMapDrivenSetBased, OIMIndexStoreSetBased, OIMMap2Keys, OIMObject, OIMObjectStore, OIMObjectStoreMapDriven, OIMObjectStoreRecordDriven, OIMObjectValueByKeySelector, OIMObjectValuesByKeysSelector, OIMOrderedListCommandStream, OIMPkSelectorFactory, OIMReactiveCollection, OIMReactiveCollectionIndexManualArrayBased, OIMReactiveCollectionIndexManualSetBased, OIMReactiveIndex, OIMReactiveIndexArrayBased, OIMReactiveIndexManualArrayBased, OIMReactiveIndexManualSetBased, OIMReactiveIndexSetBased, OIMReactiveObject, OIMSelector, type TOIMAnyEntitySlot, type TOIMCollectionEntitiesSelector, type TOIMCollectionEntitySelector, type TOIMCollectionIndexArrayBasedOptions, type TOIMCollectionIndexSetBasedOptions, type TOIMCollectionKit, type TOIMCollectionOptions, type TOIMCollectionOrderedIndexOptions, type TOIMCollectionOrderedListCommandStreamOptions, type TOIMCollectionUpdatePayload, type TOIMComparator, type TOIMComputedOptions, type TOIMComputedUpdatePayload, type TOIMDerivedCollectionIndexArrayBasedOptions, type TOIMDerivedCollectionIndexSetBasedOptions, type TOIMDerivedEntityComparator, type TOIMDerivedEntityOrderSelector, type TOIMDerivedEntityOrderValue, type TOIMDerivedIndexKeySelector, type TOIMEffectOptions, type TOIMEntitySlot, type TOIMEntitySlotResolver, type TOIMEntityUpdater, type TOIMEventHandler, type TOIMEventQueueOptions, type TOIMIndexComparator, type TOIMIndexKey, type TOIMIndexOptionsArrayBased, type TOIMIndexOptionsSetBased, type TOIMIndexPksUpdatePayload, type TOIMIndexUpdatePayload, type TOIMObjectOptions, type TOIMOrderedListCommand, type TOIMPk, type TOIMPkSelector, type TOIMReactiveCollectionFactoryOptions, type TOIMRelationsArrayIndexOptions, type TOIMRelationsDerivedArrayIndexOptions, type TOIMRelationsDerivedSetIndexOptions, type TOIMRelationsOrderedListOptions, type TOIMRelationsSetIndexOptions, type TOIMSchedulerOptions, type TOIMSchedulerType, type TOIMUpdatePayload, createInPlaceEntityUpdater, createOIMCollectionIndexFactory, createOIMCollectionKit };
1753
+ export { EOIMCollectionEventType, EOIMComputedEventType, EOIMEventQueueEventType, EOIMEventQueueSchedulerEventType, EOIMIndexEventType, EOIMObjectEventType, type IOIMCarrierResolver, type IOIMComputedEventSource, type IOIMEffectDependency, type IOIMEventQueueSchedulerEvents, type IOIMIndexStore, type IOIMKeyedUpdateEmitter, type IOIMSubscribable, OIMCarrierKeyedEmitter, OIMCollection, OIMCollectionByPkSelector, OIMCollectionByPksSelector, OIMCollectionChangedFields, OIMCollectionIndexFactory, OIMCollectionIndexManualOrderedArrayBased, OIMCollectionOrderedListCommandStream, OIMCollectionSelectors, OIMCollectionStore, OIMCollectionStoreMapDriven, OIMComparatorFactory, OIMComputeRuntime, OIMComputed, OIMDBSettings, OIMDerivedCollectionIndexArrayBased, OIMDerivedCollectionIndexSetBased, OIMEffect, OIMEffectDependencyComputed, OIMEffectDependencyKeyed, OIMEffectDependencyKeyedCollection, OIMEffectDependencyKeyedIndex, OIMEffectDependencyKeyedObject, OIMEntitiesByIndexKeyArrayBasedSelector, OIMEntitiesByIndexKeySetBasedSelector, OIMEntityUpdaterFactory, OIMEventEmitter, OIMEventQueue, OIMEventQueueScheduler, OIMEventQueueSchedulerAnimationFrame, OIMEventQueueSchedulerFactory, OIMEventQueueSchedulerImmediate, OIMEventQueueSchedulerMicrotask, OIMEventQueueSchedulerTimeout, OIMIndex, OIMIndexArrayBased, OIMIndexComparatorFactory, OIMIndexManualArrayBased, OIMIndexManualOrderedArrayBased, OIMIndexManualSetBased, OIMIndexPksByKeyArrayBasedSelector, OIMIndexPksByKeySetBasedSelector, OIMIndexPksByKeysArrayBasedSelector, OIMIndexPksByKeysSetBasedSelector, OIMIndexSetBased, OIMIndexStoreArrayBased, OIMIndexStoreMapDrivenArrayBased, OIMIndexStoreMapDrivenSetBased, OIMIndexStoreSetBased, OIMMap2Keys, OIMObject, OIMObjectStore, OIMObjectStoreMapDriven, OIMObjectStoreRecordDriven, OIMObjectValueByKeySelector, OIMObjectValuesByKeysSelector, OIMOrderedListCommandStream, OIMPkSelectorFactory, OIMReactiveCollection, OIMReactiveCollectionIndexManualArrayBased, OIMReactiveCollectionIndexManualSetBased, OIMReactiveIndex, OIMReactiveIndexArrayBased, OIMReactiveIndexManualArrayBased, OIMReactiveIndexManualSetBased, OIMReactiveIndexSetBased, OIMReactiveObject, OIMSelector, type TOIMAnyEntitySlot, type TOIMCollectionEntitiesSelector, type TOIMCollectionEntitySelector, type TOIMCollectionIndexArrayBasedOptions, type TOIMCollectionIndexSetBasedOptions, type TOIMCollectionKit, type TOIMCollectionOptions, type TOIMCollectionOrderedIndexOptions, type TOIMCollectionOrderedListCommandStreamOptions, type TOIMCollectionUpdatePayload, type TOIMComparator, type TOIMComputedOptions, type TOIMComputedUpdatePayload, type TOIMDerivedCollectionIndexArrayBasedOptions, type TOIMDerivedCollectionIndexSetBasedOptions, type TOIMDerivedEntityComparator, type TOIMDerivedEntityOrderSelector, type TOIMDerivedEntityOrderValue, type TOIMDerivedIndexKeySelector, type TOIMEffectOptions, type TOIMEntitySlot, type TOIMEntitySlotResolver, type TOIMEntityUpdater, type TOIMEventHandler, type TOIMEventQueueOptions, type TOIMIndexComparator, type TOIMIndexKey, type TOIMIndexOptionsArrayBased, type TOIMIndexOptionsSetBased, type TOIMIndexPksUpdatePayload, type TOIMIndexUpdatePayload, type TOIMObjectOptions, type TOIMOrderedListCommand, type TOIMPk, type TOIMPkSelector, type TOIMReactiveCollectionFactoryOptions, type TOIMRelationsArrayIndexOptions, type TOIMRelationsDerivedArrayIndexOptions, type TOIMRelationsDerivedSetIndexOptions, type TOIMRelationsOrderedListOptions, type TOIMRelationsSetIndexOptions, type TOIMSchedulerOptions, type TOIMSchedulerType, type TOIMUpdatePayload, createInPlaceEntityUpdater, createMergeEntityUpdater, createOIMCollectionIndexFactory, createOIMCollectionKit };
package/dist/index.js CHANGED
@@ -865,12 +865,15 @@ var OIMCollectionStoreMapDriven = class extends OIMCollectionStore {
865
865
  }
866
866
  };
867
867
 
868
+ // src/core/createMergeEntityUpdater.ts
869
+ function createMergeEntityUpdater() {
870
+ return (draft, prev) => ({ ...prev, ...draft });
871
+ }
872
+
868
873
  // src/core/OIMEntityUpdaterFactory.ts
869
874
  var OIMEntityUpdaterFactory = class {
870
875
  createMergeEntityUpdater() {
871
- return (draft, prev) => {
872
- return { ...prev, ...draft };
873
- };
876
+ return createMergeEntityUpdater();
874
877
  }
875
878
  };
876
879
 
@@ -1189,6 +1192,22 @@ var OIMIndexManualOrderedArrayBased = class extends OIMIndexArrayBased {
1189
1192
  this.emitUpdate([key]);
1190
1193
  return removed;
1191
1194
  }
1195
+ /**
1196
+ * Remove up to `count` consecutive slots starting at `index`.
1197
+ * Returns the number actually removed (clamped to bounds; 0 if nothing).
1198
+ */
1199
+ removeRange(key, index, count) {
1200
+ const list = this.store.getOneByKey(key);
1201
+ if (!list) return 0;
1202
+ if (index < 0 || index >= list.length || count <= 0) return 0;
1203
+ const actual = Math.min(count, list.length - index);
1204
+ list.splice(index, actual);
1205
+ if (list.length === 0) {
1206
+ this.store.removeOneByKey(key);
1207
+ }
1208
+ this.emitUpdate([key]);
1209
+ return actual;
1210
+ }
1192
1211
  move(key, fromIndex, toIndex) {
1193
1212
  const list = this.store.getOneByKey(key);
1194
1213
  if (!list) return void 0;
@@ -1201,6 +1220,32 @@ var OIMIndexManualOrderedArrayBased = class extends OIMIndexArrayBased {
1201
1220
  this.emitUpdate([key]);
1202
1221
  return slot;
1203
1222
  }
1223
+ /** Replace the slot at `index` in place. Returns the safe index, or -1. */
1224
+ setSlotAt(key, index, slot) {
1225
+ const list = this.store.getOneByKey(key);
1226
+ if (!list) return -1;
1227
+ if (index < 0 || index >= list.length) return -1;
1228
+ list[index] = slot;
1229
+ this.emitUpdate([key]);
1230
+ return index;
1231
+ }
1232
+ /**
1233
+ * Move up to `count` consecutive slots from `from` to `to`. `to` is in the
1234
+ * coordinate space *after* the block is extracted (same as single `move`).
1235
+ * Returns the number actually moved (0 if a no-op / out of bounds).
1236
+ */
1237
+ moveRange(key, from, to, count) {
1238
+ const list = this.store.getOneByKey(key);
1239
+ if (!list) return 0;
1240
+ if (from < 0 || from >= list.length || count <= 0) return 0;
1241
+ const actual = Math.min(count, list.length - from);
1242
+ const insertAt = Math.max(0, Math.min(to, list.length - actual));
1243
+ if (insertAt === from) return 0;
1244
+ const block = list.splice(from, actual);
1245
+ list.splice(insertAt, 0, ...block);
1246
+ this.emitUpdate([key]);
1247
+ return actual;
1248
+ }
1204
1249
  resetSlots(key, slots) {
1205
1250
  if (slots.length === 0) {
1206
1251
  if (this.store.getOneByKey(key) !== void 0) {
@@ -1411,15 +1456,15 @@ var OIMUpdateEventEmitter = class {
1411
1456
  const flushingKeys = this.updatedKeys;
1412
1457
  this.updatedKeys = /* @__PURE__ */ new Set();
1413
1458
  if (this.keyHandlers.size === 0 || flushingKeys.size === 0) return;
1414
- if (flushingKeys.size * 2 < this.keyHandlers.size) {
1415
- flushingKeys.forEach((key) => {
1416
- const handlers = this.keyHandlers.get(key);
1459
+ if (flushingKeys.size === this.keyHandlers.size) {
1460
+ this.keyHandlers.forEach((handlers, key) => {
1461
+ if (!flushingKeys.has(key)) return;
1417
1462
  if (!handlers || handlers.size === 0) return;
1418
1463
  this.notifyHandlers(handlers);
1419
1464
  });
1420
1465
  } else {
1421
- this.keyHandlers.forEach((handlers, key) => {
1422
- if (!flushingKeys.has(key)) return;
1466
+ flushingKeys.forEach((key) => {
1467
+ const handlers = this.keyHandlers.get(key);
1423
1468
  if (!handlers || handlers.size === 0) return;
1424
1469
  this.notifyHandlers(handlers);
1425
1470
  });
@@ -1531,7 +1576,7 @@ var OIMOrderedListCommandStream = class {
1531
1576
  if (payload.keys.length === 0) return;
1532
1577
  for (let i = 0; i < payload.keys.length; i++) {
1533
1578
  const key = payload.keys[i];
1534
- this.appendSetCommand(key, this.getSlotsByKey(key));
1579
+ this.appendResetCommand(key, this.getSlotsByKey(key));
1535
1580
  }
1536
1581
  };
1537
1582
  this.commandsEventEmitter = new OIMUpdateEventEmitter(
@@ -1568,12 +1613,7 @@ var OIMOrderedListCommandStream = class {
1568
1613
  pushSlot(key, slot) {
1569
1614
  this.withWrite(() => {
1570
1615
  const index = this.index.pushSlot(key, slot);
1571
- this.appendCommand(key, {
1572
- type: "insert",
1573
- pk: slot.pk,
1574
- slot,
1575
- index
1576
- });
1616
+ this.appendCommand(key, { type: "insert", index, item: slot });
1577
1617
  });
1578
1618
  }
1579
1619
  insertSlotAt(key, index, slot) {
@@ -1581,9 +1621,20 @@ var OIMOrderedListCommandStream = class {
1581
1621
  const safeIndex = this.index.insertSlotAt(key, index, slot);
1582
1622
  this.appendCommand(key, {
1583
1623
  type: "insert",
1584
- pk: slot.pk,
1585
- slot,
1586
- index: safeIndex
1624
+ index: safeIndex,
1625
+ item: slot
1626
+ });
1627
+ });
1628
+ }
1629
+ /** Replace the slot at `index` in place. */
1630
+ setSlotAt(key, index, slot) {
1631
+ this.withWrite(() => {
1632
+ const safeIndex = this.index.setSlotAt(key, index, slot);
1633
+ if (safeIndex < 0) return;
1634
+ this.appendCommand(key, {
1635
+ type: "set",
1636
+ index: safeIndex,
1637
+ item: slot
1587
1638
  });
1588
1639
  });
1589
1640
  }
@@ -1591,12 +1642,15 @@ var OIMOrderedListCommandStream = class {
1591
1642
  this.withWrite(() => {
1592
1643
  const slot = this.index.removeAt(key, index);
1593
1644
  if (slot === void 0) return;
1594
- this.appendCommand(key, {
1595
- type: "remove",
1596
- pk: slot.pk,
1597
- slot,
1598
- index
1599
- });
1645
+ this.appendCommand(key, { type: "remove", index });
1646
+ });
1647
+ }
1648
+ /** Remove `count` consecutive elements from `index`; emits `remove` with `count`. */
1649
+ removeRange(key, index, count) {
1650
+ this.withWrite(() => {
1651
+ const removed = this.index.removeRange(key, index, count);
1652
+ if (removed <= 0) return;
1653
+ this.appendCommand(key, { type: "remove", index, count: removed });
1600
1654
  });
1601
1655
  }
1602
1656
  move(key, fromIndex, toIndex) {
@@ -1605,17 +1659,26 @@ var OIMOrderedListCommandStream = class {
1605
1659
  if (slot === void 0) return;
1606
1660
  this.appendCommand(key, {
1607
1661
  type: "move",
1608
- pk: slot.pk,
1609
- slot,
1610
- fromIndex,
1611
- toIndex
1662
+ from: fromIndex,
1663
+ to: toIndex
1612
1664
  });
1613
1665
  });
1614
1666
  }
1667
+ /**
1668
+ * Move `count` consecutive elements from `from` to `to`; emits `move` with
1669
+ * `count`. `to` is in the post-extraction coordinate space (as single `move`).
1670
+ */
1671
+ moveRange(key, from, to, count) {
1672
+ this.withWrite(() => {
1673
+ const moved = this.index.moveRange(key, from, to, count);
1674
+ if (moved <= 0) return;
1675
+ this.appendCommand(key, { type: "move", from, to, count: moved });
1676
+ });
1677
+ }
1615
1678
  setSlots(key, slots) {
1616
1679
  this.withWrite(() => {
1617
1680
  this.index.resetSlots(key, slots);
1618
- this.appendSetCommand(key, slots);
1681
+ this.appendResetCommand(key, slots);
1619
1682
  });
1620
1683
  }
1621
1684
  getBufferedCommands(key) {
@@ -1659,26 +1722,21 @@ var OIMOrderedListCommandStream = class {
1659
1722
  cmds = [];
1660
1723
  this.commandsByKey.set(key, cmds);
1661
1724
  }
1662
- if (cmd.type === "set") {
1725
+ if (cmd.type === "reset") {
1663
1726
  cmds.length = 0;
1664
1727
  cmds.push(cmd);
1665
- } else if (cmds.length > 0 && cmds[0].type === "set") {
1666
- const slots = this.getSlotsByKey(key);
1667
- cmds[0] = this.createSetCommand(slots);
1728
+ } else if (cmds.length > 0 && cmds[0].type === "reset") {
1729
+ cmds[0] = this.createResetCommand(this.getSlotsByKey(key));
1668
1730
  } else {
1669
1731
  cmds.push(cmd);
1670
1732
  }
1671
1733
  this.commandsEventEmitter.markUpdatedKeys([key]);
1672
1734
  }
1673
- appendSetCommand(key, slots) {
1674
- this.appendCommand(key, this.createSetCommand(slots));
1735
+ appendResetCommand(key, slots) {
1736
+ this.appendCommand(key, this.createResetCommand(slots));
1675
1737
  }
1676
- createSetCommand(slots) {
1677
- return {
1678
- type: "set",
1679
- pks: slots.map((slot) => slot.pk),
1680
- slots
1681
- };
1738
+ createResetCommand(slots) {
1739
+ return { type: "reset", items: slots };
1682
1740
  }
1683
1741
  };
1684
1742
 
@@ -1695,12 +1753,7 @@ var OIMCollectionOrderedListCommandStream = class extends OIMOrderedListCommandS
1695
1753
  this.withWrite(() => {
1696
1754
  const slot = this.resolvePk(pk);
1697
1755
  const index = this.index.pushSlot(key, slot);
1698
- this.appendCommand(key, {
1699
- type: "insert",
1700
- pk,
1701
- slot,
1702
- index
1703
- });
1756
+ this.appendCommand(key, { type: "insert", index, item: slot });
1704
1757
  });
1705
1758
  }
1706
1759
  insertAt(key, index, pk) {
@@ -1709,9 +1762,21 @@ var OIMCollectionOrderedListCommandStream = class extends OIMOrderedListCommandS
1709
1762
  const safeIndex = this.index.insertSlotAt(key, index, slot);
1710
1763
  this.appendCommand(key, {
1711
1764
  type: "insert",
1712
- pk,
1713
- slot,
1714
- index: safeIndex
1765
+ index: safeIndex,
1766
+ item: slot
1767
+ });
1768
+ });
1769
+ }
1770
+ /** Replace the element at `index` with the slot for `pk`, in place. */
1771
+ setAt(key, index, pk) {
1772
+ this.withWrite(() => {
1773
+ const slot = this.resolvePk(pk);
1774
+ const safeIndex = this.index.setSlotAt(key, index, slot);
1775
+ if (safeIndex < 0) return;
1776
+ this.appendCommand(key, {
1777
+ type: "set",
1778
+ index: safeIndex,
1779
+ item: slot
1715
1780
  });
1716
1781
  });
1717
1782
  }
@@ -1719,7 +1784,7 @@ var OIMCollectionOrderedListCommandStream = class extends OIMOrderedListCommandS
1719
1784
  this.withWrite(() => {
1720
1785
  const slots = pks.map((pk) => this.resolvePk(pk));
1721
1786
  this.index.resetSlots(key, slots);
1722
- this.appendSetCommand(key, slots);
1787
+ this.appendResetCommand(key, slots);
1723
1788
  });
1724
1789
  }
1725
1790
  getSlotsByKey(key) {
@@ -3298,20 +3363,27 @@ var OIMCollectionChangedFields = class {
3298
3363
  constructor(queue, collection, opts) {
3299
3364
  this.isInWrite = false;
3300
3365
  this.lastEntitySnapshotByPk = /* @__PURE__ */ new Map();
3366
+ // PKs written through this wrapper during the current flush. Their fields are
3367
+ // already tracked accurately via trackPatch, so the flush-time external-write
3368
+ // handler skips them — no redundant diff + snapshot re-clone. Cleared per flush.
3369
+ this.writtenThroughThisFlush = /* @__PURE__ */ new Set();
3301
3370
  this.changedFieldsByPk = /* @__PURE__ */ new Map();
3302
3371
  this.changedPksByField = /* @__PURE__ */ new Map();
3303
3372
  this.onAfterFlush = () => {
3304
3373
  this.clear();
3374
+ this.writtenThroughThisFlush.clear();
3305
3375
  };
3306
3376
  this.onCollectionUpdate = (payload) => {
3307
3377
  if (this.isInWrite) return;
3308
3378
  if (payload.pks.length === 0) return;
3309
3379
  for (let i = 0; i < payload.pks.length; i++) {
3310
3380
  const pk = payload.pks[i];
3381
+ if (this.writtenThroughThisFlush.has(pk)) continue;
3311
3382
  this.trackExternalMutationByPk(pk);
3312
3383
  }
3313
3384
  };
3314
3385
  this.collection = collection;
3386
+ this.detectExternalMutations = opts?.detectExternalMutations !== false;
3315
3387
  this.selectPk = opts?.selectPk ?? (() => {
3316
3388
  throw new Error(
3317
3389
  `[OIMCollectionChangedFields]: selectPk was not provided. Use upsertOneByPk(...) or pass opts.selectPk.`
@@ -3326,15 +3398,17 @@ var OIMCollectionChangedFields = class {
3326
3398
  0 /* AFTER_FLUSH */,
3327
3399
  this.onAfterFlush
3328
3400
  );
3329
- const maybeReactive = collection;
3330
- this.unsubscribeFromCollectionEmitter = maybeReactive.subscribeOnAnyUpdate ? maybeReactive.subscribeOnAnyUpdate((pks) => {
3331
- this.onCollectionUpdate({
3332
- pks
3333
- });
3334
- }) : collection.emitter.on(
3335
- 0 /* UPDATE */,
3336
- this.onCollectionUpdate
3337
- );
3401
+ if (this.detectExternalMutations) {
3402
+ const maybeReactive = collection;
3403
+ this.unsubscribeFromCollectionEmitter = maybeReactive.subscribeOnAnyUpdate ? maybeReactive.subscribeOnAnyUpdate((pks) => {
3404
+ this.onCollectionUpdate({
3405
+ pks
3406
+ });
3407
+ }) : collection.emitter.on(
3408
+ 0 /* UPDATE */,
3409
+ this.onCollectionUpdate
3410
+ );
3411
+ }
3338
3412
  }
3339
3413
  upsertOneByPk(pk, patch) {
3340
3414
  const slot = this.withWrite(() => {
@@ -3390,7 +3464,7 @@ var OIMCollectionChangedFields = class {
3390
3464
  this.changedPksEventEmitter.destroy();
3391
3465
  this.changedFieldsEventEmitter.destroy();
3392
3466
  this.unsubscribeAfterFlush();
3393
- this.unsubscribeFromCollectionEmitter();
3467
+ this.unsubscribeFromCollectionEmitter?.();
3394
3468
  this.clear();
3395
3469
  }
3396
3470
  withWrite(fn) {
@@ -3432,6 +3506,8 @@ var OIMCollectionChangedFields = class {
3432
3506
  return changed;
3433
3507
  }
3434
3508
  updateEntitySnapshot(pk) {
3509
+ if (!this.detectExternalMutations) return;
3510
+ this.writtenThroughThisFlush.add(pk);
3435
3511
  const entity = this.collection.getOneByPk(pk);
3436
3512
  if (!entity) {
3437
3513
  this.lastEntitySnapshotByPk.delete(pk);
@@ -4115,6 +4191,7 @@ export {
4115
4191
  OIMReactiveObject,
4116
4192
  OIMSelector,
4117
4193
  createInPlaceEntityUpdater,
4194
+ createMergeEntityUpdater,
4118
4195
  createOIMCollectionIndexFactory,
4119
4196
  createOIMCollectionKit
4120
4197
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oimdb/core",
3
- "version": "3.3.0",
3
+ "version": "3.4.0",
4
4
  "description": "Core in-memory data library with type-safe indices, reactive subscriptions, and event processing",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",