@oimdb/core 1.3.1 → 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,6 +365,462 @@ users.upsertOne({ id: 'user1', role: 'admin' });
360
365
  // This prevents unnecessary re-renders and improves performance
361
366
  ```
362
367
 
368
+ ### Effects, Computed, and the Event Lifecycle
369
+
370
+ OIMDB uses a single-pass flush boundary: `queue.flush()` executes the current batch of pending work.
371
+
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.
373
+
374
+ #### What is an Effect?
375
+
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.
377
+
378
+ **Basic example with reactive object:**
379
+
380
+ ```typescript
381
+ import {
382
+ OIMEffect,
383
+ OIMComputativeRuntime,
384
+ OIMEventQueue,
385
+ OIMReactiveObject,
386
+ OIMEffectDependencyKeyedObject,
387
+ } from '@oimdb/core';
388
+
389
+ type TKey = 'a';
390
+
391
+ const queue = new OIMEventQueue();
392
+ const runtime = new OIMComputativeRuntime(queue);
393
+ const obj = new OIMReactiveObject<TKey, number>(queue);
394
+
395
+ const effect = new OIMEffect(runtime, {
396
+ deps: [new OIMEffectDependencyKeyedObject(obj, 'a')],
397
+ run: () => {
398
+ console.log('obj.a changed');
399
+ },
400
+ });
401
+
402
+ obj.setProperty('a', 1);
403
+ queue.flush();
404
+
405
+ effect.destroy();
406
+ obj.destroy();
407
+ queue.destroy();
408
+ ```
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
+
521
+ #### What is a Computed?
522
+
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:**
526
+
527
+ ```typescript
528
+ import {
529
+ OIMComputed,
530
+ OIMEventQueue,
531
+ OIMComputativeRuntime,
532
+ OIMReactiveObject,
533
+ OIMEffectDependencyKeyedObject,
534
+ } from '@oimdb/core';
535
+
536
+ type TKey = 'a';
537
+ const queue = new OIMEventQueue();
538
+ const runtime = new OIMComputativeRuntime(queue);
539
+ const obj = new OIMReactiveObject<TKey, number>(queue);
540
+
541
+ const doubled = new OIMComputed<number>(runtime, {
542
+ compute: () => (obj.get('a') ?? 0) * 2,
543
+ deps: [new OIMEffectDependencyKeyedObject(obj, 'a')],
544
+ });
545
+
546
+ obj.setProperty('a', 10);
547
+ queue.flush(); // run scheduled work
548
+
549
+ console.log(doubled.get()); // 20
550
+
551
+ // If you also subscribe to computed updates, delivery happens in the same drain flush:
552
+ let calls = 0;
553
+ doubled.updateEventEmitter.subscribeOnKey('value', () => {
554
+ calls++;
555
+ });
556
+ obj.setProperty('a', 11);
557
+ queue.flush(); // run scheduled work
558
+ console.log(calls); // 1
559
+
560
+ doubled.destroy();
561
+ obj.destroy();
562
+ queue.destroy();
563
+ ```
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
+
631
+ #### Computed-to-Computed dependencies
632
+
633
+ For computed-to-computed dependencies you can use `OIMEffectDependencyComputed`.
634
+
635
+ ```typescript
636
+ import {
637
+ OIMComputed,
638
+ OIMEffect,
639
+ OIMComputativeRuntime,
640
+ OIMEffectDependencyComputed,
641
+ OIMEventQueue,
642
+ OIMReactiveObject,
643
+ OIMEffectDependencyKeyedObject,
644
+ } from '@oimdb/core';
645
+
646
+ type TKey = 'a';
647
+ const queue = new OIMEventQueue();
648
+ const runtime = new OIMComputativeRuntime(queue);
649
+ const obj = new OIMReactiveObject<TKey, number>(queue);
650
+
651
+ const A = new OIMComputed<number>(runtime, {
652
+ compute: () => (obj.get('a') ?? 0) + 1,
653
+ deps: [new OIMEffectDependencyKeyedObject(obj, 'a')],
654
+ });
655
+
656
+ const B = new OIMComputed<number>(runtime, {
657
+ compute: () => A.get() * 2,
658
+ deps: [new OIMEffectDependencyComputed({ emitter: A.emitter, updateEventEmitter: A.updateEventEmitter })],
659
+ });
660
+
661
+ const effect = new OIMEffect(runtime, {
662
+ deps: [new OIMEffectDependencyComputed({ emitter: B.emitter, updateEventEmitter: B.updateEventEmitter })],
663
+ run: () => console.log('B changed'),
664
+ });
665
+
666
+ obj.setProperty('a', 1);
667
+ queue.flush(); // run scheduled work
668
+
669
+ effect.destroy();
670
+ B.destroy();
671
+ A.destroy();
672
+ obj.destroy();
673
+ queue.destroy();
674
+ ```
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
+
816
+ #### Gotchas (read this once)
817
+
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.
819
+ - **Keep `compute()` pure**: treat `compute()` as a pure function over current state. Doing writes inside `compute()` will create hard-to-debug re-entrancy.
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.
823
+
363
824
  ### Scheduler Types
364
825
 
365
826
  Choose the right scheduler for your use case:
@@ -373,7 +834,7 @@ Choose the right scheduler for your use case:
373
834
 
374
835
  ```
375
836
  OIMCollection (base)
376
- ├── OIMReactiveCollection (adds event emitter + coalescing)
837
+ ├── OIMReactiveCollection (adds updateEventEmitter wired to the queue)
377
838
  └── OIMRICollection (reactive collection + indexes)
378
839
 
379
840
  OIMIndexSetBased (base for Set-based)
@@ -498,7 +959,7 @@ new OIMReactiveCollection(queue: OIMEventQueue, opts?: TOIMCollectionOptions<TEn
498
959
  **Properties:**
499
960
  - `collection: OIMCollection<TEntity, TPk>` - Underlying collection
500
961
  - `updateEventEmitter: OIMUpdateEventEmitter<TPk>` - Key-specific subscriptions
501
- - `coalescer: OIMUpdateEventCoalescerCollection<TPk>` - Event coalescing
962
+ - Event batching/deduplication is handled internally by `OIMUpdateEventEmitter`
502
963
 
503
964
  **Methods:**
504
965
  - `upsertOne(entity: TEntity): void` - Insert or update single entity
@@ -529,7 +990,10 @@ Reactive Set-based index with manual key-to-entity mapping and change notificati
529
990
  **Constructor:**
530
991
  ```typescript
531
992
  new OIMReactiveIndexManualSetBased(queue: OIMEventQueue, opts?: {
532
- index?: OIMIndexManualSetBased<TKey, TPk>
993
+ indexOptions?: {
994
+ comparePks?: TOIMIndexComparator<TPk>;
995
+ store?: OIMIndexStoreSetBased<TKey, TPk>;
996
+ }
533
997
  })
534
998
  ```
535
999
 
@@ -552,7 +1016,10 @@ Reactive Array-based index with manual key-to-entity mapping and change notifica
552
1016
  **Constructor:**
553
1017
  ```typescript
554
1018
  new OIMReactiveIndexManualArrayBased(queue: OIMEventQueue, opts?: {
555
- index?: OIMIndexManualArrayBased<TKey, TPk>
1019
+ indexOptions?: {
1020
+ comparePks?: TOIMIndexComparator<TPk>;
1021
+ store?: OIMIndexStoreArrayBased<TKey, TPk>;
1022
+ }
556
1023
  })
557
1024
  ```
558
1025