@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 +369 -50
- package/dist/index.cjs +1409 -712
- package/dist/index.d.cts +387 -273
- package/dist/index.d.ts +387 -273
- package/dist/index.js +1397 -703
- 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,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
|
|
364
|
-
|
|
365
|
-
OIMDB uses a **two-phase** update model inside a queue flush:
|
|
368
|
+
### Effects, Computed, and the Event Lifecycle
|
|
366
369
|
|
|
367
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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>(
|
|
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(); //
|
|
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
|
|
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(); //
|
|
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
|
-
|
|
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>(
|
|
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>(
|
|
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(
|
|
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(); //
|
|
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
|
-
- **
|
|
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
|
|
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
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
1019
|
+
indexOptions?: {
|
|
1020
|
+
comparePks?: TOIMIndexComparator<TPk>;
|
|
1021
|
+
store?: OIMIndexStoreArrayBased<TKey, TPk>;
|
|
1022
|
+
}
|
|
704
1023
|
})
|
|
705
1024
|
```
|
|
706
1025
|
|