@oimdb/core 1.3.0 → 1.4.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 +148 -0
- package/dist/index.cjs +1018 -49
- package/dist/index.d.cts +430 -9
- package/dist/index.d.ts +430 -9
- package/dist/index.js +997 -49
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -360,6 +360,154 @@ users.upsertOne({ id: 'user1', role: 'admin' });
|
|
|
360
360
|
// This prevents unnecessary re-renders and improves performance
|
|
361
361
|
```
|
|
362
362
|
|
|
363
|
+
### Effects, Computed, and the Event Lifecycle (PRE vs HANDLERS)
|
|
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:**
|
|
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”.
|
|
379
|
+
|
|
380
|
+
#### What is an Effect?
|
|
381
|
+
|
|
382
|
+
`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
|
+
|
|
384
|
+
```typescript
|
|
385
|
+
import {
|
|
386
|
+
OIMEffect,
|
|
387
|
+
EOIMEffectPhase,
|
|
388
|
+
OIMEventQueue,
|
|
389
|
+
OIMReactiveObject,
|
|
390
|
+
OIMEffectDependencyKeyedObject,
|
|
391
|
+
} from '@oimdb/core';
|
|
392
|
+
|
|
393
|
+
type TKey = 'a';
|
|
394
|
+
|
|
395
|
+
const queue = new OIMEventQueue();
|
|
396
|
+
const obj = new OIMReactiveObject<TKey, number>(queue);
|
|
397
|
+
|
|
398
|
+
const effect = new OIMEffect(queue, {
|
|
399
|
+
phase: EOIMEffectPhase.HANDLERS,
|
|
400
|
+
deps: [new OIMEffectDependencyKeyedObject(obj, 'a')],
|
|
401
|
+
run: () => {
|
|
402
|
+
console.log('obj.a changed');
|
|
403
|
+
},
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
obj.setProperty('a', 1);
|
|
407
|
+
queue.flush();
|
|
408
|
+
|
|
409
|
+
effect.destroy();
|
|
410
|
+
obj.destroy();
|
|
411
|
+
queue.destroy();
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
#### What is a Computed?
|
|
415
|
+
|
|
416
|
+
`OIMComputed<T>` is built on top of `OIMEffect` (PRE phase): it recomputes a derived value and emits `update` when the value changes.
|
|
417
|
+
|
|
418
|
+
```typescript
|
|
419
|
+
import {
|
|
420
|
+
OIMComputed,
|
|
421
|
+
OIMEventQueue,
|
|
422
|
+
OIMReactiveObject,
|
|
423
|
+
OIMEffectDependencyKeyedObject,
|
|
424
|
+
} from '@oimdb/core';
|
|
425
|
+
|
|
426
|
+
type TKey = 'a';
|
|
427
|
+
const queue = new OIMEventQueue();
|
|
428
|
+
const obj = new OIMReactiveObject<TKey, number>(queue);
|
|
429
|
+
|
|
430
|
+
const doubled = new OIMComputed<number>(queue, {
|
|
431
|
+
compute: () => (obj.get('a') ?? 0) * 2,
|
|
432
|
+
deps: [new OIMEffectDependencyKeyedObject(obj, 'a')],
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
obj.setProperty('a', 10);
|
|
436
|
+
queue.flush(); // recompute happens during PRE
|
|
437
|
+
|
|
438
|
+
console.log(doubled.get()); // 20
|
|
439
|
+
|
|
440
|
+
// If you also subscribe to computed updates, delivery can be in the next flush:
|
|
441
|
+
let calls = 0;
|
|
442
|
+
doubled.updateEventEmitter.subscribeOnKey('value', () => {
|
|
443
|
+
calls++;
|
|
444
|
+
});
|
|
445
|
+
obj.setProperty('a', 11);
|
|
446
|
+
queue.flush(); // recompute (PRE)
|
|
447
|
+
queue.flush(); // subscriber delivery (HANDLERS via queue snapshot)
|
|
448
|
+
console.log(calls); // 1
|
|
449
|
+
|
|
450
|
+
doubled.destroy();
|
|
451
|
+
obj.destroy();
|
|
452
|
+
queue.destroy();
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
#### Computed-to-Computed dependencies
|
|
456
|
+
|
|
457
|
+
For computed-to-computed dependencies you can use `OIMEffectDependencyComputed`.
|
|
458
|
+
|
|
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
|
+
```typescript
|
|
463
|
+
import {
|
|
464
|
+
OIMComputed,
|
|
465
|
+
OIMEffect,
|
|
466
|
+
EOIMEffectPhase,
|
|
467
|
+
OIMEffectDependencyComputed,
|
|
468
|
+
OIMEventQueue,
|
|
469
|
+
OIMReactiveObject,
|
|
470
|
+
OIMEffectDependencyKeyedObject,
|
|
471
|
+
} from '@oimdb/core';
|
|
472
|
+
|
|
473
|
+
type TKey = 'a';
|
|
474
|
+
const queue = new OIMEventQueue();
|
|
475
|
+
const obj = new OIMReactiveObject<TKey, number>(queue);
|
|
476
|
+
|
|
477
|
+
const A = new OIMComputed<number>(queue, {
|
|
478
|
+
compute: () => (obj.get('a') ?? 0) + 1,
|
|
479
|
+
deps: [new OIMEffectDependencyKeyedObject(obj, 'a')],
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
const B = new OIMComputed<number>(queue, {
|
|
483
|
+
compute: () => A.get() * 2,
|
|
484
|
+
deps: [new OIMEffectDependencyComputed({ emitter: A.emitter, updateEventEmitter: A.updateEventEmitter })],
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
const effect = new OIMEffect(queue, {
|
|
488
|
+
phase: EOIMEffectPhase.HANDLERS,
|
|
489
|
+
deps: [new OIMEffectDependencyComputed({ emitter: B.emitter, updateEventEmitter: B.updateEventEmitter })],
|
|
490
|
+
run: () => console.log('B changed'),
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
obj.setProperty('a', 1);
|
|
494
|
+
queue.flush(); // computed recompute (PRE)
|
|
495
|
+
queue.flush(); // handlers delivered via the queue snapshot
|
|
496
|
+
|
|
497
|
+
effect.destroy();
|
|
498
|
+
B.destroy();
|
|
499
|
+
A.destroy();
|
|
500
|
+
obj.destroy();
|
|
501
|
+
queue.destroy();
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
#### Gotchas (read this once)
|
|
505
|
+
|
|
506
|
+
- **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
|
+
- **Keep `compute()` pure**: treat `compute()` as a pure function over current state. Doing writes inside `compute()` will create hard-to-debug re-entrancy.
|
|
508
|
+
- **Put side-effects in HANDLERS**: if you need to write to stores or trigger IO, do it from `OIMEffect` with `phase: EOIMEffectPhase.HANDLERS`.
|
|
509
|
+
- **Always `destroy()`**: effects/computed subscribe to dependencies; if you create them dynamically, call `destroy()` to unsubscribe and free memory.
|
|
510
|
+
|
|
363
511
|
### Scheduler Types
|
|
364
512
|
|
|
365
513
|
Choose the right scheduler for your use case:
|