@oimdb/core 1.4.0 → 1.4.1

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
@@ -21,11 +21,19 @@ This package exports all the core classes, interfaces, and types needed to build
21
21
  - **OIMCollection**: Base collection with CRUD operations and event emission
22
22
 
23
23
  ### Event System
24
- - **OIMUpdateEventEmitter**: Key-specific event subscriptions with coalescing
25
- - **OIMUpdateEventCoalescer**: Intelligent event batching and deduplication
24
+ - **OIMUpdateEventEmitter**: Key-based subscriptions with batching/deduplication (no buffering if there are no subscribers)
26
25
  - **OIMEventEmitter**: Generic type-safe event emitter
27
26
  - **Schedulers**: Multiple event processing strategies (microtask, timeout, animationFrame, immediate)
28
27
 
28
+ ### Reactive Primitives
29
+ - **OIMEffect**: Reactive effects that run when dependencies change
30
+ - **OIMComputed**: Derived values that recompute when dependencies change
31
+ - **OIMSelector**: Value watchers that deliver updates only when values actually change
32
+ - `OIMCollectionByPkSelector`: Watch single entity from collection
33
+ - `OIMCollectionByPksSelector`: Watch multiple entities from collection
34
+ - `OIMObjectValueByKeySelector`: Watch single key from reactive object
35
+ - `OIMEntitiesByIndexKey*Selector`: Watch entities by index key
36
+
29
37
  ### Storage & Indexing
30
38
  - **OIMCollectionStoreMapDriven**: Map-based storage backend
31
39
  - **OIMIndexManualSetBased**: Set-based manual index (returns `Set<TPk>`)
