@oimdb/core 3.2.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 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 existing entities from stored slots
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
@@ -647,7 +647,7 @@ var OIMCarrierKeyedEmitter = class {
647
647
  }
648
648
  destroy() {
649
649
  if (this.isFlushEnqueued) {
650
- this.queue.cancelTask(this.onFlush);
650
+ this.queue.cancel(this.onFlush);
651
651
  this.isFlushEnqueued = false;
652
652
  }
653
653
  this.subscribedCarriers.forEach((carrier) => {
@@ -671,7 +671,7 @@ var OIMCarrierKeyedEmitter = class {
671
671
  scheduleFlush() {
672
672
  if (this.isFlushEnqueued) return;
673
673
  this.isFlushEnqueued = true;
674
- this.queue.enqueueTask(this.onFlush);
674
+ this.queue.enqueue(this.onFlush);
675
675
  }
676
676
  notify(carrier) {
677
677
  const subscribers = carrier.subscribers;
@@ -1236,12 +1236,12 @@ var OIMReactiveCollection = class extends OIMCollection {
1236
1236
  ensureAnyUpdateEnqueued() {
1237
1237
  if (this.isAnyUpdateScheduled) return;
1238
1238
  this.isAnyUpdateScheduled = true;
1239
- this.queue.enqueueTask(this.runAnyUpdate);
1239
+ this.queue.enqueue(this.runAnyUpdate);
1240
1240
  }
1241
1241
  destroy() {
1242
1242
  this.updateEmitter.destroy();
1243
1243
  if (this.isAnyUpdateScheduled) {
1244
- this.queue.cancelTask(this.runAnyUpdate);
1244
+ this.queue.cancel(this.runAnyUpdate);
1245
1245
  this.isAnyUpdateScheduled = false;
1246
1246
  }
1247
1247
  this.anyUpdateHandlers.clear();
@@ -1436,7 +1436,7 @@ var OIMUpdateEventEmitter = class {
1436
1436
  }
1437
1437
  if (this.isFlushEnqueued) return;
1438
1438
  this.isFlushEnqueued = true;
1439
- this.queue.enqueueTask(this.onFlush);
1439
+ this.queue.enqueue(this.onFlush);
1440
1440
  }
1441
1441
  markUpdatedKey(key) {
1442
1442
  if (this.deliveryMode === "queue" && this.queue.isInFlush) {
@@ -1458,7 +1458,7 @@ var OIMUpdateEventEmitter = class {
1458
1458
  }
1459
1459
  if (this.isFlushEnqueued) return;
1460
1460
  this.isFlushEnqueued = true;
1461
- this.queue.enqueueTask(this.onFlush);
1461
+ this.queue.enqueue(this.onFlush);
1462
1462
  }
1463
1463
  markAllUpdated() {
1464
1464
  if (this.deliveryMode === "queue" && this.queue.isInFlush) {
@@ -1480,7 +1480,7 @@ var OIMUpdateEventEmitter = class {
1480
1480
  }
1481
1481
  if (this.isFlushEnqueued) return;
1482
1482
  this.isFlushEnqueued = true;
1483
- this.queue.enqueueTask(this.onFlush);
1483
+ this.queue.enqueue(this.onFlush);
1484
1484
  }
1485
1485
  deliverImmediate() {
1486
1486
  this.runSinglePass();
@@ -1529,7 +1529,7 @@ var OIMUpdateEventEmitter = class {
1529
1529
  }
1530
1530
  destroy() {
1531
1531
  if (this.isFlushEnqueued) {
1532
- this.queue.cancelTask(this.onFlush);
1532
+ this.queue.cancel(this.onFlush);
1533
1533
  this.isFlushEnqueued = false;
1534
1534
  }
1535
1535
  if (this.unsubscribeAfterFlushDelivery) {
@@ -1589,7 +1589,7 @@ var OIMUpdateEventEmitter = class {
1589
1589
  if (this.keyHandlers.size === 0) {
1590
1590
  this.updatedKeys.clear();
1591
1591
  if (this.isFlushEnqueued) {
1592
- this.queue.cancelTask(this.onFlush);
1592
+ this.queue.cancel(this.onFlush);
1593
1593
  }
1594
1594
  this.isFlushEnqueued = false;
1595
1595
  }
@@ -2737,48 +2737,23 @@ var OIMEventQueue = class {
2737
2737
  }
2738
2738
  }
2739
2739
  /**
2740
- * Enqueue a one-shot task to be executed on the next flush.
2741
- * Returns a dequeue function that can be called to cancel the task before it runs.
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.
2742
2744
  */
2743
2745
  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
2746
  this.tasks.add(fn);
2766
2747
  this.ensureScheduled();
2767
2748
  }
2768
- /** Cancel a task scheduled via {@link enqueueTask}; safe to call mid-flush. */
2769
- cancelTask(fn) {
2749
+ /** Cancel a previously enqueued task by its reference; safe to call mid-flush. */
2750
+ cancel(fn) {
2770
2751
  this.tasks.delete(fn);
2771
2752
  this.flushing?.delete(fn);
2772
2753
  }
2773
2754
  get isInFlush() {
2774
2755
  return this.isFlushing;
2775
2756
  }
2776
- /**
2777
- * Dequeue (cancel) a previously enqueued task by its dequeue function.
2778
- */
2779
- dequeue(dequeueFn) {
2780
- dequeueFn();
2781
- }
2782
2757
  /**
2783
2758
  * Execute all pending tasks.
2784
2759
  *
package/dist/index.d.cts CHANGED
@@ -254,25 +254,15 @@ declare class OIMEventQueue {
254
254
  protected isFlushing: boolean;
255
255
  constructor(options?: TOIMEventQueueOptions);
256
256
  /**
257
- * Enqueue a one-shot task to be executed on the next flush.
258
- * Returns a dequeue function that can be called to cancel the task before it runs.
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.
259
261
  */
260
- enqueue(fn: () => void): () => void;
261
- /**
262
- * Allocation-free enqueue for a **stable** function reference (e.g. a bound
263
- * `onFlush` method). The function is stored directly — no wrapper, no
264
- * returned closure — and cancelled via {@link cancelTask}. Enqueuing the
265
- * same reference twice is idempotent (Set-deduped); callers that may run a
266
- * task more than once per flush should use {@link enqueue} instead.
267
- */
268
- enqueueTask(fn: () => void): void;
269
- /** Cancel a task scheduled via {@link enqueueTask}; safe to call mid-flush. */
270
- cancelTask(fn: () => void): 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;
271
265
  get isInFlush(): boolean;
272
- /**
273
- * Dequeue (cancel) a previously enqueued task by its dequeue function.
274
- */
275
- dequeue(dequeueFn: () => void): void;
276
266
  /**
277
267
  * Execute all pending tasks.
278
268
  *
package/dist/index.d.ts CHANGED
@@ -254,25 +254,15 @@ declare class OIMEventQueue {
254
254
  protected isFlushing: boolean;
255
255
  constructor(options?: TOIMEventQueueOptions);
256
256
  /**
257
- * Enqueue a one-shot task to be executed on the next flush.
258
- * Returns a dequeue function that can be called to cancel the task before it runs.
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.
259
261
  */
260
- enqueue(fn: () => void): () => void;
261
- /**
262
- * Allocation-free enqueue for a **stable** function reference (e.g. a bound
263
- * `onFlush` method). The function is stored directly — no wrapper, no
264
- * returned closure — and cancelled via {@link cancelTask}. Enqueuing the
265
- * same reference twice is idempotent (Set-deduped); callers that may run a
266
- * task more than once per flush should use {@link enqueue} instead.
267
- */
268
- enqueueTask(fn: () => void): void;
269
- /** Cancel a task scheduled via {@link enqueueTask}; safe to call mid-flush. */
270
- cancelTask(fn: () => void): 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;
271
265
  get isInFlush(): boolean;
272
- /**
273
- * Dequeue (cancel) a previously enqueued task by its dequeue function.
274
- */
275
- dequeue(dequeueFn: () => void): void;
276
266
  /**
277
267
  * Execute all pending tasks.
278
268
  *
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;
@@ -1134,12 +1134,12 @@ var OIMReactiveCollection = class extends OIMCollection {
1134
1134
  ensureAnyUpdateEnqueued() {
1135
1135
  if (this.isAnyUpdateScheduled) return;
1136
1136
  this.isAnyUpdateScheduled = true;
1137
- this.queue.enqueueTask(this.runAnyUpdate);
1137
+ this.queue.enqueue(this.runAnyUpdate);
1138
1138
  }
1139
1139
  destroy() {
1140
1140
  this.updateEmitter.destroy();
1141
1141
  if (this.isAnyUpdateScheduled) {
1142
- this.queue.cancelTask(this.runAnyUpdate);
1142
+ this.queue.cancel(this.runAnyUpdate);
1143
1143
  this.isAnyUpdateScheduled = false;
1144
1144
  }
1145
1145
  this.anyUpdateHandlers.clear();
@@ -1334,7 +1334,7 @@ var OIMUpdateEventEmitter = class {
1334
1334
  }
1335
1335
  if (this.isFlushEnqueued) return;
1336
1336
  this.isFlushEnqueued = true;
1337
- this.queue.enqueueTask(this.onFlush);
1337
+ this.queue.enqueue(this.onFlush);
1338
1338
  }
1339
1339
  markUpdatedKey(key) {
1340
1340
  if (this.deliveryMode === "queue" && this.queue.isInFlush) {
@@ -1356,7 +1356,7 @@ var OIMUpdateEventEmitter = class {
1356
1356
  }
1357
1357
  if (this.isFlushEnqueued) return;
1358
1358
  this.isFlushEnqueued = true;
1359
- this.queue.enqueueTask(this.onFlush);
1359
+ this.queue.enqueue(this.onFlush);
1360
1360
  }
1361
1361
  markAllUpdated() {
1362
1362
  if (this.deliveryMode === "queue" && this.queue.isInFlush) {
@@ -1378,7 +1378,7 @@ var OIMUpdateEventEmitter = class {
1378
1378
  }
1379
1379
  if (this.isFlushEnqueued) return;
1380
1380
  this.isFlushEnqueued = true;
1381
- this.queue.enqueueTask(this.onFlush);
1381
+ this.queue.enqueue(this.onFlush);
1382
1382
  }
1383
1383
  deliverImmediate() {
1384
1384
  this.runSinglePass();
@@ -1427,7 +1427,7 @@ var OIMUpdateEventEmitter = class {
1427
1427
  }
1428
1428
  destroy() {
1429
1429
  if (this.isFlushEnqueued) {
1430
- this.queue.cancelTask(this.onFlush);
1430
+ this.queue.cancel(this.onFlush);
1431
1431
  this.isFlushEnqueued = false;
1432
1432
  }
1433
1433
  if (this.unsubscribeAfterFlushDelivery) {
@@ -1487,7 +1487,7 @@ var OIMUpdateEventEmitter = class {
1487
1487
  if (this.keyHandlers.size === 0) {
1488
1488
  this.updatedKeys.clear();
1489
1489
  if (this.isFlushEnqueued) {
1490
- this.queue.cancelTask(this.onFlush);
1490
+ this.queue.cancel(this.onFlush);
1491
1491
  }
1492
1492
  this.isFlushEnqueued = false;
1493
1493
  }
@@ -2635,48 +2635,23 @@ var OIMEventQueue = class {
2635
2635
  }
2636
2636
  }
2637
2637
  /**
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.
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.
2640
2642
  */
2641
2643
  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
2644
  this.tasks.add(fn);
2664
2645
  this.ensureScheduled();
2665
2646
  }
2666
- /** Cancel a task scheduled via {@link enqueueTask}; safe to call mid-flush. */
2667
- cancelTask(fn) {
2647
+ /** Cancel a previously enqueued task by its reference; safe to call mid-flush. */
2648
+ cancel(fn) {
2668
2649
  this.tasks.delete(fn);
2669
2650
  this.flushing?.delete(fn);
2670
2651
  }
2671
2652
  get isInFlush() {
2672
2653
  return this.isFlushing;
2673
2654
  }
2674
- /**
2675
- * Dequeue (cancel) a previously enqueued task by its dequeue function.
2676
- */
2677
- dequeue(dequeueFn) {
2678
- dequeueFn();
2679
- }
2680
2655
  /**
2681
2656
  * Execute all pending tasks.
2682
2657
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oimdb/core",
3
- "version": "3.2.0",
3
+ "version": "3.3.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",