@oimdb/core 3.1.0 → 3.3.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 +28 -3
- package/dist/index.cjs +71 -54
- package/dist/index.d.cts +26 -11
- package/dist/index.d.ts +26 -11
- package/dist/index.js +71 -54
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -546,6 +546,31 @@ users.upsertOne({ id: 'user1', name: 'John' });
|
|
|
546
546
|
users.upsertOne({ id: 'user1', email: 'john@example.com' }); // Merges with existing
|
|
547
547
|
```
|
|
548
548
|
|
|
549
|
+
#### Built-in updaters
|
|
550
|
+
|
|
551
|
+
Two updater strategies ship as factories — pass the result as `updateEntity`:
|
|
552
|
+
|
|
553
|
+
```typescript
|
|
554
|
+
import { createMergeEntityUpdater, createInPlaceEntityUpdater } from '@oimdb/core';
|
|
555
|
+
|
|
556
|
+
// Default: immutable shallow merge — `{ ...prev, ...draft }`. Each update produces
|
|
557
|
+
// a NEW entity object (required for React's Object.is / useSyncExternalStore).
|
|
558
|
+
const a = new OIMReactiveCollection<User, string>(queue, {
|
|
559
|
+
selectPk: (u) => u.id,
|
|
560
|
+
updateEntity: createMergeEntityUpdater(),
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
// In-place: `Object.assign(prev, draft)` — mutates the existing object, no
|
|
564
|
+
// per-update allocation. The entity reference is STABLE across changes, so it
|
|
565
|
+
// only works with subscription-driven readers (e.g. the `*Signal` hooks in
|
|
566
|
+
// @oimdb/react), not with Object.is/React.memo diffing. Fastest on the data
|
|
567
|
+
// layer for update-heavy workloads.
|
|
568
|
+
const b = new OIMReactiveCollection<User, string>(queue, {
|
|
569
|
+
selectPk: (u) => u.id,
|
|
570
|
+
updateEntity: createInPlaceEntityUpdater(),
|
|
571
|
+
});
|
|
572
|
+
```
|
|
573
|
+
|
|
549
574
|
### Event Coalescing and Update Subscriptions
|
|
550
575
|
|
|
551
576
|
```typescript
|
|
@@ -1509,7 +1534,7 @@ new OIMIndexManualOrderedArrayBased<TKey, TPk>()
|
|
|
1509
1534
|
**Query:**
|
|
1510
1535
|
- `getPksByKey(key: TKey): TPk[]` - Returns the ordered primary-key array
|
|
1511
1536
|
- `getSlotsByKey(key: TKey): readonly TOIMAnyEntitySlot<TPk>[]` - Returns the stored ordered slots
|
|
1512
|
-
- `getEntitiesByKey<TEntity>(key: TKey): TEntity[]` - Returns
|
|
1537
|
+
- `getEntitiesByKey<TEntity>(key: TKey): (TEntity | undefined)[]` - Returns entities from stored slots, aligned 1:1 with the pks (holes are `undefined`)
|
|
1513
1538
|
|
|
1514
1539
|
#### `OIMCollectionIndexManualOrderedArrayBased<TKey, TPk, TEntity>`
|
|
1515
1540
|
Collection-bound ordered Array-based index. Use this when ordered PK writes should resolve to canonical slots from a collection.
|
|
@@ -1518,7 +1543,7 @@ Collection-bound ordered Array-based index. Use this when ordered PK writes shou
|
|
|
1518
1543
|
- `push(key: TKey, pk: TPk): number` - Append a PK as its canonical slot
|
|
1519
1544
|
- `insertAt(key: TKey, index: number, pk: TPk): number` - Insert a PK as its canonical slot
|
|
1520
1545
|
- `reset(key: TKey, pks: readonly TPk[]): void` - Replace the whole ordered list from PKs
|
|
1521
|
-
- `getEntitiesByKey(key: TKey): TEntity[]` - Returns entities from canonical slots
|
|
1546
|
+
- `getEntitiesByKey(key: TKey): (TEntity | undefined)[]` - Returns entities from canonical slots (holes are `undefined`)
|
|
1522
1547
|
|
|
1523
1548
|
#### `OIMOrderedListCommandStream<TKey, TPk, TEntity>`
|
|
1524
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`.
|
|
@@ -1545,7 +1570,7 @@ new OIMOrderedListCommandStream(
|
|
|
1545
1570
|
- `getBufferedCommands(key: TKey): readonly TOIMOrderedListCommand<TPk, TEntity>[]` - Peek at buffered commands without clearing them
|
|
1546
1571
|
- `getPksByKey(key: TKey): readonly TPk[]` - Read the current ordered list
|
|
1547
1572
|
- `getSlotsByKey(key: TKey): readonly TOIMEntitySlot<TEntity, TPk>[]` - Read current ordered slots
|
|
1548
|
-
- `getEntitiesByKey(key: TKey): TEntity[]` - Read current ordered entities
|
|
1573
|
+
- `getEntitiesByKey(key: TKey): (TEntity | undefined)[]` - Read current ordered entities (holes are `undefined`)
|
|
1549
1574
|
- `clear(key?: TKey): void` - Clear all keys or a specific key
|
|
1550
1575
|
- `destroy(): void` - Dispose subscriptions and clear state
|
|
1551
1576
|
|
package/dist/index.cjs
CHANGED
|
@@ -537,7 +537,6 @@ var OIMCarrierKeyedEmitter = class {
|
|
|
537
537
|
this.isFlushEnqueued = false;
|
|
538
538
|
this.onFlush = () => {
|
|
539
539
|
this.isFlushEnqueued = false;
|
|
540
|
-
this.dequeueFlush = void 0;
|
|
541
540
|
if (this.dirtyCarriers.length === 0) return;
|
|
542
541
|
const flushing = this.dirtyCarriers;
|
|
543
542
|
this.dirtyCarriers = [];
|
|
@@ -647,9 +646,9 @@ var OIMCarrierKeyedEmitter = class {
|
|
|
647
646
|
};
|
|
648
647
|
}
|
|
649
648
|
destroy() {
|
|
650
|
-
if (this.
|
|
651
|
-
this.
|
|
652
|
-
this.
|
|
649
|
+
if (this.isFlushEnqueued) {
|
|
650
|
+
this.queue.cancel(this.onFlush);
|
|
651
|
+
this.isFlushEnqueued = false;
|
|
653
652
|
}
|
|
654
653
|
this.subscribedCarriers.forEach((carrier) => {
|
|
655
654
|
carrier.subscribers?.clear();
|
|
@@ -672,7 +671,7 @@ var OIMCarrierKeyedEmitter = class {
|
|
|
672
671
|
scheduleFlush() {
|
|
673
672
|
if (this.isFlushEnqueued) return;
|
|
674
673
|
this.isFlushEnqueued = true;
|
|
675
|
-
this.
|
|
674
|
+
this.queue.enqueue(this.onFlush);
|
|
676
675
|
}
|
|
677
676
|
notify(carrier) {
|
|
678
677
|
const subscribers = carrier.subscribers;
|
|
@@ -1098,7 +1097,6 @@ var OIMReactiveCollection = class extends OIMCollection {
|
|
|
1098
1097
|
this.runAnyUpdate = () => {
|
|
1099
1098
|
if (!this.isAnyUpdateScheduled) return;
|
|
1100
1099
|
this.isAnyUpdateScheduled = false;
|
|
1101
|
-
this.dequeueAnyUpdate = void 0;
|
|
1102
1100
|
const pks = this.isAnyUpdateClearPending ? [] : Array.from(this.pendingAnyUpdatePks);
|
|
1103
1101
|
this.pendingAnyUpdatePks.clear();
|
|
1104
1102
|
this.isAnyUpdateClearPending = false;
|
|
@@ -1238,13 +1236,13 @@ var OIMReactiveCollection = class extends OIMCollection {
|
|
|
1238
1236
|
ensureAnyUpdateEnqueued() {
|
|
1239
1237
|
if (this.isAnyUpdateScheduled) return;
|
|
1240
1238
|
this.isAnyUpdateScheduled = true;
|
|
1241
|
-
this.
|
|
1239
|
+
this.queue.enqueue(this.runAnyUpdate);
|
|
1242
1240
|
}
|
|
1243
1241
|
destroy() {
|
|
1244
1242
|
this.updateEmitter.destroy();
|
|
1245
|
-
if (this.
|
|
1246
|
-
this.
|
|
1247
|
-
this.
|
|
1243
|
+
if (this.isAnyUpdateScheduled) {
|
|
1244
|
+
this.queue.cancel(this.runAnyUpdate);
|
|
1245
|
+
this.isAnyUpdateScheduled = false;
|
|
1248
1246
|
}
|
|
1249
1247
|
this.anyUpdateHandlers.clear();
|
|
1250
1248
|
this.pendingAnyUpdatePks.clear();
|
|
@@ -1386,7 +1384,6 @@ var OIMUpdateEventEmitter = class {
|
|
|
1386
1384
|
// Stable handler reference to avoid allocating a new closure on every HAS_CHANGES.
|
|
1387
1385
|
this.onFlush = () => {
|
|
1388
1386
|
this.isFlushEnqueued = false;
|
|
1389
|
-
this.dequeueFlush = void 0;
|
|
1390
1387
|
if (this.keyHandlers.size === 0) {
|
|
1391
1388
|
this.updatedKeys.clear();
|
|
1392
1389
|
return;
|
|
@@ -1439,7 +1436,7 @@ var OIMUpdateEventEmitter = class {
|
|
|
1439
1436
|
}
|
|
1440
1437
|
if (this.isFlushEnqueued) return;
|
|
1441
1438
|
this.isFlushEnqueued = true;
|
|
1442
|
-
this.
|
|
1439
|
+
this.queue.enqueue(this.onFlush);
|
|
1443
1440
|
}
|
|
1444
1441
|
markUpdatedKey(key) {
|
|
1445
1442
|
if (this.deliveryMode === "queue" && this.queue.isInFlush) {
|
|
@@ -1461,7 +1458,7 @@ var OIMUpdateEventEmitter = class {
|
|
|
1461
1458
|
}
|
|
1462
1459
|
if (this.isFlushEnqueued) return;
|
|
1463
1460
|
this.isFlushEnqueued = true;
|
|
1464
|
-
this.
|
|
1461
|
+
this.queue.enqueue(this.onFlush);
|
|
1465
1462
|
}
|
|
1466
1463
|
markAllUpdated() {
|
|
1467
1464
|
if (this.deliveryMode === "queue" && this.queue.isInFlush) {
|
|
@@ -1483,7 +1480,7 @@ var OIMUpdateEventEmitter = class {
|
|
|
1483
1480
|
}
|
|
1484
1481
|
if (this.isFlushEnqueued) return;
|
|
1485
1482
|
this.isFlushEnqueued = true;
|
|
1486
|
-
this.
|
|
1483
|
+
this.queue.enqueue(this.onFlush);
|
|
1487
1484
|
}
|
|
1488
1485
|
deliverImmediate() {
|
|
1489
1486
|
this.runSinglePass();
|
|
@@ -1531,9 +1528,9 @@ var OIMUpdateEventEmitter = class {
|
|
|
1531
1528
|
}
|
|
1532
1529
|
}
|
|
1533
1530
|
destroy() {
|
|
1534
|
-
if (this.
|
|
1535
|
-
this.
|
|
1536
|
-
this.
|
|
1531
|
+
if (this.isFlushEnqueued) {
|
|
1532
|
+
this.queue.cancel(this.onFlush);
|
|
1533
|
+
this.isFlushEnqueued = false;
|
|
1537
1534
|
}
|
|
1538
1535
|
if (this.unsubscribeAfterFlushDelivery) {
|
|
1539
1536
|
this.unsubscribeAfterFlushDelivery();
|
|
@@ -1591,9 +1588,8 @@ var OIMUpdateEventEmitter = class {
|
|
|
1591
1588
|
}
|
|
1592
1589
|
if (this.keyHandlers.size === 0) {
|
|
1593
1590
|
this.updatedKeys.clear();
|
|
1594
|
-
if (this.
|
|
1595
|
-
this.
|
|
1596
|
-
this.dequeueFlush = void 0;
|
|
1591
|
+
if (this.isFlushEnqueued) {
|
|
1592
|
+
this.queue.cancel(this.onFlush);
|
|
1597
1593
|
}
|
|
1598
1594
|
this.isFlushEnqueued = false;
|
|
1599
1595
|
}
|
|
@@ -2551,17 +2547,15 @@ var OIMObject = class {
|
|
|
2551
2547
|
}
|
|
2552
2548
|
setProperty(key, value) {
|
|
2553
2549
|
this.store.setProperty(key, value);
|
|
2554
|
-
this.
|
|
2550
|
+
this.onUpdatedKey(key);
|
|
2555
2551
|
}
|
|
2556
2552
|
merge(draft) {
|
|
2557
2553
|
this.store.merge(draft);
|
|
2558
|
-
this.
|
|
2559
|
-
keys: Object.keys(draft)
|
|
2560
|
-
});
|
|
2554
|
+
this.onUpdatedKeys(Object.keys(draft));
|
|
2561
2555
|
}
|
|
2562
2556
|
delete(key) {
|
|
2563
2557
|
this.store.delete(key);
|
|
2564
|
-
this.
|
|
2558
|
+
this.onUpdatedKey(key);
|
|
2565
2559
|
}
|
|
2566
2560
|
get(key) {
|
|
2567
2561
|
return this.store.get(key);
|
|
@@ -2571,7 +2565,24 @@ var OIMObject = class {
|
|
|
2571
2565
|
}
|
|
2572
2566
|
clear() {
|
|
2573
2567
|
this.store.clear();
|
|
2574
|
-
this.
|
|
2568
|
+
this.onUpdatedKeys([]);
|
|
2569
|
+
}
|
|
2570
|
+
/**
|
|
2571
|
+
* Write fan-out hooks. Split single-key vs multi-key so a reactive subclass
|
|
2572
|
+
* can mark its keyed emitter on the hot single-property path without
|
|
2573
|
+
* allocating a `{ keys: [key] }` payload. The base only fires the general
|
|
2574
|
+
* UPDATE event, and only when something is actually subscribed to it — so an
|
|
2575
|
+
* unobserved write skips the payload allocation entirely.
|
|
2576
|
+
*/
|
|
2577
|
+
onUpdatedKey(key) {
|
|
2578
|
+
if (this.emitter.hasHandlers(0 /* UPDATE */)) {
|
|
2579
|
+
this.emitter.emit(0 /* UPDATE */, { keys: [key] });
|
|
2580
|
+
}
|
|
2581
|
+
}
|
|
2582
|
+
onUpdatedKeys(keys) {
|
|
2583
|
+
if (this.emitter.hasHandlers(0 /* UPDATE */)) {
|
|
2584
|
+
this.emitter.emit(0 /* UPDATE */, { keys });
|
|
2585
|
+
}
|
|
2575
2586
|
}
|
|
2576
2587
|
count() {
|
|
2577
2588
|
return this.store.count();
|
|
@@ -2595,10 +2606,20 @@ var OIMObject = class {
|
|
|
2595
2606
|
var OIMReactiveObject = class extends OIMObject {
|
|
2596
2607
|
constructor(queue, opts) {
|
|
2597
2608
|
super(opts);
|
|
2598
|
-
this.updateEmitter = new
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2609
|
+
this.updateEmitter = new OIMCarrierKeyedEmitter(queue, new OIMKeyedCarrierResolver());
|
|
2610
|
+
}
|
|
2611
|
+
/**
|
|
2612
|
+
* Mark the written key(s) on the keyed emitter directly — no general-event
|
|
2613
|
+
* bridge, no `{ keys }` payload on the single-key path. `super` still fires
|
|
2614
|
+
* the general UPDATE event for any low-level `emitter` subscribers.
|
|
2615
|
+
*/
|
|
2616
|
+
onUpdatedKey(key) {
|
|
2617
|
+
this.updateEmitter.markUpdatedKey(key);
|
|
2618
|
+
super.onUpdatedKey(key);
|
|
2619
|
+
}
|
|
2620
|
+
onUpdatedKeys(keys) {
|
|
2621
|
+
this.updateEmitter.markUpdatedKeys(keys);
|
|
2622
|
+
super.onUpdatedKeys(keys);
|
|
2602
2623
|
}
|
|
2603
2624
|
subscribeOnKey(key, handler) {
|
|
2604
2625
|
return this.updateEmitter.subscribeOnKey(key, handler);
|
|
@@ -2702,6 +2723,9 @@ var OIMEventQueue = class {
|
|
|
2702
2723
|
constructor(options = {}) {
|
|
2703
2724
|
this.emitter = new OIMEventEmitter();
|
|
2704
2725
|
this.tasks = /* @__PURE__ */ new Set();
|
|
2726
|
+
// Reused buffer swapped in during flush so the pending set is never copied
|
|
2727
|
+
// (no per-flush array snapshot) and new enqueues land in a fresh set.
|
|
2728
|
+
this.tasksSpare = /* @__PURE__ */ new Set();
|
|
2705
2729
|
this.isFlushing = false;
|
|
2706
2730
|
this.scheduler = options.scheduler;
|
|
2707
2731
|
if (this.scheduler) {
|
|
@@ -2713,32 +2737,23 @@ var OIMEventQueue = class {
|
|
|
2713
2737
|
}
|
|
2714
2738
|
}
|
|
2715
2739
|
/**
|
|
2716
|
-
* Enqueue a one-shot task to
|
|
2717
|
-
*
|
|
2740
|
+
* Enqueue a one-shot task to run on the next flush. The function is stored
|
|
2741
|
+
* directly (no wrapper, no returned closure) and cancelled via {@link cancel}
|
|
2742
|
+
* by passing the same reference. Enqueuing the same reference twice is
|
|
2743
|
+
* idempotent — it is Set-deduped and runs once per flush.
|
|
2718
2744
|
*/
|
|
2719
2745
|
enqueue(fn) {
|
|
2720
|
-
|
|
2721
|
-
const task = () => {
|
|
2722
|
-
if (!isActive) return;
|
|
2723
|
-
fn();
|
|
2724
|
-
};
|
|
2725
|
-
this.tasks.add(task);
|
|
2746
|
+
this.tasks.add(fn);
|
|
2726
2747
|
this.ensureScheduled();
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2748
|
+
}
|
|
2749
|
+
/** Cancel a previously enqueued task by its reference; safe to call mid-flush. */
|
|
2750
|
+
cancel(fn) {
|
|
2751
|
+
this.tasks.delete(fn);
|
|
2752
|
+
this.flushing?.delete(fn);
|
|
2732
2753
|
}
|
|
2733
2754
|
get isInFlush() {
|
|
2734
2755
|
return this.isFlushing;
|
|
2735
2756
|
}
|
|
2736
|
-
/**
|
|
2737
|
-
* Dequeue (cancel) a previously enqueued task by its dequeue function.
|
|
2738
|
-
*/
|
|
2739
|
-
dequeue(dequeueFn) {
|
|
2740
|
-
dequeueFn();
|
|
2741
|
-
}
|
|
2742
2757
|
/**
|
|
2743
2758
|
* Execute all pending tasks.
|
|
2744
2759
|
*
|
|
@@ -2749,11 +2764,13 @@ var OIMEventQueue = class {
|
|
|
2749
2764
|
this.scheduler?.cancel();
|
|
2750
2765
|
this.isFlushing = true;
|
|
2751
2766
|
this.emitter.emit(1 /* BEFORE_FLUSH */, void 0);
|
|
2752
|
-
const
|
|
2753
|
-
this.tasks.
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2767
|
+
const flushing = this.tasks;
|
|
2768
|
+
this.tasks = this.tasksSpare;
|
|
2769
|
+
this.flushing = flushing;
|
|
2770
|
+
for (const task of flushing) task();
|
|
2771
|
+
flushing.clear();
|
|
2772
|
+
this.tasksSpare = flushing;
|
|
2773
|
+
this.flushing = void 0;
|
|
2757
2774
|
this.isFlushing = false;
|
|
2758
2775
|
this.emitter.emit(0 /* AFTER_FLUSH */, void 0);
|
|
2759
2776
|
}
|
package/dist/index.d.cts
CHANGED
|
@@ -247,20 +247,22 @@ declare enum EOIMEventQueueEventType {
|
|
|
247
247
|
declare class OIMEventQueue {
|
|
248
248
|
readonly emitter: OIMEventEmitter<Record<EOIMEventQueueEventType, void>>;
|
|
249
249
|
protected tasks: Set<() => void>;
|
|
250
|
+
protected tasksSpare: Set<() => void>;
|
|
251
|
+
protected flushing?: Set<() => void>;
|
|
250
252
|
protected readonly scheduler?: OIMEventQueueScheduler;
|
|
251
253
|
protected flushBound?: () => void;
|
|
252
254
|
protected isFlushing: boolean;
|
|
253
255
|
constructor(options?: TOIMEventQueueOptions);
|
|
254
256
|
/**
|
|
255
|
-
* Enqueue a one-shot task to
|
|
256
|
-
*
|
|
257
|
+
* Enqueue a one-shot task to run on the next flush. The function is stored
|
|
258
|
+
* directly (no wrapper, no returned closure) and cancelled via {@link cancel}
|
|
259
|
+
* by passing the same reference. Enqueuing the same reference twice is
|
|
260
|
+
* idempotent — it is Set-deduped and runs once per flush.
|
|
257
261
|
*/
|
|
258
|
-
enqueue(fn: () => void):
|
|
262
|
+
enqueue(fn: () => void): void;
|
|
263
|
+
/** Cancel a previously enqueued task by its reference; safe to call mid-flush. */
|
|
264
|
+
cancel(fn: () => void): void;
|
|
259
265
|
get isInFlush(): boolean;
|
|
260
|
-
/**
|
|
261
|
-
* Dequeue (cancel) a previously enqueued task by its dequeue function.
|
|
262
|
-
*/
|
|
263
|
-
dequeue(dequeueFn: () => void): void;
|
|
264
266
|
/**
|
|
265
267
|
* Execute all pending tasks.
|
|
266
268
|
*
|
|
@@ -454,7 +456,6 @@ declare class OIMCarrierKeyedEmitter<TKey extends TOIMPk, TCarrier extends IOIMS
|
|
|
454
456
|
private readonly subscribedCarriers;
|
|
455
457
|
private dirtyCarriers;
|
|
456
458
|
private isFlushEnqueued;
|
|
457
|
-
private dequeueFlush?;
|
|
458
459
|
constructor(queue: OIMEventQueue, resolver: IOIMCarrierResolver<TKey, TCarrier>);
|
|
459
460
|
subscribeOnKey(key: TKey, handler: TOIMEventHandler<void>): () => void;
|
|
460
461
|
subscribeOnKeys(keys: readonly TKey[], handler: TOIMEventHandler<void>): () => void;
|
|
@@ -489,7 +490,6 @@ declare class OIMReactiveCollection<TEntity extends object, TPk extends TOIMPk>
|
|
|
489
490
|
private pendingAnyUpdatePks;
|
|
490
491
|
private isAnyUpdateClearPending;
|
|
491
492
|
private isAnyUpdateScheduled;
|
|
492
|
-
private dequeueAnyUpdate?;
|
|
493
493
|
constructor(queue: OIMEventQueue, opts?: TOIMCollectionOptions<TEntity, TPk>);
|
|
494
494
|
subscribeOnKey(pk: TPk, handler: TOIMEventHandler<void>): () => void;
|
|
495
495
|
subscribeOnKeys(pks: readonly TPk[], handler: TOIMEventHandler<void>): () => void;
|
|
@@ -661,7 +661,6 @@ declare class OIMUpdateEventEmitter<TKey extends TOIMPk> {
|
|
|
661
661
|
protected queue: OIMEventQueue;
|
|
662
662
|
private readonly deliveryMode;
|
|
663
663
|
private isFlushEnqueued;
|
|
664
|
-
private dequeueFlush?;
|
|
665
664
|
private updatedKeys;
|
|
666
665
|
private isAfterFlushDeliveryScheduled;
|
|
667
666
|
private unsubscribeAfterFlushDelivery?;
|
|
@@ -999,6 +998,15 @@ declare class OIMObject<TKey extends string, TValue> {
|
|
|
999
998
|
get(key: TKey): TValue | undefined;
|
|
1000
999
|
getAll(): Record<TKey, TValue>;
|
|
1001
1000
|
clear(): void;
|
|
1001
|
+
/**
|
|
1002
|
+
* Write fan-out hooks. Split single-key vs multi-key so a reactive subclass
|
|
1003
|
+
* can mark its keyed emitter on the hot single-property path without
|
|
1004
|
+
* allocating a `{ keys: [key] }` payload. The base only fires the general
|
|
1005
|
+
* UPDATE event, and only when something is actually subscribed to it — so an
|
|
1006
|
+
* unobserved write skips the payload allocation entirely.
|
|
1007
|
+
*/
|
|
1008
|
+
protected onUpdatedKey(key: TKey): void;
|
|
1009
|
+
protected onUpdatedKeys(keys: TKey[]): void;
|
|
1002
1010
|
count(): number;
|
|
1003
1011
|
keys(): TKey[];
|
|
1004
1012
|
values(): TValue[];
|
|
@@ -1007,8 +1015,15 @@ declare class OIMObject<TKey extends string, TValue> {
|
|
|
1007
1015
|
}
|
|
1008
1016
|
|
|
1009
1017
|
declare class OIMReactiveObject<TKey extends string, TValue> extends OIMObject<TKey, TValue> implements IOIMKeyedSubscription<TKey> {
|
|
1010
|
-
protected readonly updateEmitter:
|
|
1018
|
+
protected readonly updateEmitter: IOIMKeyedUpdateEmitter<TKey>;
|
|
1011
1019
|
constructor(queue: OIMEventQueue, opts?: TOIMObjectOptions<TKey, TValue>);
|
|
1020
|
+
/**
|
|
1021
|
+
* Mark the written key(s) on the keyed emitter directly — no general-event
|
|
1022
|
+
* bridge, no `{ keys }` payload on the single-key path. `super` still fires
|
|
1023
|
+
* the general UPDATE event for any low-level `emitter` subscribers.
|
|
1024
|
+
*/
|
|
1025
|
+
protected onUpdatedKey(key: TKey): void;
|
|
1026
|
+
protected onUpdatedKeys(keys: TKey[]): void;
|
|
1012
1027
|
subscribeOnKey(key: TKey, handler: TOIMEventHandler<void>): () => void;
|
|
1013
1028
|
subscribeOnKeys(keys: readonly TKey[], handler: TOIMEventHandler<void>): () => void;
|
|
1014
1029
|
unsubscribeFromKey(key: TKey, handler: TOIMEventHandler<void>): void;
|
package/dist/index.d.ts
CHANGED
|
@@ -247,20 +247,22 @@ declare enum EOIMEventQueueEventType {
|
|
|
247
247
|
declare class OIMEventQueue {
|
|
248
248
|
readonly emitter: OIMEventEmitter<Record<EOIMEventQueueEventType, void>>;
|
|
249
249
|
protected tasks: Set<() => void>;
|
|
250
|
+
protected tasksSpare: Set<() => void>;
|
|
251
|
+
protected flushing?: Set<() => void>;
|
|
250
252
|
protected readonly scheduler?: OIMEventQueueScheduler;
|
|
251
253
|
protected flushBound?: () => void;
|
|
252
254
|
protected isFlushing: boolean;
|
|
253
255
|
constructor(options?: TOIMEventQueueOptions);
|
|
254
256
|
/**
|
|
255
|
-
* Enqueue a one-shot task to
|
|
256
|
-
*
|
|
257
|
+
* Enqueue a one-shot task to run on the next flush. The function is stored
|
|
258
|
+
* directly (no wrapper, no returned closure) and cancelled via {@link cancel}
|
|
259
|
+
* by passing the same reference. Enqueuing the same reference twice is
|
|
260
|
+
* idempotent — it is Set-deduped and runs once per flush.
|
|
257
261
|
*/
|
|
258
|
-
enqueue(fn: () => void):
|
|
262
|
+
enqueue(fn: () => void): void;
|
|
263
|
+
/** Cancel a previously enqueued task by its reference; safe to call mid-flush. */
|
|
264
|
+
cancel(fn: () => void): void;
|
|
259
265
|
get isInFlush(): boolean;
|
|
260
|
-
/**
|
|
261
|
-
* Dequeue (cancel) a previously enqueued task by its dequeue function.
|
|
262
|
-
*/
|
|
263
|
-
dequeue(dequeueFn: () => void): void;
|
|
264
266
|
/**
|
|
265
267
|
* Execute all pending tasks.
|
|
266
268
|
*
|
|
@@ -454,7 +456,6 @@ declare class OIMCarrierKeyedEmitter<TKey extends TOIMPk, TCarrier extends IOIMS
|
|
|
454
456
|
private readonly subscribedCarriers;
|
|
455
457
|
private dirtyCarriers;
|
|
456
458
|
private isFlushEnqueued;
|
|
457
|
-
private dequeueFlush?;
|
|
458
459
|
constructor(queue: OIMEventQueue, resolver: IOIMCarrierResolver<TKey, TCarrier>);
|
|
459
460
|
subscribeOnKey(key: TKey, handler: TOIMEventHandler<void>): () => void;
|
|
460
461
|
subscribeOnKeys(keys: readonly TKey[], handler: TOIMEventHandler<void>): () => void;
|
|
@@ -489,7 +490,6 @@ declare class OIMReactiveCollection<TEntity extends object, TPk extends TOIMPk>
|
|
|
489
490
|
private pendingAnyUpdatePks;
|
|
490
491
|
private isAnyUpdateClearPending;
|
|
491
492
|
private isAnyUpdateScheduled;
|
|
492
|
-
private dequeueAnyUpdate?;
|
|
493
493
|
constructor(queue: OIMEventQueue, opts?: TOIMCollectionOptions<TEntity, TPk>);
|
|
494
494
|
subscribeOnKey(pk: TPk, handler: TOIMEventHandler<void>): () => void;
|
|
495
495
|
subscribeOnKeys(pks: readonly TPk[], handler: TOIMEventHandler<void>): () => void;
|
|
@@ -661,7 +661,6 @@ declare class OIMUpdateEventEmitter<TKey extends TOIMPk> {
|
|
|
661
661
|
protected queue: OIMEventQueue;
|
|
662
662
|
private readonly deliveryMode;
|
|
663
663
|
private isFlushEnqueued;
|
|
664
|
-
private dequeueFlush?;
|
|
665
664
|
private updatedKeys;
|
|
666
665
|
private isAfterFlushDeliveryScheduled;
|
|
667
666
|
private unsubscribeAfterFlushDelivery?;
|
|
@@ -999,6 +998,15 @@ declare class OIMObject<TKey extends string, TValue> {
|
|
|
999
998
|
get(key: TKey): TValue | undefined;
|
|
1000
999
|
getAll(): Record<TKey, TValue>;
|
|
1001
1000
|
clear(): void;
|
|
1001
|
+
/**
|
|
1002
|
+
* Write fan-out hooks. Split single-key vs multi-key so a reactive subclass
|
|
1003
|
+
* can mark its keyed emitter on the hot single-property path without
|
|
1004
|
+
* allocating a `{ keys: [key] }` payload. The base only fires the general
|
|
1005
|
+
* UPDATE event, and only when something is actually subscribed to it — so an
|
|
1006
|
+
* unobserved write skips the payload allocation entirely.
|
|
1007
|
+
*/
|
|
1008
|
+
protected onUpdatedKey(key: TKey): void;
|
|
1009
|
+
protected onUpdatedKeys(keys: TKey[]): void;
|
|
1002
1010
|
count(): number;
|
|
1003
1011
|
keys(): TKey[];
|
|
1004
1012
|
values(): TValue[];
|
|
@@ -1007,8 +1015,15 @@ declare class OIMObject<TKey extends string, TValue> {
|
|
|
1007
1015
|
}
|
|
1008
1016
|
|
|
1009
1017
|
declare class OIMReactiveObject<TKey extends string, TValue> extends OIMObject<TKey, TValue> implements IOIMKeyedSubscription<TKey> {
|
|
1010
|
-
protected readonly updateEmitter:
|
|
1018
|
+
protected readonly updateEmitter: IOIMKeyedUpdateEmitter<TKey>;
|
|
1011
1019
|
constructor(queue: OIMEventQueue, opts?: TOIMObjectOptions<TKey, TValue>);
|
|
1020
|
+
/**
|
|
1021
|
+
* Mark the written key(s) on the keyed emitter directly — no general-event
|
|
1022
|
+
* bridge, no `{ keys }` payload on the single-key path. `super` still fires
|
|
1023
|
+
* the general UPDATE event for any low-level `emitter` subscribers.
|
|
1024
|
+
*/
|
|
1025
|
+
protected onUpdatedKey(key: TKey): void;
|
|
1026
|
+
protected onUpdatedKeys(keys: TKey[]): void;
|
|
1012
1027
|
subscribeOnKey(key: TKey, handler: TOIMEventHandler<void>): () => void;
|
|
1013
1028
|
subscribeOnKeys(keys: readonly TKey[], handler: TOIMEventHandler<void>): () => void;
|
|
1014
1029
|
unsubscribeFromKey(key: TKey, handler: TOIMEventHandler<void>): void;
|
package/dist/index.js
CHANGED
|
@@ -435,7 +435,6 @@ var OIMCarrierKeyedEmitter = class {
|
|
|
435
435
|
this.isFlushEnqueued = false;
|
|
436
436
|
this.onFlush = () => {
|
|
437
437
|
this.isFlushEnqueued = false;
|
|
438
|
-
this.dequeueFlush = void 0;
|
|
439
438
|
if (this.dirtyCarriers.length === 0) return;
|
|
440
439
|
const flushing = this.dirtyCarriers;
|
|
441
440
|
this.dirtyCarriers = [];
|
|
@@ -545,9 +544,9 @@ var OIMCarrierKeyedEmitter = class {
|
|
|
545
544
|
};
|
|
546
545
|
}
|
|
547
546
|
destroy() {
|
|
548
|
-
if (this.
|
|
549
|
-
this.
|
|
550
|
-
this.
|
|
547
|
+
if (this.isFlushEnqueued) {
|
|
548
|
+
this.queue.cancel(this.onFlush);
|
|
549
|
+
this.isFlushEnqueued = false;
|
|
551
550
|
}
|
|
552
551
|
this.subscribedCarriers.forEach((carrier) => {
|
|
553
552
|
carrier.subscribers?.clear();
|
|
@@ -570,7 +569,7 @@ var OIMCarrierKeyedEmitter = class {
|
|
|
570
569
|
scheduleFlush() {
|
|
571
570
|
if (this.isFlushEnqueued) return;
|
|
572
571
|
this.isFlushEnqueued = true;
|
|
573
|
-
this.
|
|
572
|
+
this.queue.enqueue(this.onFlush);
|
|
574
573
|
}
|
|
575
574
|
notify(carrier) {
|
|
576
575
|
const subscribers = carrier.subscribers;
|
|
@@ -996,7 +995,6 @@ var OIMReactiveCollection = class extends OIMCollection {
|
|
|
996
995
|
this.runAnyUpdate = () => {
|
|
997
996
|
if (!this.isAnyUpdateScheduled) return;
|
|
998
997
|
this.isAnyUpdateScheduled = false;
|
|
999
|
-
this.dequeueAnyUpdate = void 0;
|
|
1000
998
|
const pks = this.isAnyUpdateClearPending ? [] : Array.from(this.pendingAnyUpdatePks);
|
|
1001
999
|
this.pendingAnyUpdatePks.clear();
|
|
1002
1000
|
this.isAnyUpdateClearPending = false;
|
|
@@ -1136,13 +1134,13 @@ var OIMReactiveCollection = class extends OIMCollection {
|
|
|
1136
1134
|
ensureAnyUpdateEnqueued() {
|
|
1137
1135
|
if (this.isAnyUpdateScheduled) return;
|
|
1138
1136
|
this.isAnyUpdateScheduled = true;
|
|
1139
|
-
this.
|
|
1137
|
+
this.queue.enqueue(this.runAnyUpdate);
|
|
1140
1138
|
}
|
|
1141
1139
|
destroy() {
|
|
1142
1140
|
this.updateEmitter.destroy();
|
|
1143
|
-
if (this.
|
|
1144
|
-
this.
|
|
1145
|
-
this.
|
|
1141
|
+
if (this.isAnyUpdateScheduled) {
|
|
1142
|
+
this.queue.cancel(this.runAnyUpdate);
|
|
1143
|
+
this.isAnyUpdateScheduled = false;
|
|
1146
1144
|
}
|
|
1147
1145
|
this.anyUpdateHandlers.clear();
|
|
1148
1146
|
this.pendingAnyUpdatePks.clear();
|
|
@@ -1284,7 +1282,6 @@ var OIMUpdateEventEmitter = class {
|
|
|
1284
1282
|
// Stable handler reference to avoid allocating a new closure on every HAS_CHANGES.
|
|
1285
1283
|
this.onFlush = () => {
|
|
1286
1284
|
this.isFlushEnqueued = false;
|
|
1287
|
-
this.dequeueFlush = void 0;
|
|
1288
1285
|
if (this.keyHandlers.size === 0) {
|
|
1289
1286
|
this.updatedKeys.clear();
|
|
1290
1287
|
return;
|
|
@@ -1337,7 +1334,7 @@ var OIMUpdateEventEmitter = class {
|
|
|
1337
1334
|
}
|
|
1338
1335
|
if (this.isFlushEnqueued) return;
|
|
1339
1336
|
this.isFlushEnqueued = true;
|
|
1340
|
-
this.
|
|
1337
|
+
this.queue.enqueue(this.onFlush);
|
|
1341
1338
|
}
|
|
1342
1339
|
markUpdatedKey(key) {
|
|
1343
1340
|
if (this.deliveryMode === "queue" && this.queue.isInFlush) {
|
|
@@ -1359,7 +1356,7 @@ var OIMUpdateEventEmitter = class {
|
|
|
1359
1356
|
}
|
|
1360
1357
|
if (this.isFlushEnqueued) return;
|
|
1361
1358
|
this.isFlushEnqueued = true;
|
|
1362
|
-
this.
|
|
1359
|
+
this.queue.enqueue(this.onFlush);
|
|
1363
1360
|
}
|
|
1364
1361
|
markAllUpdated() {
|
|
1365
1362
|
if (this.deliveryMode === "queue" && this.queue.isInFlush) {
|
|
@@ -1381,7 +1378,7 @@ var OIMUpdateEventEmitter = class {
|
|
|
1381
1378
|
}
|
|
1382
1379
|
if (this.isFlushEnqueued) return;
|
|
1383
1380
|
this.isFlushEnqueued = true;
|
|
1384
|
-
this.
|
|
1381
|
+
this.queue.enqueue(this.onFlush);
|
|
1385
1382
|
}
|
|
1386
1383
|
deliverImmediate() {
|
|
1387
1384
|
this.runSinglePass();
|
|
@@ -1429,9 +1426,9 @@ var OIMUpdateEventEmitter = class {
|
|
|
1429
1426
|
}
|
|
1430
1427
|
}
|
|
1431
1428
|
destroy() {
|
|
1432
|
-
if (this.
|
|
1433
|
-
this.
|
|
1434
|
-
this.
|
|
1429
|
+
if (this.isFlushEnqueued) {
|
|
1430
|
+
this.queue.cancel(this.onFlush);
|
|
1431
|
+
this.isFlushEnqueued = false;
|
|
1435
1432
|
}
|
|
1436
1433
|
if (this.unsubscribeAfterFlushDelivery) {
|
|
1437
1434
|
this.unsubscribeAfterFlushDelivery();
|
|
@@ -1489,9 +1486,8 @@ var OIMUpdateEventEmitter = class {
|
|
|
1489
1486
|
}
|
|
1490
1487
|
if (this.keyHandlers.size === 0) {
|
|
1491
1488
|
this.updatedKeys.clear();
|
|
1492
|
-
if (this.
|
|
1493
|
-
this.
|
|
1494
|
-
this.dequeueFlush = void 0;
|
|
1489
|
+
if (this.isFlushEnqueued) {
|
|
1490
|
+
this.queue.cancel(this.onFlush);
|
|
1495
1491
|
}
|
|
1496
1492
|
this.isFlushEnqueued = false;
|
|
1497
1493
|
}
|
|
@@ -2449,17 +2445,15 @@ var OIMObject = class {
|
|
|
2449
2445
|
}
|
|
2450
2446
|
setProperty(key, value) {
|
|
2451
2447
|
this.store.setProperty(key, value);
|
|
2452
|
-
this.
|
|
2448
|
+
this.onUpdatedKey(key);
|
|
2453
2449
|
}
|
|
2454
2450
|
merge(draft) {
|
|
2455
2451
|
this.store.merge(draft);
|
|
2456
|
-
this.
|
|
2457
|
-
keys: Object.keys(draft)
|
|
2458
|
-
});
|
|
2452
|
+
this.onUpdatedKeys(Object.keys(draft));
|
|
2459
2453
|
}
|
|
2460
2454
|
delete(key) {
|
|
2461
2455
|
this.store.delete(key);
|
|
2462
|
-
this.
|
|
2456
|
+
this.onUpdatedKey(key);
|
|
2463
2457
|
}
|
|
2464
2458
|
get(key) {
|
|
2465
2459
|
return this.store.get(key);
|
|
@@ -2469,7 +2463,24 @@ var OIMObject = class {
|
|
|
2469
2463
|
}
|
|
2470
2464
|
clear() {
|
|
2471
2465
|
this.store.clear();
|
|
2472
|
-
this.
|
|
2466
|
+
this.onUpdatedKeys([]);
|
|
2467
|
+
}
|
|
2468
|
+
/**
|
|
2469
|
+
* Write fan-out hooks. Split single-key vs multi-key so a reactive subclass
|
|
2470
|
+
* can mark its keyed emitter on the hot single-property path without
|
|
2471
|
+
* allocating a `{ keys: [key] }` payload. The base only fires the general
|
|
2472
|
+
* UPDATE event, and only when something is actually subscribed to it — so an
|
|
2473
|
+
* unobserved write skips the payload allocation entirely.
|
|
2474
|
+
*/
|
|
2475
|
+
onUpdatedKey(key) {
|
|
2476
|
+
if (this.emitter.hasHandlers(0 /* UPDATE */)) {
|
|
2477
|
+
this.emitter.emit(0 /* UPDATE */, { keys: [key] });
|
|
2478
|
+
}
|
|
2479
|
+
}
|
|
2480
|
+
onUpdatedKeys(keys) {
|
|
2481
|
+
if (this.emitter.hasHandlers(0 /* UPDATE */)) {
|
|
2482
|
+
this.emitter.emit(0 /* UPDATE */, { keys });
|
|
2483
|
+
}
|
|
2473
2484
|
}
|
|
2474
2485
|
count() {
|
|
2475
2486
|
return this.store.count();
|
|
@@ -2493,10 +2504,20 @@ var OIMObject = class {
|
|
|
2493
2504
|
var OIMReactiveObject = class extends OIMObject {
|
|
2494
2505
|
constructor(queue, opts) {
|
|
2495
2506
|
super(opts);
|
|
2496
|
-
this.updateEmitter = new
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2507
|
+
this.updateEmitter = new OIMCarrierKeyedEmitter(queue, new OIMKeyedCarrierResolver());
|
|
2508
|
+
}
|
|
2509
|
+
/**
|
|
2510
|
+
* Mark the written key(s) on the keyed emitter directly — no general-event
|
|
2511
|
+
* bridge, no `{ keys }` payload on the single-key path. `super` still fires
|
|
2512
|
+
* the general UPDATE event for any low-level `emitter` subscribers.
|
|
2513
|
+
*/
|
|
2514
|
+
onUpdatedKey(key) {
|
|
2515
|
+
this.updateEmitter.markUpdatedKey(key);
|
|
2516
|
+
super.onUpdatedKey(key);
|
|
2517
|
+
}
|
|
2518
|
+
onUpdatedKeys(keys) {
|
|
2519
|
+
this.updateEmitter.markUpdatedKeys(keys);
|
|
2520
|
+
super.onUpdatedKeys(keys);
|
|
2500
2521
|
}
|
|
2501
2522
|
subscribeOnKey(key, handler) {
|
|
2502
2523
|
return this.updateEmitter.subscribeOnKey(key, handler);
|
|
@@ -2600,6 +2621,9 @@ var OIMEventQueue = class {
|
|
|
2600
2621
|
constructor(options = {}) {
|
|
2601
2622
|
this.emitter = new OIMEventEmitter();
|
|
2602
2623
|
this.tasks = /* @__PURE__ */ new Set();
|
|
2624
|
+
// Reused buffer swapped in during flush so the pending set is never copied
|
|
2625
|
+
// (no per-flush array snapshot) and new enqueues land in a fresh set.
|
|
2626
|
+
this.tasksSpare = /* @__PURE__ */ new Set();
|
|
2603
2627
|
this.isFlushing = false;
|
|
2604
2628
|
this.scheduler = options.scheduler;
|
|
2605
2629
|
if (this.scheduler) {
|
|
@@ -2611,32 +2635,23 @@ var OIMEventQueue = class {
|
|
|
2611
2635
|
}
|
|
2612
2636
|
}
|
|
2613
2637
|
/**
|
|
2614
|
-
* Enqueue a one-shot task to
|
|
2615
|
-
*
|
|
2638
|
+
* Enqueue a one-shot task to run on the next flush. The function is stored
|
|
2639
|
+
* directly (no wrapper, no returned closure) and cancelled via {@link cancel}
|
|
2640
|
+
* by passing the same reference. Enqueuing the same reference twice is
|
|
2641
|
+
* idempotent — it is Set-deduped and runs once per flush.
|
|
2616
2642
|
*/
|
|
2617
2643
|
enqueue(fn) {
|
|
2618
|
-
|
|
2619
|
-
const task = () => {
|
|
2620
|
-
if (!isActive) return;
|
|
2621
|
-
fn();
|
|
2622
|
-
};
|
|
2623
|
-
this.tasks.add(task);
|
|
2644
|
+
this.tasks.add(fn);
|
|
2624
2645
|
this.ensureScheduled();
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2646
|
+
}
|
|
2647
|
+
/** Cancel a previously enqueued task by its reference; safe to call mid-flush. */
|
|
2648
|
+
cancel(fn) {
|
|
2649
|
+
this.tasks.delete(fn);
|
|
2650
|
+
this.flushing?.delete(fn);
|
|
2630
2651
|
}
|
|
2631
2652
|
get isInFlush() {
|
|
2632
2653
|
return this.isFlushing;
|
|
2633
2654
|
}
|
|
2634
|
-
/**
|
|
2635
|
-
* Dequeue (cancel) a previously enqueued task by its dequeue function.
|
|
2636
|
-
*/
|
|
2637
|
-
dequeue(dequeueFn) {
|
|
2638
|
-
dequeueFn();
|
|
2639
|
-
}
|
|
2640
2655
|
/**
|
|
2641
2656
|
* Execute all pending tasks.
|
|
2642
2657
|
*
|
|
@@ -2647,11 +2662,13 @@ var OIMEventQueue = class {
|
|
|
2647
2662
|
this.scheduler?.cancel();
|
|
2648
2663
|
this.isFlushing = true;
|
|
2649
2664
|
this.emitter.emit(1 /* BEFORE_FLUSH */, void 0);
|
|
2650
|
-
const
|
|
2651
|
-
this.tasks.
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2665
|
+
const flushing = this.tasks;
|
|
2666
|
+
this.tasks = this.tasksSpare;
|
|
2667
|
+
this.flushing = flushing;
|
|
2668
|
+
for (const task of flushing) task();
|
|
2669
|
+
flushing.clear();
|
|
2670
|
+
this.tasksSpare = flushing;
|
|
2671
|
+
this.flushing = void 0;
|
|
2655
2672
|
this.isFlushing = false;
|
|
2656
2673
|
this.emitter.emit(0 /* AFTER_FLUSH */, void 0);
|
|
2657
2674
|
}
|