@@ -113,8 +121,8 @@ const queue = new OIMEventQueue({
113
121
  const userRoleIndex = new OIMReactiveIndexManualSetBased<string, string>(queue);
114
122
 
115
123
  // Subscribe to specific index key changes
116
- userRoleIndex.updateEventEmitter.subscribeOnKey('admin', (pks) => {
117
- console.log('Admin users changed:', pks); // pks is Set<string>
124
+ userRoleIndex.updateEventEmitter.subscribeOnKey('admin', () => {
125
+ console.log('Admin users changed:', userRoleIndex.getPksByKey('admin')); // Set<string>
118
126
  });
119
127
 
120
128
  // Build the index manually
@@ -145,8 +153,8 @@ const queue = new OIMEventQueue({
145
153
  const cardsByDeckIndex = new OIMReactiveIndexManualArrayBased<string, string>(queue);
146
154
 
147
155
  // Subscribe to specific index key changes
148
- cardsByDeckIndex.updateEventEmitter.subscribeOnKey('deck1', (pks) => {
149
- console.log('Deck cards changed:', pks); // pks is string[]
156
+ cardsByDeckIndex.updateEventEmitter.subscribeOnKey('deck1', () => {
157
+ console.log('Deck cards changed:', cardsByDeckIndex.getPksByKey('deck1')); // string[]
150
158
  });
151
159
 
152
160
  // Build the index manually - set full array
@@ -326,10 +334,7 @@ users.upsertOne({ id: 'user1', email: 'john@example.com' });
326
334
 
327
335
  // Only one notification will fire (in next microtask)
328
336
 
329
- // Access the underlying coalescer directly
330
- users.coalescer.emitter.on('upsert', (coalescedUpdates) => {
331
- console.log('Raw coalesced updates:', coalescedUpdates);
332
- });
337
+ // (No separate "coalescer" object exists: batching/deduplication is handled inside OIMUpdateEventEmitter.)
333
338
  ```
334
339
 
335
340
  ## 🔄 Reactive Architecture
@@ -340,7 +345,7 @@ OIMDB core uses a reactive architecture where changes automatically trigger noti
340
345
 
341
346
  ```typescript
342
347
  // Collection updates trigger events through the event queue
343
- collection.upsertOne(entity) → coalescer → event queue → subscribers
348
+ collection.upsertOne(entity) → updateEventEmitter → event queue → subscribers
344
349
 
345
350
  // Key-specific subscriptions only notify when relevant data changes
346
351
  updateEventEmitter.subscribeOnKey('user1', callback) // Only fires for user1 changes
@@ -360,31 +365,22 @@ users.upsertOne({ id: 'user1', role: 'admin' });
360
365
  // This prevents unnecessary re-renders and improves performance
361
366
  ```
362
367
 
363
- ### Effects, Computed, and the Event Lifecycle (PRE vs HANDLERS)
364
-
365
- OIMDB uses a **two-phase** update model inside a queue flush:
368
+ ### Effects, Computed, and the Event Lifecycle
366
369
 
367
- - **PRE phase**: derived logic (e.g. `OIMComputed`) and **pure** effects run here
368
- - **HANDLERS phase**: regular subscribers run here (UI updates, integrations, etc.)
370
+ OIMDB uses a single-pass flush boundary: `queue.flush()` executes the current batch of pending work.
369
371
 
370
- **Important rules / guarantees:**
371
-
372
- - **PRE should be pure**: effects in the PRE phase should **not write** back into collections/objects/indexes. This keeps the update model predictable and avoids re-entrant update loops.
373
- - **Computed values are consistent after a single flush**: after `queue.flush()`, reading `computed.get()` returns an up-to-date value.
374
- - **Notifications may be delivered in the next flush**: subscribers attached via `updateEventEmitter` are delivered through the queue and follow the queue snapshot rules. That means:
375
- - computed may recompute in flush #1 (PRE)
376
- - subscribers may run in flush #2 (HANDLERS, next queue drain) if the subscription itself is scheduled from within flush #1
377
-
378
- If you're using an auto-scheduled queue (microtask/raf/timeout), you typically don't call `flush()` manually; the above becomes “next tick/microtask” semantics instead of “second flush call”.
372
+ Effects and computed values are scheduled through `OIMComputativeRuntime`, which is backed by the same queue. This keeps the public API simple and avoids a multi-phase flush model.
379
373
 
380
374
  #### What is an Effect?
381
375
 
382
376
  `OIMEffect` is the base reactive primitive: it subscribes to dependencies and calls `run()` when those dependencies change. It coalesces multiple invalidations during the same flush into a single run.
383
377
 
378
+ **Basic example with reactive object:**
379
+
384
380
  ```typescript
385
381
  import {
386
382
  OIMEffect,
387
- EOIMEffectPhase,
383
+ OIMComputativeRuntime,
388
384
  OIMEventQueue,
389
385
  OIMReactiveObject,
390
386
  OIMEffectDependencyKeyedObject,
@@ -393,10 +389,10 @@ import {
393
389
  type TKey = 'a';
394
390
 
395
391
  const queue = new OIMEventQueue();
392
+ const runtime = new OIMComputativeRuntime(queue);
396
393
  const obj = new OIMReactiveObject<TKey, number>(queue);
397
394
 
398
- const effect = new OIMEffect(queue, {
399
- phase: EOIMEffectPhase.HANDLERS,
395
+ const effect = new OIMEffect(runtime, {
400
396
  deps: [new OIMEffectDependencyKeyedObject(obj, 'a')],
401
397
  run: () => {
402
398
  console.log('obj.a changed');
@@ -411,40 +407,154 @@ obj.destroy();
411
407
  queue.destroy();
412
408
  ```
413
409
 
410
+ **Effect with collection dependency:**
411
+
412
+ ```typescript
413
+ import {
414
+ OIMEffect,
415
+ OIMComputativeRuntime,
416
+ OIMEventQueue,
417
+ OIMReactiveCollection,
418
+ OIMEffectDependencyKeyedCollection,
419
+ } from '@oimdb/core';
420
+
421
+ interface User {
422
+ id: string;
423
+ name: string;
424
+ }
425
+
426
+ const queue = new OIMEventQueue();
427
+ const runtime = new OIMComputativeRuntime(queue);
428
+ const users = new OIMReactiveCollection<User, string>(queue, {
429
+ selectPk: (u) => u.id,
430
+ });
431
+
432
+ const effect = new OIMEffect(runtime, {
433
+ deps: [new OIMEffectDependencyKeyedCollection(users, 'user1')],
434
+ run: () => {
435
+ const user = users.getOneByPk('user1');
436
+ console.log('User1 changed:', user);
437
+ },
438
+ });
439
+
440
+ users.upsertOne({ id: 'user1', name: 'John' });
441
+ queue.flush();
442
+
443
+ effect.destroy();
444
+ users.destroy();
445
+ queue.destroy();
446
+ ```
447
+
448
+ **Effect with index dependency:**
449
+
450
+ ```typescript
451
+ import {
452
+ OIMEffect,
453
+ OIMComputativeRuntime,
454
+ OIMEventQueue,
455
+ OIMReactiveIndexManualSetBased,
456
+ OIMEffectDependencyKeyedIndex,
457
+ } from '@oimdb/core';
458
+
459
+ const queue = new OIMEventQueue();
460
+ const runtime = new OIMComputativeRuntime(queue);
461
+ const roleIndex = new OIMReactiveIndexManualSetBased<string, string>(queue);
462
+
463
+ const effect = new OIMEffect(runtime, {
464
+ deps: [new OIMEffectDependencyKeyedIndex(roleIndex, 'admin')],
465
+ run: () => {
466
+ const adminPks = roleIndex.getPksByKey('admin');
467
+ console.log('Admin users changed:', Array.from(adminPks));
468
+ },
469
+ });
470
+
471
+ roleIndex.setPks('admin', ['user1', 'user2']);
472
+ queue.flush();
473
+
474
+ effect.destroy();
475
+ roleIndex.destroy();
476
+ queue.destroy();
477
+ ```
478
+
479
+ **Effect with multiple dependencies:**
480
+
481
+ ```typescript
482
+ import {
483
+ OIMEffect,
484
+ OIMComputativeRuntime,
485
+ OIMEventQueue,
486
+ OIMReactiveObject,
487
+ OIMReactiveCollection,
488
+ OIMEffectDependencyKeyedObject,
489
+ OIMEffectDependencyKeyedCollection,
490
+ } from '@oimdb/core';
491
+
492
+ const queue = new OIMEventQueue();
493
+ const runtime = new OIMComputativeRuntime(queue);
494
+ const settings = new OIMReactiveObject<'theme' | 'lang', string>(queue);
495
+ const users = new OIMReactiveCollection<User, string>(queue, {
496
+ selectPk: (u) => u.id,
497
+ });
498
+
499
+ const effect = new OIMEffect(runtime, {
500
+ deps: [
501
+ new OIMEffectDependencyKeyedObject(settings, ['theme', 'lang']),
502
+ new OIMEffectDependencyKeyedCollection(users, 'currentUser'),
503
+ ],
504
+ run: () => {
505
+ const theme = settings.get('theme');
506
+ const user = users.getOneByPk('currentUser');
507
+ console.log('Settings or user changed:', { theme, user });
508
+ },
509
+ });
510
+
511
+ settings.setProperty('theme', 'dark');
512
+ users.upsertOne({ id: 'currentUser', name: 'John' });
513
+ queue.flush(); // Effect runs once, even though multiple deps changed
514
+
515
+ effect.destroy();
516
+ settings.destroy();
517
+ users.destroy();
518
+ queue.destroy();
519
+ ```
520
+
414
521
  #### What is a Computed?
415
522
 
416
- `OIMComputed<T>` is built on top of `OIMEffect` (PRE phase): it recomputes a derived value and emits `update` when the value changes.
523
+ `OIMComputed<T>` is built on top of `OIMEffect`: it recomputes a derived value and emits `update` when the value changes.
524
+
525
+ **Basic example:**
417
526
 
418
527
  ```typescript
419
528
  import {
420
529
  OIMComputed,
421
530
  OIMEventQueue,
531
+ OIMComputativeRuntime,
422
532
  OIMReactiveObject,
423
533
  OIMEffectDependencyKeyedObject,
424
534
  } from '@oimdb/core';
425
535
 
426
536
  type TKey = 'a';
427
537
  const queue = new OIMEventQueue();
538
+ const runtime = new OIMComputativeRuntime(queue);
428
539
  const obj = new OIMReactiveObject<TKey, number>(queue);
429
540
 
430
- const doubled = new OIMComputed<number>(queue, {
541
+ const doubled = new OIMComputed<number>(runtime, {
431
542
  compute: () => (obj.get('a') ?? 0) * 2,
432
543
  deps: [new OIMEffectDependencyKeyedObject(obj, 'a')],
433
544
  });
434
545
 
435
546
  obj.setProperty('a', 10);
436
- queue.flush(); // recompute happens during PRE
547
+ queue.flush(); // run scheduled work
437
548
 
438
549
  console.log(doubled.get()); // 20
439
550
 
440
- // If you also subscribe to computed updates, delivery can be in the next flush:
551
+ // If you also subscribe to computed updates, delivery happens in the same drain flush:
441
552
  let calls = 0;
442
553
  doubled.updateEventEmitter.subscribeOnKey('value', () => {
443
554
  calls++;
444
555
  });
445
556
  obj.setProperty('a', 11);
446
- queue.flush(); // recompute (PRE)
447
- queue.flush(); // subscriber delivery (HANDLERS via queue snapshot)
557
+ queue.flush(); // run scheduled work
448
558
  console.log(calls); // 1
449
559
 
450
560
  doubled.destroy();
@@ -452,18 +562,81 @@ obj.destroy();
452
562
  queue.destroy();
453
563
  ```
454
564
 
565
+ **Computed with collection and index dependencies:**
566
+
567
+ ```typescript
568
+ import {
569
+ OIMComputed,
570
+ OIMEventQueue,
571
+ OIMComputativeRuntime,
572
+ OIMReactiveCollection,
573
+ OIMReactiveIndexManualSetBased,
574
+ OIMEffectDependencyKeyedCollection,
575
+ OIMEffectDependencyKeyedIndex,
576
+ } from '@oimdb/core';
577
+
578
+ interface User {
579
+ id: string;
580
+ name: string;
581
+ role: string;
582
+ }
583
+
584
+ const queue = new OIMEventQueue();
585
+ const runtime = new OIMComputativeRuntime(queue);
586
+ const users = new OIMReactiveCollection<User, string>(queue, {
587
+ selectPk: (u) => u.id,
588
+ });
589
+ const roleIndex = new OIMReactiveIndexManualSetBased<string, string>(queue);
590
+
591
+ // Computed that counts admin users
592
+ const adminCount = new OIMComputed<number>(runtime, {
593
+ compute: () => {
594
+ const adminPks = roleIndex.getPksByKey('admin');
595
+ return adminPks.size;
596
+ },
597
+ deps: [new OIMEffectDependencyKeyedIndex(roleIndex, 'admin')],
598
+ });
599
+
600
+ // Computed that gets admin user names
601
+ const adminNames = new OIMComputed<string[]>(runtime, {
602
+ compute: () => {
603
+ const adminPks = Array.from(roleIndex.getPksByKey('admin'));
604
+ return adminPks
605
+ .map((pk) => users.getOneByPk(pk)?.name)
606
+ .filter((name): name is string => name !== undefined);
607
+ },
608
+ deps: [
609
+ new OIMEffectDependencyKeyedIndex(roleIndex, 'admin'),
610
+ new OIMEffectDependencyKeyedCollection(users, Array.from(roleIndex.getPksByKey('admin'))),
611
+ ],
612
+ });
613
+
614
+ users.upsertMany([
615
+ { id: 'u1', name: 'Alice', role: 'admin' },
616
+ { id: 'u2', name: 'Bob', role: 'user' },
617
+ ]);
618
+ roleIndex.setPks('admin', ['u1']);
619
+ queue.flush();
620
+
621
+ console.log(adminCount.get()); // 1
622
+ console.log(adminNames.get()); // ['Alice']
623
+
624
+ adminNames.destroy();
625
+ adminCount.destroy();
626
+ roleIndex.destroy();
627
+ users.destroy();
628
+ queue.destroy();
629
+ ```
630
+
455
631
  #### Computed-to-Computed dependencies
456
632
 
457
633
  For computed-to-computed dependencies you can use `OIMEffectDependencyComputed`.
458
634
 
459
- - In **PRE** it subscribes directly to the computed's internal `emitter` to invalidate downstream derivations immediately during the pre-drain.
460
- - In **HANDLERS** it subscribes via `computed.updateEventEmitter` (delivered through the queue).
461
-
462
635
  ```typescript
463
636
  import {
464
637
  OIMComputed,
465
638
  OIMEffect,
466
- EOIMEffectPhase,
639
+ OIMComputativeRuntime,
467
640
  OIMEffectDependencyComputed,
468
641
  OIMEventQueue,
469
642
  OIMReactiveObject,
@@ -472,27 +645,26 @@ import {
472
645
 
473
646
  type TKey = 'a';
474
647
  const queue = new OIMEventQueue();
648
+ const runtime = new OIMComputativeRuntime(queue);
475
649
  const obj = new OIMReactiveObject<TKey, number>(queue);
476
650
 
477
- const A = new OIMComputed<number>(queue, {
651
+ const A = new OIMComputed<number>(runtime, {
478
652
  compute: () => (obj.get('a') ?? 0) + 1,
479
653
  deps: [new OIMEffectDependencyKeyedObject(obj, 'a')],
480
654
  });
481
655
 
482
- const B = new OIMComputed<number>(queue, {
656
+ const B = new OIMComputed<number>(runtime, {
483
657
  compute: () => A.get() * 2,
484
658
  deps: [new OIMEffectDependencyComputed({ emitter: A.emitter, updateEventEmitter: A.updateEventEmitter })],
485
659
  });
486
660
 
487
- const effect = new OIMEffect(queue, {
488
- phase: EOIMEffectPhase.HANDLERS,
661
+ const effect = new OIMEffect(runtime, {
489
662
  deps: [new OIMEffectDependencyComputed({ emitter: B.emitter, updateEventEmitter: B.updateEventEmitter })],
490
663
  run: () => console.log('B changed'),
491
664
  });
492
665
 
493
666
  obj.setProperty('a', 1);
494
- queue.flush(); // computed recompute (PRE)
495
- queue.flush(); // handlers delivered via the queue snapshot
667
+ queue.flush(); // run scheduled work
496
668
 
497
669
  effect.destroy();
498
670
  B.destroy();
@@ -501,12 +673,153 @@ obj.destroy();
501
673
  queue.destroy();
502
674
  ```
503
675
 
676
+ #### What are Selectors?
677
+
678
+ Selectors provide a convenient way to watch and react to changes in collections, objects, and indexes. They automatically handle subscription management and deliver updates only when values actually change.
679
+
680
+ **Collection selector:**
681
+
682
+ ```typescript
683
+ import {
684
+ OIMCollectionByPkSelector,
685
+ OIMCollectionByPksSelector,
686
+ OIMComputativeRuntime,
687
+ OIMEventQueue,
688
+ OIMReactiveCollection,
689
+ } from '@oimdb/core';
690
+
691
+ interface User {
692
+ id: string;
693
+ name: string;
694
+ }
695
+
696
+ const queue = new OIMEventQueue();
697
+ const runtime = new OIMComputativeRuntime(queue);
698
+ const users = new OIMReactiveCollection<User, string>(queue, {
699
+ selectPk: (u) => u.id,
700
+ });
701
+
702
+ // Watch a single user
703
+ const userSelector = new OIMCollectionByPkSelector(runtime, users, 'user1');
704
+ const unwatch = userSelector.watch((user) => {
705
+ console.log('User1 changed:', user);
706
+ });
707
+
708
+ users.upsertOne({ id: 'user1', name: 'John' });
709
+ queue.flush(); // Callback fires with { id: 'user1', name: 'John' }
710
+
711
+ // Watch multiple users
712
+ const usersSelector = new OIMCollectionByPksSelector(runtime, users, ['user1', 'user2']);
713
+ usersSelector.watch((users) => {
714
+ console.log('Users changed:', users);
715
+ });
716
+
717
+ users.upsertMany([
718
+ { id: 'user1', name: 'John Doe' },
719
+ { id: 'user2', name: 'Jane Smith' },
720
+ ]);
721
+ queue.flush(); // Callback fires with array of users
722
+
723
+ unwatch(); // Stop watching
724
+ usersSelector.watch(() => {}); // Get unsubscribe function
725
+ users.destroy();
726
+ queue.destroy();
727
+ ```
728
+
729
+ **Selector with index (entities by index key):**
730
+
731
+ ```typescript
732
+ import {
733
+ OIMEntitiesByIndexKeySetBasedSelector,
734
+ OIMComputativeRuntime,
735
+ OIMEventQueue,
736
+ OIMReactiveCollection,
737
+ OIMReactiveIndexManualSetBased,
738
+ } from '@oimdb/core';
739
+
740
+ const queue = new OIMEventQueue();
741
+ const runtime = new OIMComputativeRuntime(queue);
742
+ const users = new OIMReactiveCollection<User, string>(queue, {
743
+ selectPk: (u) => u.id,
744
+ });
745
+ const roleIndex = new OIMReactiveIndexManualSetBased<string, string>(queue);
746
+
747
+ // Watch all admin users
748
+ const adminUsersSelector = new OIMEntitiesByIndexKeySetBasedSelector(
749
+ runtime,
750
+ users,
751
+ roleIndex,
752
+ 'admin'
753
+ );
754
+
755
+ adminUsersSelector.watch((adminUsers) => {
756
+ console.log('Admin users:', adminUsers.map((u) => u?.name));
757
+ });
758
+
759
+ users.upsertMany([
760
+ { id: 'u1', name: 'Alice', role: 'admin' },
761
+ { id: 'u2', name: 'Bob', role: 'admin' },
762
+ ]);
763
+ roleIndex.setPks('admin', ['u1', 'u2']);
764
+ queue.flush(); // Callback fires with [Alice, Bob]
765
+
766
+ // When index changes, selector automatically resubscribes to new entities
767
+ roleIndex.setPks('admin', ['u1']);
768
+ queue.flush(); // Callback fires with [Alice]
769
+
770
+ adminUsersSelector.watch(() => {}); // Get unsubscribe function
771
+ roleIndex.destroy();
772
+ users.destroy();
773
+ queue.destroy();
774
+ ```
775
+
776
+ **Object selector:**
777
+
778
+ ```typescript
779
+ import {
780
+ OIMObjectValueByKeySelector,
781
+ OIMObjectValuesByKeysSelector,
782
+ OIMComputativeRuntime,
783
+ OIMEventQueue,
784
+ OIMReactiveObject,
785
+ } from '@oimdb/core';
786
+
787
+ const queue = new OIMEventQueue();
788
+ const runtime = new OIMComputativeRuntime(queue);
789
+ const settings = new OIMReactiveObject<'theme' | 'lang', string>(queue);
790
+
791
+ // Watch single key
792
+ const themeSelector = new OIMObjectValueByKeySelector(runtime, settings, 'theme');
793
+ themeSelector.watch((theme) => {
794
+ console.log('Theme changed:', theme);
795
+ });
796
+
797
+ // Watch multiple keys
798
+ const settingsSelector = new OIMObjectValuesByKeysSelector(runtime, settings, ['theme', 'lang']);
799
+ settingsSelector.watch((values) => {
800
+ console.log('Settings changed:', values); // [theme, lang]
801
+ });
802
+
803
+ settings.setProperty('theme', 'dark');
804
+ queue.flush();
805
+
806
+ settings.destroy();
807
+ queue.destroy();
808
+ ```
809
+
810
+ **Key differences: Effects vs Selectors:**
811
+
812
+ - **Effects** (`OIMEffect`): Run side effects when dependencies change. Use for logging, API calls, UI updates.
813
+ - **Selectors** (`OIMSelector`): Watch and deliver values only when they actually change. Use for reactive data access with automatic change detection.
814
+ - **Computed** (`OIMComputed`): Derive values from dependencies. Use for calculated/transformed data.
815
+
504
816
  #### Gotchas (read this once)
505
817
 
506
818
  - **Avoid cycles**: if A depends on B and B depends on A (directly or indirectly), you can get endless invalidation/recompute. Keep your dependency graph acyclic.
507
819
  - **Keep `compute()` pure**: treat `compute()` as a pure function over current state. Doing writes inside `compute()` will create hard-to-debug re-entrancy.
508
- - **Put side-effects in HANDLERS**: if you need to write to stores or trigger IO, do it from `OIMEffect` with `phase: EOIMEffectPhase.HANDLERS`.
509
- - **Always `destroy()`**: effects/computed subscribe to dependencies; if you create them dynamically, call `destroy()` to unsubscribe and free memory.
820
+ - **Keep effects safe**: if you need to write to stores or trigger IO, do it from `OIMEffect`, but avoid creating endless update loops.
821
+ - **Always `destroy()`**: effects/computed/selectors subscribe to dependencies; if you create them dynamically, call `destroy()` or use the unsubscribe function to unsubscribe and free memory.
822
+ - **Selectors deliver only on change**: Selectors use equality checks (`areEqual`) to avoid delivering the same value multiple times. Override `areEqual` in custom selectors if needed.
510
823
 
511
824
  ### Scheduler Types
512
825
 
@@ -521,7 +834,7 @@ Choose the right scheduler for your use case:
521
834
 
522
835
  ```
523
836
  OIMCollection (base)
524
- ├── OIMReactiveCollection (adds event emitter + coalescing)
837
+ ├── OIMReactiveCollection (adds updateEventEmitter wired to the queue)
525
838
  └── OIMRICollection (reactive collection + indexes)
526
839
 
527
840
  OIMIndexSetBased (base for Set-based)
@@ -646,7 +959,7 @@ new OIMReactiveCollection(queue: OIMEventQueue, opts?: TOIMCollectionOptions<TEn
646
959
  **Properties:**
647
960
  - `collection: OIMCollection<TEntity, TPk>` - Underlying collection
648
961
  - `updateEventEmitter: OIMUpdateEventEmitter<TPk>` - Key-specific subscriptions
649
- - `coalescer: OIMUpdateEventCoalescerCollection<TPk>` - Event coalescing
962
+ - Event batching/deduplication is handled internally by `OIMUpdateEventEmitter`
650
963
 
651
964
  **Methods:**
652
965
  - `upsertOne(entity: TEntity): void` - Insert or update single entity
@@ -677,7 +990,10 @@ Reactive Set-based index with manual key-to-entity mapping and change notificati
677
990
  **Constructor:**
678
991
  ```typescript
679
992
  new OIMReactiveIndexManualSetBased(queue: OIMEventQueue, opts?: {
680
- index?: OIMIndexManualSetBased<TKey, TPk>
993
+ indexOptions?: {
994
+ comparePks?: TOIMIndexComparator<TPk>;
995
+ store?: OIMIndexStoreSetBased<TKey, TPk>;
996
+ }
681
997
  })
682
998
  ```
683
999
 
@@ -700,7 +1016,10 @@ Reactive Array-based index with manual key-to-entity mapping and change notifica
700
1016
  **Constructor:**
701
1017
  ```typescript
702
1018
  new OIMReactiveIndexManualArrayBased(queue: OIMEventQueue, opts?: {
703
- index?: OIMIndexManualArrayBased<TKey, TPk>
1019
+ indexOptions?: {
1020
+ comparePks?: TOIMIndexComparator<TPk>;
1021
+ store?: OIMIndexStoreArrayBased<TKey, TPk>;
1022
+ }
704
1023
  })
705
1024
  ```
706
1025