@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 +482 -15
- package/dist/index.cjs +1997 -335
- package/dist/index.d.cts +685 -151
- package/dist/index.d.ts +685 -151
- package/dist/index.js +1968 -330
- package/package.json +1 -1
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-
|
|
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', (
|
|
117
|
-
console.log('Admin users changed:',
|
|
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', (
|
|
149
|
-
console.log('Deck cards changed:',
|
|
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
|
-
//
|
|
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) →
|
|
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
|
|
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
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
1019
|
+
indexOptions?: {
|
|
1020
|
+
comparePks?: TOIMIndexComparator<TPk>;
|
|
1021
|
+
store?: OIMIndexStoreArrayBased<TKey, TPk>;
|
|
1022
|
+
}
|
|
556
1023
|
})
|
|
557
1024
|
```
|
|
558
1025
|
|