@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 +58 -16
- package/dist/index.cjs +154 -101
- package/dist/index.d.cts +90 -40
- package/dist/index.d.ts +90 -40
- package/dist/index.js +153 -101
- package/package.json +1 -1
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.
|
|
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.
|
|
325
|
+
// Remove command.count ?? 1 elements starting at command.index
|
|
325
326
|
break;
|
|
326
327
|
case 'move':
|
|
327
|
-
// Move command.
|
|
328
|
+
// Move command.count ?? 1 elements from command.from to command.to
|
|
328
329
|
break;
|
|
329
330
|
case 'set':
|
|
330
|
-
// Replace the
|
|
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
|
|
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
|
|
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
|
|
1540
|
-
- `pushSlot(key: TKey, slot:
|
|
1541
|
-
- `insertSlotAt(key: TKey, index: number, slot:
|
|
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
|
-
- `
|
|
1545
|
-
- `
|
|
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
|
|
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.
|
|
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.
|
|
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 (
|
|
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.
|
|
1243
|
+
this.queue.enqueue(this.runAnyUpdate);
|
|
1240
1244
|
}
|
|
1241
1245
|
destroy() {
|
|
1242
1246
|
this.updateEmitter.destroy();
|
|
1243
1247
|
if (this.isAnyUpdateScheduled) {
|
|
1244
|
-
this.queue.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
1517
|
-
|
|
1518
|
-
|
|
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
|
-
|
|
1524
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
1687
|
-
slot
|
|
1688
|
-
|
|
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
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
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
|
-
|
|
1711
|
-
|
|
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.
|
|
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 === "
|
|
1828
|
+
if (cmd.type === "reset") {
|
|
1765
1829
|
cmds.length = 0;
|
|
1766
1830
|
cmds.push(cmd);
|
|
1767
|
-
} else if (cmds.length > 0 && cmds[0].type === "
|
|
1768
|
-
|
|
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
|
-
|
|
1776
|
-
this.appendCommand(key, this.
|
|
1838
|
+
appendResetCommand(key, slots) {
|
|
1839
|
+
this.appendCommand(key, this.createResetCommand(slots));
|
|
1777
1840
|
}
|
|
1778
|
-
|
|
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
|
-
|
|
1815
|
-
slot
|
|
1816
|
-
|
|
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.
|
|
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
|
|
2741
|
-
*
|
|
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
|
|
2769
|
-
|
|
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
|
-
|
|
3457
|
-
|
|
3458
|
-
this.
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
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
|
});
|