@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/dist/index.js CHANGED
@@ -545,7 +545,7 @@ var OIMCarrierKeyedEmitter = class {
545
545
  }
546
546
  destroy() {
547
547
  if (this.isFlushEnqueued) {
548
- this.queue.cancelTask(this.onFlush);
548
+ this.queue.cancel(this.onFlush);
549
549
  this.isFlushEnqueued = false;
550
550
  }
551
551
  this.subscribedCarriers.forEach((carrier) => {
@@ -569,7 +569,7 @@ var OIMCarrierKeyedEmitter = class {
569
569
  scheduleFlush() {
570
570
  if (this.isFlushEnqueued) return;
571
571
  this.isFlushEnqueued = true;
572
- this.queue.enqueueTask(this.onFlush);
572
+ this.queue.enqueue(this.onFlush);
573
573
  }
574
574
  notify(carrier) {
575
575
  const subscribers = carrier.subscribers;
@@ -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
 
@@ -1134,12 +1137,12 @@ var OIMReactiveCollection = class extends OIMCollection {
1134
1137
  ensureAnyUpdateEnqueued() {
1135
1138
  if (this.isAnyUpdateScheduled) return;
1136
1139
  this.isAnyUpdateScheduled = true;
1137
- this.queue.enqueueTask(this.runAnyUpdate);
1140
+ this.queue.enqueue(this.runAnyUpdate);
1138
1141
  }
1139
1142
  destroy() {
1140
1143
  this.updateEmitter.destroy();
1141
1144
  if (this.isAnyUpdateScheduled) {
1142
- this.queue.cancelTask(this.runAnyUpdate);
1145
+ this.queue.cancel(this.runAnyUpdate);
1143
1146
  this.isAnyUpdateScheduled = false;
1144
1147
  }
1145
1148
  this.anyUpdateHandlers.clear();
@@ -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) {
@@ -1334,7 +1379,7 @@ var OIMUpdateEventEmitter = class {
1334
1379
  }
1335
1380
  if (this.isFlushEnqueued) return;
1336
1381
  this.isFlushEnqueued = true;
1337
- this.queue.enqueueTask(this.onFlush);
1382
+ this.queue.enqueue(this.onFlush);
1338
1383
  }
1339
1384
  markUpdatedKey(key) {
1340
1385
  if (this.deliveryMode === "queue" && this.queue.isInFlush) {
@@ -1356,7 +1401,7 @@ var OIMUpdateEventEmitter = class {
1356
1401
  }
1357
1402
  if (this.isFlushEnqueued) return;
1358
1403
  this.isFlushEnqueued = true;
1359
- this.queue.enqueueTask(this.onFlush);
1404
+ this.queue.enqueue(this.onFlush);
1360
1405
  }
1361
1406
  markAllUpdated() {
1362
1407
  if (this.deliveryMode === "queue" && this.queue.isInFlush) {
@@ -1378,7 +1423,7 @@ var OIMUpdateEventEmitter = class {
1378
1423
  }
1379
1424
  if (this.isFlushEnqueued) return;
1380
1425
  this.isFlushEnqueued = true;
1381
- this.queue.enqueueTask(this.onFlush);
1426
+ this.queue.enqueue(this.onFlush);
1382
1427
  }
1383
1428
  deliverImmediate() {
1384
1429
  this.runSinglePass();
@@ -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
  });
@@ -1427,7 +1472,7 @@ var OIMUpdateEventEmitter = class {
1427
1472
  }
1428
1473
  destroy() {
1429
1474
  if (this.isFlushEnqueued) {
1430
- this.queue.cancelTask(this.onFlush);
1475
+ this.queue.cancel(this.onFlush);
1431
1476
  this.isFlushEnqueued = false;
1432
1477
  }
1433
1478
  if (this.unsubscribeAfterFlushDelivery) {
@@ -1487,7 +1532,7 @@ var OIMUpdateEventEmitter = class {
1487
1532
  if (this.keyHandlers.size === 0) {
1488
1533
  this.updatedKeys.clear();
1489
1534
  if (this.isFlushEnqueued) {
1490
- this.queue.cancelTask(this.onFlush);
1535
+ this.queue.cancel(this.onFlush);
1491
1536
  }
1492
1537
  this.isFlushEnqueued = false;
1493
1538
  }
@@ -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) {
@@ -2635,48 +2700,23 @@ var OIMEventQueue = class {
2635
2700
  }
2636
2701
  }
2637
2702
  /**
2638
- * Enqueue a one-shot task to be executed on the next flush.
2639
- * Returns a dequeue function that can be called to cancel the task before it runs.
2703
+ * Enqueue a one-shot task to run on the next flush. The function is stored
2704
+ * directly (no wrapper, no returned closure) and cancelled via {@link cancel}
2705
+ * by passing the same reference. Enqueuing the same reference twice is
2706
+ * idempotent — it is Set-deduped and runs once per flush.
2640
2707
  */
2641
2708
  enqueue(fn) {
2642
- let isActive = true;
2643
- const task = () => {
2644
- if (!isActive) return;
2645
- fn();
2646
- };
2647
- this.tasks.add(task);
2648
- this.ensureScheduled();
2649
- return () => {
2650
- if (!isActive) return;
2651
- isActive = false;
2652
- this.tasks.delete(task);
2653
- };
2654
- }
2655
- /**
2656
- * Allocation-free enqueue for a **stable** function reference (e.g. a bound
2657
- * `onFlush` method). The function is stored directly — no wrapper, no
2658
- * returned closure — and cancelled via {@link cancelTask}. Enqueuing the
2659
- * same reference twice is idempotent (Set-deduped); callers that may run a
2660
- * task more than once per flush should use {@link enqueue} instead.
2661
- */
2662
- enqueueTask(fn) {
2663
2709
  this.tasks.add(fn);
2664
2710
  this.ensureScheduled();
2665
2711
  }
2666
- /** Cancel a task scheduled via {@link enqueueTask}; safe to call mid-flush. */
2667
- cancelTask(fn) {
2712
+ /** Cancel a previously enqueued task by its reference; safe to call mid-flush. */
2713
+ cancel(fn) {
2668
2714
  this.tasks.delete(fn);
2669
2715
  this.flushing?.delete(fn);
2670
2716
  }
2671
2717
  get isInFlush() {
2672
2718
  return this.isFlushing;
2673
2719
  }
2674
- /**
2675
- * Dequeue (cancel) a previously enqueued task by its dequeue function.
2676
- */
2677
- dequeue(dequeueFn) {
2678
- dequeueFn();
2679
- }
2680
2720
  /**
2681
2721
  * Execute all pending tasks.
2682
2722
  *
@@ -3323,20 +3363,27 @@ var OIMCollectionChangedFields = class {
3323
3363
  constructor(queue, collection, opts) {
3324
3364
  this.isInWrite = false;
3325
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();
3326
3370
  this.changedFieldsByPk = /* @__PURE__ */ new Map();
3327
3371
  this.changedPksByField = /* @__PURE__ */ new Map();
3328
3372
  this.onAfterFlush = () => {
3329
3373
  this.clear();
3374
+ this.writtenThroughThisFlush.clear();
3330
3375
  };
3331
3376
  this.onCollectionUpdate = (payload) => {
3332
3377
  if (this.isInWrite) return;
3333
3378
  if (payload.pks.length === 0) return;
3334
3379
  for (let i = 0; i < payload.pks.length; i++) {
3335
3380
  const pk = payload.pks[i];
3381
+ if (this.writtenThroughThisFlush.has(pk)) continue;
3336
3382
  this.trackExternalMutationByPk(pk);
3337
3383
  }
3338
3384
  };
3339
3385
  this.collection = collection;
3386
+ this.detectExternalMutations = opts?.detectExternalMutations !== false;
3340
3387
  this.selectPk = opts?.selectPk ?? (() => {
3341
3388
  throw new Error(
3342
3389
  `[OIMCollectionChangedFields]: selectPk was not provided. Use upsertOneByPk(...) or pass opts.selectPk.`
@@ -3351,15 +3398,17 @@ var OIMCollectionChangedFields = class {
3351
3398
  0 /* AFTER_FLUSH */,
3352
3399
  this.onAfterFlush
3353
3400
  );
3354
- const maybeReactive = collection;
3355
- this.unsubscribeFromCollectionEmitter = maybeReactive.subscribeOnAnyUpdate ? maybeReactive.subscribeOnAnyUpdate((pks) => {
3356
- this.onCollectionUpdate({
3357
- pks
3358
- });
3359
- }) : collection.emitter.on(
3360
- 0 /* UPDATE */,
3361
- this.onCollectionUpdate
3362
- );
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
+ }
3363
3412
  }
3364
3413
  upsertOneByPk(pk, patch) {
3365
3414
  const slot = this.withWrite(() => {
@@ -3415,7 +3464,7 @@ var OIMCollectionChangedFields = class {
3415
3464
  this.changedPksEventEmitter.destroy();
3416
3465
  this.changedFieldsEventEmitter.destroy();
3417
3466
  this.unsubscribeAfterFlush();
3418
- this.unsubscribeFromCollectionEmitter();
3467
+ this.unsubscribeFromCollectionEmitter?.();
3419
3468
  this.clear();
3420
3469
  }
3421
3470
  withWrite(fn) {
@@ -3457,6 +3506,8 @@ var OIMCollectionChangedFields = class {
3457
3506
  return changed;
3458
3507
  }
3459
3508
  updateEntitySnapshot(pk) {
3509
+ if (!this.detectExternalMutations) return;
3510
+ this.writtenThroughThisFlush.add(pk);
3460
3511
  const entity = this.collection.getOneByPk(pk);
3461
3512
  if (!entity) {
3462
3513
  this.lastEntitySnapshotByPk.delete(pk);
@@ -4140,6 +4191,7 @@ export {
4140
4191
  OIMReactiveObject,
4141
4192
  OIMSelector,
4142
4193
  createInPlaceEntityUpdater,
4194
+ createMergeEntityUpdater,
4143
4195
  createOIMCollectionIndexFactory,
4144
4196
  createOIMCollectionKit
4145
4197
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oimdb/core",
3
- "version": "3.2.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",