@oimdb/core 1.4.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +396 -63
- package/dist/index.cjs +1706 -799
- package/dist/index.d.cts +456 -292
- package/dist/index.d.ts +456 -292
- package/dist/index.js +1694 -790
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -21,24 +21,32 @@ 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
|
-
- **OIMIndexManualSetBased**: Set-based manual index (
|
|
32
|
-
- **OIMIndexManualArrayBased**: Array-based manual index (
|
|
33
|
-
- **OIMIndexStoreMapDrivenSetBased**: Set-based index storage backend
|
|
34
|
-
- **OIMIndexStoreMapDrivenArrayBased**: Array-based index storage backend
|
|
39
|
+
- **OIMIndexManualSetBased**: Set-based manual index (stores slots, projects to `Set<TPk>`)
|
|
40
|
+
- **OIMIndexManualArrayBased**: Array-based manual index (stores slots, projects to `TPk[]`)
|
|
41
|
+
- **OIMIndexStoreMapDrivenSetBased**: Set-based slot index storage backend
|
|
42
|
+
- **OIMIndexStoreMapDrivenArrayBased**: Array-based slot index storage backend
|
|
35
43
|
- **OIMMap2Keys**: Two-key mapping utilities for complex indexing
|
|
36
44
|
|
|
37
45
|
### Abstract Classes & Interfaces
|
|
38
46
|
- **OIMCollectionStore**: Storage backend interface
|
|
39
47
|
- **OIMEventQueueScheduler**: Event processing scheduler interface
|
|
40
|
-
- **OIMIndexSetBased**: Base Set-based index interface (
|
|
41
|
-
- **OIMIndexArrayBased**: Base Array-based index interface (
|
|
48
|
+
- **OIMIndexSetBased**: Base Set-based index interface (slot-backed, projects to `Set<TPk>`)
|
|
49
|
+
- **OIMIndexArrayBased**: Base Array-based index interface (slot-backed, projects to `TPk[]`)
|
|
42
50
|
- **OIMReactiveIndexSetBased**: Reactive Set-based index interface
|
|
43
51
|
- **OIMReactiveIndexArrayBased**: Reactive Array-based index interface
|
|
44
52
|
|
|
@@ -100,6 +108,8 @@ const multipleUsers = users.getManyByPks(['user1', 'user2']);
|
|
|
100
108
|
|
|
101
109
|
OIMDB provides two types of indexes optimized for different use cases:
|
|
102
110
|
|
|
111
|
+
Indexes are slot-backed internally. Collections own canonical `{ pk, item }` slots, while indexes store references to those slots. `getPksByKey` remains available as a PK projection (`Set<TPk>` for SetBased, `TPk[]` for ArrayBased), and entity-by-index selectors/hooks read `slot.item` directly for faster reads.
|
|
112
|
+
|
|
103
113
|
#### SetBased Indexes (for incremental updates)
|
|
104
114
|
|
|
105
115
|
```typescript
|
|
@@ -113,8 +123,8 @@ const queue = new OIMEventQueue({
|
|
|
113
123
|
const userRoleIndex = new OIMReactiveIndexManualSetBased<string, string>(queue);
|
|
114
124
|
|
|
115
125
|
// Subscribe to specific index key changes
|
|
116
|
-
userRoleIndex.updateEventEmitter.subscribeOnKey('admin', (
|
|
117
|
-
console.log('Admin users changed:',
|
|
126
|
+
userRoleIndex.updateEventEmitter.subscribeOnKey('admin', () => {
|
|
127
|
+
console.log('Admin users changed:', userRoleIndex.getPksByKey('admin')); // Set<string>
|
|
118
128
|
});
|
|
119
129
|
|
|
120
130
|
// Build the index manually
|
|
@@ -145,8 +155,8 @@ const queue = new OIMEventQueue({
|
|
|
145
155
|
const cardsByDeckIndex = new OIMReactiveIndexManualArrayBased<string, string>(queue);
|
|
146
156
|
|
|
147
157
|
// Subscribe to specific index key changes
|
|
148
|
-
cardsByDeckIndex.updateEventEmitter.subscribeOnKey('deck1', (
|
|
149
|
-
console.log('Deck cards changed:',
|
|
158
|
+
cardsByDeckIndex.updateEventEmitter.subscribeOnKey('deck1', () => {
|
|
159
|
+
console.log('Deck cards changed:', cardsByDeckIndex.getPksByKey('deck1')); // string[]
|
|
150
160
|
});
|
|
151
161
|
|
|
152
162
|
// Build the index manually - set full array
|
|
@@ -326,10 +336,7 @@ users.upsertOne({ id: 'user1', email: 'john@example.com' });
|
|
|
326
336
|
|
|
327
337
|
// Only one notification will fire (in next microtask)
|
|
328
338
|
|
|
329
|
-
//
|
|
330
|
-
users.coalescer.emitter.on('upsert', (coalescedUpdates) => {
|
|
331
|
-
console.log('Raw coalesced updates:', coalescedUpdates);
|
|
332
|
-
});
|
|
339
|
+
// (No separate "coalescer" object exists: batching/deduplication is handled inside OIMUpdateEventEmitter.)
|
|
333
340
|
```
|
|
334
341
|
|
|
335
342
|
## 🔄 Reactive Architecture
|
|
@@ -340,7 +347,7 @@ OIMDB core uses a reactive architecture where changes automatically trigger noti
|
|
|
340
347
|
|
|
341
348
|
```typescript
|
|
342
349
|
// Collection updates trigger events through the event queue
|
|
343
|
-
collection.upsertOne(entity) →
|
|
350
|
+
collection.upsertOne(entity) → updateEventEmitter → event queue → subscribers
|
|
344
351
|
|
|
345
352
|
// Key-specific subscriptions only notify when relevant data changes
|
|
346
353
|
updateEventEmitter.subscribeOnKey('user1', callback) // Only fires for user1 changes
|
|
@@ -360,31 +367,22 @@ users.upsertOne({ id: 'user1', role: 'admin' });
|
|
|
360
367
|
// This prevents unnecessary re-renders and improves performance
|
|
361
368
|
```
|
|
362
369
|
|
|
363
|
-
### Effects, Computed, and the Event Lifecycle
|
|
364
|
-
|
|
365
|
-
OIMDB uses a **two-phase** update model inside a queue flush:
|
|
366
|
-
|
|
367
|
-
- **PRE phase**: derived logic (e.g. `OIMComputed`) and **pure** effects run here
|
|
368
|
-
- **HANDLERS phase**: regular subscribers run here (UI updates, integrations, etc.)
|
|
369
|
-
|
|
370
|
-
**Important rules / guarantees:**
|
|
370
|
+
### Effects, Computed, and the Event Lifecycle
|
|
371
371
|
|
|
372
|
-
|
|
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
|
|
372
|
+
OIMDB uses a single-pass flush boundary: `queue.flush()` executes the current batch of pending work.
|
|
377
373
|
|
|
378
|
-
|
|
374
|
+
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
375
|
|
|
380
376
|
#### What is an Effect?
|
|
381
377
|
|
|
382
378
|
`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
379
|
|
|
380
|
+
**Basic example with reactive object:**
|
|
381
|
+
|
|
384
382
|
```typescript
|
|
385
383
|
import {
|
|
386
384
|
OIMEffect,
|
|
387
|
-
|
|
385
|
+
OIMComputativeRuntime,
|
|
388
386
|
OIMEventQueue,
|
|
389
387
|
OIMReactiveObject,
|
|
390
388
|
OIMEffectDependencyKeyedObject,
|
|
@@ -393,10 +391,10 @@ import {
|
|
|
393
391
|
type TKey = 'a';
|
|
394
392
|
|
|
395
393
|
const queue = new OIMEventQueue();
|
|
394
|
+
const runtime = new OIMComputativeRuntime(queue);
|
|
396
395
|
const obj = new OIMReactiveObject<TKey, number>(queue);
|
|
397
396
|
|
|
398
|
-
const effect = new OIMEffect(
|
|
399
|
-
phase: EOIMEffectPhase.HANDLERS,
|
|
397
|
+
const effect = new OIMEffect(runtime, {
|
|
400
398
|
deps: [new OIMEffectDependencyKeyedObject(obj, 'a')],
|
|
401
399
|
run: () => {
|
|
402
400
|
console.log('obj.a changed');
|
|
@@ -411,40 +409,154 @@ obj.destroy();
|
|
|
411
409
|
queue.destroy();
|
|
412
410
|
```
|
|
413
411
|
|
|
412
|
+
**Effect with collection dependency:**
|
|
413
|
+
|
|
414
|
+
```typescript
|
|
415
|
+
import {
|
|
416
|
+
OIMEffect,
|
|
417
|
+
OIMComputativeRuntime,
|
|
418
|
+
OIMEventQueue,
|
|
419
|
+
OIMReactiveCollection,
|
|
420
|
+
OIMEffectDependencyKeyedCollection,
|
|
421
|
+
} from '@oimdb/core';
|
|
422
|
+
|
|
423
|
+
interface User {
|
|
424
|
+
id: string;
|
|
425
|
+
name: string;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const queue = new OIMEventQueue();
|
|
429
|
+
const runtime = new OIMComputativeRuntime(queue);
|
|
430
|
+
const users = new OIMReactiveCollection<User, string>(queue, {
|
|
431
|
+
selectPk: (u) => u.id,
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
const effect = new OIMEffect(runtime, {
|
|
435
|
+
deps: [new OIMEffectDependencyKeyedCollection(users, 'user1')],
|
|
436
|
+
run: () => {
|
|
437
|
+
const user = users.getOneByPk('user1');
|
|
438
|
+
console.log('User1 changed:', user);
|
|
439
|
+
},
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
users.upsertOne({ id: 'user1', name: 'John' });
|
|
443
|
+
queue.flush();
|
|
444
|
+
|
|
445
|
+
effect.destroy();
|
|
446
|
+
users.destroy();
|
|
447
|
+
queue.destroy();
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
**Effect with index dependency:**
|
|
451
|
+
|
|
452
|
+
```typescript
|
|
453
|
+
import {
|
|
454
|
+
OIMEffect,
|
|
455
|
+
OIMComputativeRuntime,
|
|
456
|
+
OIMEventQueue,
|
|
457
|
+
OIMReactiveIndexManualSetBased,
|
|
458
|
+
OIMEffectDependencyKeyedIndex,
|
|
459
|
+
} from '@oimdb/core';
|
|
460
|
+
|
|
461
|
+
const queue = new OIMEventQueue();
|
|
462
|
+
const runtime = new OIMComputativeRuntime(queue);
|
|
463
|
+
const roleIndex = new OIMReactiveIndexManualSetBased<string, string>(queue);
|
|
464
|
+
|
|
465
|
+
const effect = new OIMEffect(runtime, {
|
|
466
|
+
deps: [new OIMEffectDependencyKeyedIndex(roleIndex, 'admin')],
|
|
467
|
+
run: () => {
|
|
468
|
+
const adminPks = roleIndex.getPksByKey('admin');
|
|
469
|
+
console.log('Admin users changed:', Array.from(adminPks));
|
|
470
|
+
},
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
roleIndex.setPks('admin', ['user1', 'user2']);
|
|
474
|
+
queue.flush();
|
|
475
|
+
|
|
476
|
+
effect.destroy();
|
|
477
|
+
roleIndex.destroy();
|
|
478
|
+
queue.destroy();
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
**Effect with multiple dependencies:**
|
|
482
|
+
|
|
483
|
+
```typescript
|
|
484
|
+
import {
|
|
485
|
+
OIMEffect,
|
|
486
|
+
OIMComputativeRuntime,
|
|
487
|
+
OIMEventQueue,
|
|
488
|
+
OIMReactiveObject,
|
|
489
|
+
OIMReactiveCollection,
|
|
490
|
+
OIMEffectDependencyKeyedObject,
|
|
491
|
+
OIMEffectDependencyKeyedCollection,
|
|
492
|
+
} from '@oimdb/core';
|
|
493
|
+
|
|
494
|
+
const queue = new OIMEventQueue();
|
|
495
|
+
const runtime = new OIMComputativeRuntime(queue);
|
|
496
|
+
const settings = new OIMReactiveObject<'theme' | 'lang', string>(queue);
|
|
497
|
+
const users = new OIMReactiveCollection<User, string>(queue, {
|
|
498
|
+
selectPk: (u) => u.id,
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
const effect = new OIMEffect(runtime, {
|
|
502
|
+
deps: [
|
|
503
|
+
new OIMEffectDependencyKeyedObject(settings, ['theme', 'lang']),
|
|
504
|
+
new OIMEffectDependencyKeyedCollection(users, 'currentUser'),
|
|
505
|
+
],
|
|
506
|
+
run: () => {
|
|
507
|
+
const theme = settings.get('theme');
|
|
508
|
+
const user = users.getOneByPk('currentUser');
|
|
509
|
+
console.log('Settings or user changed:', { theme, user });
|
|
510
|
+
},
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
settings.setProperty('theme', 'dark');
|
|
514
|
+
users.upsertOne({ id: 'currentUser', name: 'John' });
|
|
515
|
+
queue.flush(); // Effect runs once, even though multiple deps changed
|
|
516
|
+
|
|
517
|
+
effect.destroy();
|
|
518
|
+
settings.destroy();
|
|
519
|
+
users.destroy();
|
|
520
|
+
queue.destroy();
|
|
521
|
+
```
|
|
522
|
+
|
|
414
523
|
#### What is a Computed?
|
|
415
524
|
|
|
416
|
-
`OIMComputed<T>` is built on top of `OIMEffect
|
|
525
|
+
`OIMComputed<T>` is built on top of `OIMEffect`: it recomputes a derived value and emits `update` when the value changes.
|
|
526
|
+
|
|
527
|
+
**Basic example:**
|
|
417
528
|
|
|
418
529
|
```typescript
|
|
419
530
|
import {
|
|
420
531
|
OIMComputed,
|
|
421
532
|
OIMEventQueue,
|
|
533
|
+
OIMComputativeRuntime,
|
|
422
534
|
OIMReactiveObject,
|
|
423
535
|
OIMEffectDependencyKeyedObject,
|
|
424
536
|
} from '@oimdb/core';
|
|
425
537
|
|
|
426
538
|
type TKey = 'a';
|
|
427
539
|
const queue = new OIMEventQueue();
|
|
540
|
+
const runtime = new OIMComputativeRuntime(queue);
|
|
428
541
|
const obj = new OIMReactiveObject<TKey, number>(queue);
|
|
429
542
|
|
|
430
|
-
const doubled = new OIMComputed<number>(
|
|
543
|
+
const doubled = new OIMComputed<number>(runtime, {
|
|
431
544
|
compute: () => (obj.get('a') ?? 0) * 2,
|
|
432
545
|
deps: [new OIMEffectDependencyKeyedObject(obj, 'a')],
|
|
433
546
|
});
|
|
434
547
|
|
|
435
548
|
obj.setProperty('a', 10);
|
|
436
|
-
queue.flush(); //
|
|
549
|
+
queue.flush(); // run scheduled work
|
|
437
550
|
|
|
438
551
|
console.log(doubled.get()); // 20
|
|
439
552
|
|
|
440
|
-
// If you also subscribe to computed updates, delivery
|
|
553
|
+
// If you also subscribe to computed updates, delivery happens in the same drain flush:
|
|
441
554
|
let calls = 0;
|
|
442
555
|
doubled.updateEventEmitter.subscribeOnKey('value', () => {
|
|
443
556
|
calls++;
|
|
444
557
|
});
|
|
445
558
|
obj.setProperty('a', 11);
|
|
446
|
-
queue.flush(); //
|
|
447
|
-
queue.flush(); // subscriber delivery (HANDLERS via queue snapshot)
|
|
559
|
+
queue.flush(); // run scheduled work
|
|
448
560
|
console.log(calls); // 1
|
|
449
561
|
|
|
450
562
|
doubled.destroy();
|
|
@@ -452,18 +564,81 @@ obj.destroy();
|
|
|
452
564
|
queue.destroy();
|
|
453
565
|
```
|
|
454
566
|
|
|
567
|
+
**Computed with collection and index dependencies:**
|
|
568
|
+
|
|
569
|
+
```typescript
|
|
570
|
+
import {
|
|
571
|
+
OIMComputed,
|
|
572
|
+
OIMEventQueue,
|
|
573
|
+
OIMComputativeRuntime,
|
|
574
|
+
OIMReactiveCollection,
|
|
575
|
+
OIMReactiveIndexManualSetBased,
|
|
576
|
+
OIMEffectDependencyKeyedCollection,
|
|
577
|
+
OIMEffectDependencyKeyedIndex,
|
|
578
|
+
} from '@oimdb/core';
|
|
579
|
+
|
|
580
|
+
interface User {
|
|
581
|
+
id: string;
|
|
582
|
+
name: string;
|
|
583
|
+
role: string;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
const queue = new OIMEventQueue();
|
|
587
|
+
const runtime = new OIMComputativeRuntime(queue);
|
|
588
|
+
const users = new OIMReactiveCollection<User, string>(queue, {
|
|
589
|
+
selectPk: (u) => u.id,
|
|
590
|
+
});
|
|
591
|
+
const roleIndex = new OIMReactiveIndexManualSetBased<string, string>(queue);
|
|
592
|
+
|
|
593
|
+
// Computed that counts admin users
|
|
594
|
+
const adminCount = new OIMComputed<number>(runtime, {
|
|
595
|
+
compute: () => {
|
|
596
|
+
const adminPks = roleIndex.getPksByKey('admin');
|
|
597
|
+
return adminPks.size;
|
|
598
|
+
},
|
|
599
|
+
deps: [new OIMEffectDependencyKeyedIndex(roleIndex, 'admin')],
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
// Computed that gets admin user names
|
|
603
|
+
const adminNames = new OIMComputed<string[]>(runtime, {
|
|
604
|
+
compute: () => {
|
|
605
|
+
const adminPks = Array.from(roleIndex.getPksByKey('admin'));
|
|
606
|
+
return adminPks
|
|
607
|
+
.map((pk) => users.getOneByPk(pk)?.name)
|
|
608
|
+
.filter((name): name is string => name !== undefined);
|
|
609
|
+
},
|
|
610
|
+
deps: [
|
|
611
|
+
new OIMEffectDependencyKeyedIndex(roleIndex, 'admin'),
|
|
612
|
+
new OIMEffectDependencyKeyedCollection(users, Array.from(roleIndex.getPksByKey('admin'))),
|
|
613
|
+
],
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
users.upsertMany([
|
|
617
|
+
{ id: 'u1', name: 'Alice', role: 'admin' },
|
|
618
|
+
{ id: 'u2', name: 'Bob', role: 'user' },
|
|
619
|
+
]);
|
|
620
|
+
roleIndex.setPks('admin', ['u1']);
|
|
621
|
+
queue.flush();
|
|
622
|
+
|
|
623
|
+
console.log(adminCount.get()); // 1
|
|
624
|
+
console.log(adminNames.get()); // ['Alice']
|
|
625
|
+
|
|
626
|
+
adminNames.destroy();
|
|
627
|
+
adminCount.destroy();
|
|
628
|
+
roleIndex.destroy();
|
|
629
|
+
users.destroy();
|
|
630
|
+
queue.destroy();
|
|
631
|
+
```
|
|
632
|
+
|
|
455
633
|
#### Computed-to-Computed dependencies
|
|
456
634
|
|
|
457
635
|
For computed-to-computed dependencies you can use `OIMEffectDependencyComputed`.
|
|
458
636
|
|
|
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
637
|
```typescript
|
|
463
638
|
import {
|
|
464
639
|
OIMComputed,
|
|
465
640
|
OIMEffect,
|
|
466
|
-
|
|
641
|
+
OIMComputativeRuntime,
|
|
467
642
|
OIMEffectDependencyComputed,
|
|
468
643
|
OIMEventQueue,
|
|
469
644
|
OIMReactiveObject,
|
|
@@ -472,27 +647,26 @@ import {
|
|
|
472
647
|
|
|
473
648
|
type TKey = 'a';
|
|
474
649
|
const queue = new OIMEventQueue();
|
|
650
|
+
const runtime = new OIMComputativeRuntime(queue);
|
|
475
651
|
const obj = new OIMReactiveObject<TKey, number>(queue);
|
|
476
652
|
|
|
477
|
-
const A = new OIMComputed<number>(
|
|
653
|
+
const A = new OIMComputed<number>(runtime, {
|
|
478
654
|
compute: () => (obj.get('a') ?? 0) + 1,
|
|
479
655
|
deps: [new OIMEffectDependencyKeyedObject(obj, 'a')],
|
|
480
656
|
});
|
|
481
657
|
|
|
482
|
-
const B = new OIMComputed<number>(
|
|
658
|
+
const B = new OIMComputed<number>(runtime, {
|
|
483
659
|
compute: () => A.get() * 2,
|
|
484
660
|
deps: [new OIMEffectDependencyComputed({ emitter: A.emitter, updateEventEmitter: A.updateEventEmitter })],
|
|
485
661
|
});
|
|
486
662
|
|
|
487
|
-
const effect = new OIMEffect(
|
|
488
|
-
phase: EOIMEffectPhase.HANDLERS,
|
|
663
|
+
const effect = new OIMEffect(runtime, {
|
|
489
664
|
deps: [new OIMEffectDependencyComputed({ emitter: B.emitter, updateEventEmitter: B.updateEventEmitter })],
|
|
490
665
|
run: () => console.log('B changed'),
|
|
491
666
|
});
|
|
492
667
|
|
|
493
668
|
obj.setProperty('a', 1);
|
|
494
|
-
queue.flush(); //
|
|
495
|
-
queue.flush(); // handlers delivered via the queue snapshot
|
|
669
|
+
queue.flush(); // run scheduled work
|
|
496
670
|
|
|
497
671
|
effect.destroy();
|
|
498
672
|
B.destroy();
|
|
@@ -501,12 +675,153 @@ obj.destroy();
|
|
|
501
675
|
queue.destroy();
|
|
502
676
|
```
|
|
503
677
|
|
|
678
|
+
#### What are Selectors?
|
|
679
|
+
|
|
680
|
+
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.
|
|
681
|
+
|
|
682
|
+
**Collection selector:**
|
|
683
|
+
|
|
684
|
+
```typescript
|
|
685
|
+
import {
|
|
686
|
+
OIMCollectionByPkSelector,
|
|
687
|
+
OIMCollectionByPksSelector,
|
|
688
|
+
OIMComputativeRuntime,
|
|
689
|
+
OIMEventQueue,
|
|
690
|
+
OIMReactiveCollection,
|
|
691
|
+
} from '@oimdb/core';
|
|
692
|
+
|
|
693
|
+
interface User {
|
|
694
|
+
id: string;
|
|
695
|
+
name: string;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
const queue = new OIMEventQueue();
|
|
699
|
+
const runtime = new OIMComputativeRuntime(queue);
|
|
700
|
+
const users = new OIMReactiveCollection<User, string>(queue, {
|
|
701
|
+
selectPk: (u) => u.id,
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
// Watch a single user
|
|
705
|
+
const userSelector = new OIMCollectionByPkSelector(runtime, users, 'user1');
|
|
706
|
+
const unwatch = userSelector.watch((user) => {
|
|
707
|
+
console.log('User1 changed:', user);
|
|
708
|
+
});
|
|
709
|
+
|
|
710
|
+
users.upsertOne({ id: 'user1', name: 'John' });
|
|
711
|
+
queue.flush(); // Callback fires with { id: 'user1', name: 'John' }
|
|
712
|
+
|
|
713
|
+
// Watch multiple users
|
|
714
|
+
const usersSelector = new OIMCollectionByPksSelector(runtime, users, ['user1', 'user2']);
|
|
715
|
+
usersSelector.watch((users) => {
|
|
716
|
+
console.log('Users changed:', users);
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
users.upsertMany([
|
|
720
|
+
{ id: 'user1', name: 'John Doe' },
|
|
721
|
+
{ id: 'user2', name: 'Jane Smith' },
|
|
722
|
+
]);
|
|
723
|
+
queue.flush(); // Callback fires with array of users
|
|
724
|
+
|
|
725
|
+
unwatch(); // Stop watching
|
|
726
|
+
usersSelector.watch(() => {}); // Get unsubscribe function
|
|
727
|
+
users.destroy();
|
|
728
|
+
queue.destroy();
|
|
729
|
+
```
|
|
730
|
+
|
|
731
|
+
**Selector with index (entities by index key):**
|
|
732
|
+
|
|
733
|
+
```typescript
|
|
734
|
+
import {
|
|
735
|
+
OIMEntitiesByIndexKeySetBasedSelector,
|
|
736
|
+
OIMComputativeRuntime,
|
|
737
|
+
OIMEventQueue,
|
|
738
|
+
OIMReactiveCollection,
|
|
739
|
+
OIMReactiveIndexManualSetBased,
|
|
740
|
+
} from '@oimdb/core';
|
|
741
|
+
|
|
742
|
+
const queue = new OIMEventQueue();
|
|
743
|
+
const runtime = new OIMComputativeRuntime(queue);
|
|
744
|
+
const users = new OIMReactiveCollection<User, string>(queue, {
|
|
745
|
+
selectPk: (u) => u.id,
|
|
746
|
+
});
|
|
747
|
+
const roleIndex = new OIMReactiveIndexManualSetBased<string, string>(queue);
|
|
748
|
+
|
|
749
|
+
// Watch all admin users
|
|
750
|
+
const adminUsersSelector = new OIMEntitiesByIndexKeySetBasedSelector(
|
|
751
|
+
runtime,
|
|
752
|
+
users,
|
|
753
|
+
roleIndex,
|
|
754
|
+
'admin'
|
|
755
|
+
);
|
|
756
|
+
|
|
757
|
+
adminUsersSelector.watch((adminUsers) => {
|
|
758
|
+
console.log('Admin users:', adminUsers.map((u) => u?.name));
|
|
759
|
+
});
|
|
760
|
+
|
|
761
|
+
users.upsertMany([
|
|
762
|
+
{ id: 'u1', name: 'Alice', role: 'admin' },
|
|
763
|
+
{ id: 'u2', name: 'Bob', role: 'admin' },
|
|
764
|
+
]);
|
|
765
|
+
roleIndex.setPks('admin', ['u1', 'u2']);
|
|
766
|
+
queue.flush(); // Callback fires with [Alice, Bob]
|
|
767
|
+
|
|
768
|
+
// When index changes, selector automatically resubscribes to new entities
|
|
769
|
+
roleIndex.setPks('admin', ['u1']);
|
|
770
|
+
queue.flush(); // Callback fires with [Alice]
|
|
771
|
+
|
|
772
|
+
adminUsersSelector.watch(() => {}); // Get unsubscribe function
|
|
773
|
+
roleIndex.destroy();
|
|
774
|
+
users.destroy();
|
|
775
|
+
queue.destroy();
|
|
776
|
+
```
|
|
777
|
+
|
|
778
|
+
**Object selector:**
|
|
779
|
+
|
|
780
|
+
```typescript
|
|
781
|
+
import {
|
|
782
|
+
OIMObjectValueByKeySelector,
|
|
783
|
+
OIMObjectValuesByKeysSelector,
|
|
784
|
+
OIMComputativeRuntime,
|
|
785
|
+
OIMEventQueue,
|
|
786
|
+
OIMReactiveObject,
|
|
787
|
+
} from '@oimdb/core';
|
|
788
|
+
|
|
789
|
+
const queue = new OIMEventQueue();
|
|
790
|
+
const runtime = new OIMComputativeRuntime(queue);
|
|
791
|
+
const settings = new OIMReactiveObject<'theme' | 'lang', string>(queue);
|
|
792
|
+
|
|
793
|
+
// Watch single key
|
|
794
|
+
const themeSelector = new OIMObjectValueByKeySelector(runtime, settings, 'theme');
|
|
795
|
+
themeSelector.watch((theme) => {
|
|
796
|
+
console.log('Theme changed:', theme);
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
// Watch multiple keys
|
|
800
|
+
const settingsSelector = new OIMObjectValuesByKeysSelector(runtime, settings, ['theme', 'lang']);
|
|
801
|
+
settingsSelector.watch((values) => {
|
|
802
|
+
console.log('Settings changed:', values); // [theme, lang]
|
|
803
|
+
});
|
|
804
|
+
|
|
805
|
+
settings.setProperty('theme', 'dark');
|
|
806
|
+
queue.flush();
|
|
807
|
+
|
|
808
|
+
settings.destroy();
|
|
809
|
+
queue.destroy();
|
|
810
|
+
```
|
|
811
|
+
|
|
812
|
+
**Key differences: Effects vs Selectors:**
|
|
813
|
+
|
|
814
|
+
- **Effects** (`OIMEffect`): Run side effects when dependencies change. Use for logging, API calls, UI updates.
|
|
815
|
+
- **Selectors** (`OIMSelector`): Watch and deliver values only when they actually change. Use for reactive data access with automatic change detection.
|
|
816
|
+
- **Computed** (`OIMComputed`): Derive values from dependencies. Use for calculated/transformed data.
|
|
817
|
+
|
|
504
818
|
#### Gotchas (read this once)
|
|
505
819
|
|
|
506
820
|
- **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
821
|
- **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.
|
|
822
|
+
- **Keep effects safe**: if you need to write to stores or trigger IO, do it from `OIMEffect`, but avoid creating endless update loops.
|
|
823
|
+
- **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.
|
|
824
|
+
- **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
825
|
|
|
511
826
|
### Scheduler Types
|
|
512
827
|
|
|
@@ -521,7 +836,7 @@ Choose the right scheduler for your use case:
|
|
|
521
836
|
|
|
522
837
|
```
|
|
523
838
|
OIMCollection (base)
|
|
524
|
-
├── OIMReactiveCollection (adds
|
|
839
|
+
├── OIMReactiveCollection (adds updateEventEmitter wired to the queue)
|
|
525
840
|
└── OIMRICollection (reactive collection + indexes)
|
|
526
841
|
|
|
527
842
|
OIMIndexSetBased (base for Set-based)
|
|
@@ -549,13 +864,17 @@ OIMIndexArrayBased (base for Array-based)
|
|
|
549
864
|
### Index Performance
|
|
550
865
|
|
|
551
866
|
**SetBased Indexes** (`OIMReactiveIndexManualSetBased`):
|
|
552
|
-
- **
|
|
867
|
+
- **Storage**: Set of canonical entity slots
|
|
868
|
+
- **PK projection**: `getPksByKey` returns `Set<TPk>` for efficient membership checks
|
|
869
|
+
- **Entity reads**: selectors/hooks read `slot.item` directly, avoiding per-item collection lookups
|
|
553
870
|
- **Best for**: Frequent incremental updates using `addPks`/`removePks`
|
|
554
871
|
- **Performance**: O(1) add/remove operations, O(n) for `setPks` (requires Set creation)
|
|
555
872
|
- **Use case**: When you need to frequently add/remove individual items
|
|
556
873
|
|
|
557
874
|
**ArrayBased Indexes** (`OIMReactiveIndexManualArrayBased`):
|
|
558
|
-
- **
|
|
875
|
+
- **Storage**: Array of canonical entity slots
|
|
876
|
+
- **PK projection**: `getPksByKey` returns `TPk[]` for direct array access
|
|
877
|
+
- **Entity reads**: selectors/hooks read `slot.item` directly, preserving array order
|
|
559
878
|
- **Best for**: Full array replacements using `setPks`
|
|
560
879
|
- **Performance**: O(1) `setPks` operation (direct assignment, no diff computation)
|
|
561
880
|
- **Use case**: When you typically replace the entire array (e.g., deck cards, ordered lists) or when you need to preserve element order/sorting
|
|
@@ -646,7 +965,7 @@ new OIMReactiveCollection(queue: OIMEventQueue, opts?: TOIMCollectionOptions<TEn
|
|
|
646
965
|
**Properties:**
|
|
647
966
|
- `collection: OIMCollection<TEntity, TPk>` - Underlying collection
|
|
648
967
|
- `updateEventEmitter: OIMUpdateEventEmitter<TPk>` - Key-specific subscriptions
|
|
649
|
-
-
|
|
968
|
+
- Event batching/deduplication is handled internally by `OIMUpdateEventEmitter`
|
|
650
969
|
|
|
651
970
|
**Methods:**
|
|
652
971
|
- `upsertOne(entity: TEntity): void` - Insert or update single entity
|
|
@@ -654,7 +973,9 @@ new OIMReactiveCollection(queue: OIMEventQueue, opts?: TOIMCollectionOptions<TEn
|
|
|
654
973
|
- `removeOne(entity: TEntity): void` - Remove single entity
|
|
655
974
|
- `removeMany(entities: TEntity[]): void` - Remove multiple entities
|
|
656
975
|
- `getOneByPk(pk: TPk): TEntity | undefined` - Get entity by primary key
|
|
657
|
-
- `getManyByPks(pks: readonly TPk[]):
|
|
976
|
+
- `getManyByPks(pks: readonly TPk[]): TEntity[]` - Get existing entities for primary keys
|
|
977
|
+
- `getSlotByPk(pk: TPk): OIMEntitySlot<TEntity, TPk> | undefined` - Get the canonical slot for a primary key
|
|
978
|
+
- `getSlotsByPks(pks: readonly TPk[]): OIMEntitySlot<TEntity, TPk>[]` - Get existing canonical slots for primary keys
|
|
658
979
|
|
|
659
980
|
#### `OIMRICollection<TEntity, TPk, TIndexName, TIndexKey, TIndex, TReactiveIndex, TReactiveIndexMap>`
|
|
660
981
|
Reactive collection with integrated indexing capabilities.
|
|
@@ -672,12 +993,16 @@ new OIMRICollection(queue: OIMEventQueue, opts: {
|
|
|
672
993
|
- *(inherits all OIMReactiveCollection properties)*
|
|
673
994
|
|
|
674
995
|
#### `OIMReactiveIndexManualSetBased<TKey, TPk>`
|
|
675
|
-
Reactive Set-based index with manual key-to-entity mapping and change notifications.
|
|
996
|
+
Reactive Set-based index with manual key-to-entity mapping and change notifications. Stores canonical slots internally and projects to `Set<TPk>` for PK reads.
|
|
676
997
|
|
|
677
998
|
**Constructor:**
|
|
678
999
|
```typescript
|
|
679
1000
|
new OIMReactiveIndexManualSetBased(queue: OIMEventQueue, opts?: {
|
|
680
|
-
|
|
1001
|
+
indexOptions?: {
|
|
1002
|
+
comparePks?: TOIMIndexComparator<TPk>;
|
|
1003
|
+
store?: OIMIndexStoreSetBased<TKey, TPk>;
|
|
1004
|
+
resolveSlot?: TOIMEntitySlotResolver<TPk>;
|
|
1005
|
+
}
|
|
681
1006
|
})
|
|
682
1007
|
```
|
|
683
1008
|
|
|
@@ -689,18 +1014,24 @@ new OIMReactiveIndexManualSetBased(queue: OIMEventQueue, opts?: {
|
|
|
689
1014
|
- `setPks(key: TKey, pks: readonly TPk[]): void` - Set primary keys for index key (replaces entire Set)
|
|
690
1015
|
- `addPks(key: TKey, pks: readonly TPk[]): void` - Add primary keys to index key (efficient)
|
|
691
1016
|
- `removePks(key: TKey, pks: readonly TPk[]): void` - Remove primary keys from index key (efficient)
|
|
1017
|
+
- `setSlots(key: TKey, slots: Iterable<TOIMAnyEntitySlot<TPk>>): void` - Set canonical slots directly
|
|
692
1018
|
- `clear(key?: TKey): void` - Clear all keys or specific key
|
|
693
1019
|
|
|
694
1020
|
**Query:**
|
|
695
|
-
- `index.getPksByKey(key: TKey): Set<TPk>` - Returns Set of primary keys
|
|
1021
|
+
- `index.getPksByKey(key: TKey): Set<TPk>` - Returns Set projection of primary keys
|
|
1022
|
+
- `index.getSlotsByKey(key: TKey): ReadonlySet<TOIMAnyEntitySlot<TPk>>` - Returns stored slots for fast entity reads
|
|
696
1023
|
|
|
697
1024
|
#### `OIMReactiveIndexManualArrayBased<TKey, TPk>`
|
|
698
|
-
Reactive Array-based index with manual key-to-entity mapping and change notifications.
|
|
1025
|
+
Reactive Array-based index with manual key-to-entity mapping and change notifications. Stores canonical slots internally and projects to `TPk[]` for PK reads.
|
|
699
1026
|
|
|
700
1027
|
**Constructor:**
|
|
701
1028
|
```typescript
|
|
702
1029
|
new OIMReactiveIndexManualArrayBased(queue: OIMEventQueue, opts?: {
|
|
703
|
-
|
|
1030
|
+
indexOptions?: {
|
|
1031
|
+
comparePks?: TOIMIndexComparator<TPk>;
|
|
1032
|
+
store?: OIMIndexStoreArrayBased<TKey, TPk>;
|
|
1033
|
+
resolveSlot?: TOIMEntitySlotResolver<TPk>;
|
|
1034
|
+
}
|
|
704
1035
|
})
|
|
705
1036
|
```
|
|
706
1037
|
|
|
@@ -712,10 +1043,12 @@ new OIMReactiveIndexManualArrayBased(queue: OIMEventQueue, opts?: {
|
|
|
712
1043
|
- `setPks(key: TKey, pks: readonly TPk[]): void` - Set primary keys for index key (direct assignment, no diff) - **Recommended for ArrayBased**
|
|
713
1044
|
- `addPks(key: TKey, pks: readonly TPk[]): void` - Add primary keys to index key (O(n) operation, less efficient than SetBased)
|
|
714
1045
|
- `removePks(key: TKey, pks: readonly TPk[]): void` - Remove primary keys from index key (O(n) operation, less efficient than SetBased)
|
|
1046
|
+
- `setSlots(key: TKey, slots: TOIMAnyEntitySlot<TPk>[]): void` - Set canonical slots directly
|
|
715
1047
|
- `clear(key?: TKey): void` - Clear all keys or specific key
|
|
716
1048
|
|
|
717
1049
|
**Query:**
|
|
718
|
-
- `index.getPksByKey(key: TKey): TPk[]` - Returns Array of primary keys
|
|
1050
|
+
- `index.getPksByKey(key: TKey): TPk[]` - Returns Array projection of primary keys
|
|
1051
|
+
- `index.getSlotsByKey(key: TKey): readonly TOIMAnyEntitySlot<TPk>[]` - Returns stored slots for fast entity reads
|
|
719
1052
|
|
|
720
1053
|
**Note**: While `addPks`/`removePks` are available, they require array operations (Set creation, filtering) making them O(n) compared to O(1) for SetBased indexes. For ArrayBased indexes, prefer `setPks` for better performance when replacing the entire array.
|
|
721
1054
|
|