@pumped-fn/lite 2.1.0 → 2.1.3
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/CHANGELOG.md +47 -0
- package/PATTERNS.md +1 -1
- package/dist/index.cjs +223 -109
- package/dist/index.d.cts +28 -7
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +28 -7
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +223 -110
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,52 @@
|
|
|
1
1
|
# @pumped-fn/lite
|
|
2
2
|
|
|
3
|
+
## 2.1.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- b84f763: Fix `watch: true` default equality so structurally equal plain-object results do not trigger false cascades, while non-plain values like `Map` and symbol-keyed state still invalidate correctly.
|
|
8
|
+
|
|
9
|
+
## 2.1.2
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 8ed17e7: - Fix watch and invalidation edge cases in `@pumped-fn/lite` by aligning `select()` with `Object.is`, snapshotting select listeners during notification, making watch option typing match the runtime contract, and surfacing invalidation-chain failures from `flush()` instead of leaking them as background rejections.
|
|
14
|
+
- Fix `@pumped-fn/lite-react` hook refresh behavior by keeping stale values visible during re-resolution, recomputing `useSelect` snapshots when selector or equality semantics change, tracking pending promises per controller, and suppressing non-Suspense `unhandledRejection` leaks on failed refreshes.
|
|
15
|
+
|
|
16
|
+
## 2.1.1
|
|
17
|
+
|
|
18
|
+
### Patch Changes
|
|
19
|
+
|
|
20
|
+
- 2ce41fc: Fix 16 bugs found via adversarial triage + 5 rounds of Codex review:
|
|
21
|
+
|
|
22
|
+
**Correctness**
|
|
23
|
+
|
|
24
|
+
- `preset(atom, undefined)` now works — uses `has()` check instead of `!== undefined`
|
|
25
|
+
- `seekHas()` traverses parent chain via interface dispatch, not `instanceof`
|
|
26
|
+
- Error-path `pendingSet` only reschedules value-type sets — `fn(undefined)` no longer produces garbage
|
|
27
|
+
- `doInvalidateSequential` swallows resolve errors when pending operations exist
|
|
28
|
+
- Resource cycle detection moved to per-execution-chain WeakMap — fixes false errors with `ctx.exec()`
|
|
29
|
+
- Resource inflight check runs before circular check — sibling `ctx.exec()` no longer false-positives
|
|
30
|
+
|
|
31
|
+
**Reactive system**
|
|
32
|
+
|
|
33
|
+
- `set()`/`update()` pendingSet path skips cleanups — watch deps preserved since factory doesn't re-run
|
|
34
|
+
- Unconditional `invalidationChain.delete()` in pendingSet fast-path — prevents self-loops
|
|
35
|
+
- Copy-on-iterate on all 4 listener iteration sites — unsub during notification no longer drops siblings
|
|
36
|
+
|
|
37
|
+
**Lifecycle**
|
|
38
|
+
|
|
39
|
+
- `dispose()` awaits `chainPromise` before setting `disposed` — drains pending invalidation chain
|
|
40
|
+
- `resolve()`, `controller()`, `createContext()` throw after dispose
|
|
41
|
+
- `release()` cleans up dependents + schedules GC on freed deps
|
|
42
|
+
|
|
43
|
+
**SelectHandle**
|
|
44
|
+
|
|
45
|
+
- Eager subscription in constructor — tracks changes without active subscribers
|
|
46
|
+
- `dispose()` method for explicit teardown
|
|
47
|
+
- Re-subscribe refreshes cached value after auto-cleanup
|
|
48
|
+
- Added `seekHas()` to `ContextData` interface, `dispose()` to `SelectHandle` interface
|
|
49
|
+
|
|
3
50
|
## 2.1.0
|
|
4
51
|
|
|
5
52
|
### Minor Changes
|
package/PATTERNS.md
CHANGED
|
@@ -232,7 +232,7 @@ sequenceDiagram
|
|
|
232
232
|
Parent->>Scope: resolve configAtom first
|
|
233
233
|
Scope-->>Ctrl: ctrl (resolved)
|
|
234
234
|
Parent->>Parent: factory(_, { cfg: ctrl })
|
|
235
|
-
Note over Scope: on dep 'resolved': compare prev/next via eq ?? Object.is
|
|
235
|
+
Note over Scope: on dep 'resolved': compare prev/next via eq ?? shallowEqual (plain objects, Object.is otherwise)
|
|
236
236
|
Scope->>Parent: scheduleInvalidation if changed
|
|
237
237
|
Note over Parent: watch listener auto-cleaned on re-resolve / release / dispose
|
|
238
238
|
```
|
package/dist/index.cjs
CHANGED
|
@@ -88,7 +88,7 @@ function getAtomsForTag(tag$1) {
|
|
|
88
88
|
return live;
|
|
89
89
|
}
|
|
90
90
|
function tag(options) {
|
|
91
|
-
const key = Symbol
|
|
91
|
+
const key = Symbol(`@pumped-fn/lite/tag/${options.label}`);
|
|
92
92
|
const hasDefault = "default" in options;
|
|
93
93
|
const defaultValue = hasDefault ? options.default : void 0;
|
|
94
94
|
const parse = options.parse;
|
|
@@ -266,7 +266,8 @@ function isAtom(value) {
|
|
|
266
266
|
* @param options - Optional configuration:
|
|
267
267
|
* - `resolve: true` — auto-resolves the dep before the parent factory runs; `config.get()` is safe.
|
|
268
268
|
* - `watch: true` — atom deps only; requires `resolve: true`; automatically re-runs the parent factory
|
|
269
|
-
* when the dep resolves to a new value (value-equality gated via `
|
|
269
|
+
* when the dep resolves to a new value (value-equality gated via plain-object `shallowEqual` by default,
|
|
270
|
+
* otherwise `Object.is`). Replaces
|
|
270
271
|
* manual `ctx.cleanup(ctx.scope.on('resolved', dep, () => ctx.invalidate()))` wiring. Watch
|
|
271
272
|
* listeners are auto-cleaned on re-resolve, release, and dispose.
|
|
272
273
|
* - `eq` — custom equality function `(a: T, b: T) => boolean`; only used with `watch: true`.
|
|
@@ -425,12 +426,38 @@ function isResource(value) {
|
|
|
425
426
|
//#endregion
|
|
426
427
|
//#region src/service.ts
|
|
427
428
|
function service(config) {
|
|
428
|
-
|
|
429
|
+
const atomInstance = {
|
|
429
430
|
[atomSymbol]: true,
|
|
430
431
|
factory: config.factory,
|
|
431
432
|
deps: config.deps,
|
|
432
433
|
tags: config.tags
|
|
433
434
|
};
|
|
435
|
+
if (config.tags?.length) registerAtomToTags(atomInstance, config.tags);
|
|
436
|
+
return atomInstance;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
//#endregion
|
|
440
|
+
//#region src/equality.ts
|
|
441
|
+
function isPlainObject(value) {
|
|
442
|
+
const prototype = Object.getPrototypeOf(value);
|
|
443
|
+
return prototype === Object.prototype || prototype === null;
|
|
444
|
+
}
|
|
445
|
+
function enumerableOwnKeys(value) {
|
|
446
|
+
return Reflect.ownKeys(value).filter((key) => Object.prototype.propertyIsEnumerable.call(value, key));
|
|
447
|
+
}
|
|
448
|
+
function shallowEqual(a, b) {
|
|
449
|
+
if (Object.is(a, b)) return true;
|
|
450
|
+
if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return false;
|
|
451
|
+
if (!isPlainObject(a) || !isPlainObject(b)) return false;
|
|
452
|
+
const objA = a;
|
|
453
|
+
const objB = b;
|
|
454
|
+
const keysA = enumerableOwnKeys(objA);
|
|
455
|
+
if (keysA.length !== enumerableOwnKeys(objB).length) return false;
|
|
456
|
+
for (const key of keysA) {
|
|
457
|
+
if (!Object.hasOwn(objB, key)) return false;
|
|
458
|
+
if (!Object.is(objA[key], objB[key])) return false;
|
|
459
|
+
}
|
|
460
|
+
return true;
|
|
434
461
|
}
|
|
435
462
|
|
|
436
463
|
//#endregion
|
|
@@ -446,7 +473,7 @@ function getResourceKey(resource$1) {
|
|
|
446
473
|
return key;
|
|
447
474
|
}
|
|
448
475
|
const inflightResources = /* @__PURE__ */ new WeakMap();
|
|
449
|
-
const
|
|
476
|
+
const resolvingResourcesMap = /* @__PURE__ */ new WeakMap();
|
|
450
477
|
var ContextDataImpl = class {
|
|
451
478
|
map = /* @__PURE__ */ new Map();
|
|
452
479
|
constructor(parentData) {
|
|
@@ -471,6 +498,10 @@ var ContextDataImpl = class {
|
|
|
471
498
|
if (this.map.has(key)) return this.map.get(key);
|
|
472
499
|
return this.parentData?.seek(key);
|
|
473
500
|
}
|
|
501
|
+
seekHas(key) {
|
|
502
|
+
if (this.map.has(key)) return true;
|
|
503
|
+
return this.parentData?.seekHas(key) ?? false;
|
|
504
|
+
}
|
|
474
505
|
getTag(tag$1) {
|
|
475
506
|
return this.map.get(tag$1.key);
|
|
476
507
|
}
|
|
@@ -516,6 +547,16 @@ var SelectHandleImpl = class {
|
|
|
516
547
|
return this.currentValue;
|
|
517
548
|
}
|
|
518
549
|
subscribe(listener) {
|
|
550
|
+
if (!this.ctrlUnsub) {
|
|
551
|
+
this.currentValue = this.selector(this.ctrl.get());
|
|
552
|
+
this.ctrlUnsub = this.ctrl.on("resolved", () => {
|
|
553
|
+
const nextValue = this.selector(this.ctrl.get());
|
|
554
|
+
if (!this.eq(this.currentValue, nextValue)) {
|
|
555
|
+
this.currentValue = nextValue;
|
|
556
|
+
this.notifyListeners();
|
|
557
|
+
}
|
|
558
|
+
});
|
|
559
|
+
}
|
|
519
560
|
this.listeners.add(listener);
|
|
520
561
|
return () => {
|
|
521
562
|
this.listeners.delete(listener);
|
|
@@ -523,12 +564,15 @@ var SelectHandleImpl = class {
|
|
|
523
564
|
};
|
|
524
565
|
}
|
|
525
566
|
notifyListeners() {
|
|
526
|
-
for (const listener of this.listeners) listener();
|
|
567
|
+
for (const listener of [...this.listeners]) listener();
|
|
568
|
+
}
|
|
569
|
+
dispose() {
|
|
570
|
+
this.listeners.clear();
|
|
571
|
+
this.cleanup();
|
|
527
572
|
}
|
|
528
573
|
cleanup() {
|
|
529
574
|
this.ctrlUnsub?.();
|
|
530
575
|
this.ctrlUnsub = null;
|
|
531
|
-
this.listeners.clear();
|
|
532
576
|
}
|
|
533
577
|
};
|
|
534
578
|
var ControllerImpl = class {
|
|
@@ -577,11 +621,15 @@ var ScopeImpl = class {
|
|
|
577
621
|
invalidationScheduled = false;
|
|
578
622
|
invalidationChain = null;
|
|
579
623
|
chainPromise = null;
|
|
624
|
+
chainError = null;
|
|
580
625
|
initialized = false;
|
|
626
|
+
disposed = false;
|
|
581
627
|
controllers = /* @__PURE__ */ new Map();
|
|
582
628
|
gcOptions;
|
|
583
629
|
extensions;
|
|
584
630
|
tags;
|
|
631
|
+
resolveExts;
|
|
632
|
+
execExts;
|
|
585
633
|
ready;
|
|
586
634
|
scheduleInvalidation(atom$1) {
|
|
587
635
|
const entry = this.cache.get(atom$1);
|
|
@@ -594,16 +642,22 @@ var ScopeImpl = class {
|
|
|
594
642
|
if (!this.chainPromise) {
|
|
595
643
|
this.invalidationChain = /* @__PURE__ */ new Set();
|
|
596
644
|
this.invalidationScheduled = true;
|
|
597
|
-
this.
|
|
598
|
-
|
|
599
|
-
|
|
645
|
+
this.chainError = null;
|
|
646
|
+
this.chainPromise = (async () => {
|
|
647
|
+
await new Promise((resolve) => {
|
|
648
|
+
queueMicrotask(resolve);
|
|
600
649
|
});
|
|
601
|
-
|
|
650
|
+
try {
|
|
651
|
+
await this.processInvalidationChain();
|
|
652
|
+
} catch (error) {
|
|
653
|
+
if (this.chainError === null) this.chainError = error;
|
|
654
|
+
}
|
|
655
|
+
})();
|
|
602
656
|
}
|
|
603
657
|
}
|
|
604
658
|
async processInvalidationChain() {
|
|
605
659
|
try {
|
|
606
|
-
while (this.invalidationQueue.size > 0) {
|
|
660
|
+
while (this.invalidationQueue.size > 0 && !this.disposed) {
|
|
607
661
|
const atom$1 = this.invalidationQueue.values().next().value;
|
|
608
662
|
this.invalidationQueue.delete(atom$1);
|
|
609
663
|
if (this.invalidationChain.has(atom$1)) {
|
|
@@ -624,6 +678,8 @@ var ScopeImpl = class {
|
|
|
624
678
|
constructor(options) {
|
|
625
679
|
this.extensions = options?.extensions ?? [];
|
|
626
680
|
this.tags = options?.tags ?? [];
|
|
681
|
+
this.resolveExts = this.extensions.filter((e) => e.wrapResolve);
|
|
682
|
+
this.execExts = this.extensions.filter((e) => e.wrapExec);
|
|
627
683
|
for (const p of options?.presets ?? []) this.presets.set(p.target, p.value);
|
|
628
684
|
this.gcOptions = {
|
|
629
685
|
enabled: options?.gc?.enabled ?? true,
|
|
@@ -716,21 +772,21 @@ var ScopeImpl = class {
|
|
|
716
772
|
const entry = this.cache.get(atom$1);
|
|
717
773
|
if (!entry) return;
|
|
718
774
|
const eventListeners = entry.listeners.get(event);
|
|
719
|
-
if (eventListeners) for (const listener of eventListeners) listener();
|
|
775
|
+
if (eventListeners?.size) for (const listener of [...eventListeners]) listener();
|
|
720
776
|
const allListeners = entry.listeners.get("*");
|
|
721
|
-
if (allListeners) for (const listener of allListeners) listener();
|
|
777
|
+
if (allListeners?.size) for (const listener of [...allListeners]) listener();
|
|
722
778
|
}
|
|
723
779
|
notifyAllListeners(atom$1) {
|
|
724
780
|
const entry = this.cache.get(atom$1);
|
|
725
781
|
if (!entry) return;
|
|
726
782
|
const allListeners = entry.listeners.get("*");
|
|
727
|
-
if (allListeners) for (const listener of allListeners) listener();
|
|
783
|
+
if (allListeners?.size) for (const listener of [...allListeners]) listener();
|
|
728
784
|
}
|
|
729
785
|
emitStateChange(state, atom$1) {
|
|
730
786
|
const stateMap = this.stateListeners.get(state);
|
|
731
787
|
if (stateMap) {
|
|
732
788
|
const listeners = stateMap.get(atom$1);
|
|
733
|
-
if (listeners) for (const listener of listeners) listener();
|
|
789
|
+
if (listeners?.size) for (const listener of [...listeners]) listener();
|
|
734
790
|
}
|
|
735
791
|
}
|
|
736
792
|
on(event, atom$1, listener) {
|
|
@@ -756,14 +812,15 @@ var ScopeImpl = class {
|
|
|
756
812
|
};
|
|
757
813
|
}
|
|
758
814
|
async resolve(atom$1) {
|
|
815
|
+
if (this.disposed) throw new Error("Scope is disposed");
|
|
759
816
|
if (!this.initialized) await this.ready;
|
|
760
817
|
const entry = this.cache.get(atom$1);
|
|
761
818
|
if (entry?.state === "resolved") return entry.value;
|
|
762
819
|
const pendingPromise = this.pending.get(atom$1);
|
|
763
820
|
if (pendingPromise) return pendingPromise;
|
|
764
821
|
if (this.resolving.has(atom$1)) throw new Error("Circular dependency detected");
|
|
765
|
-
|
|
766
|
-
|
|
822
|
+
if (this.presets.has(atom$1)) {
|
|
823
|
+
const presetValue = this.presets.get(atom$1);
|
|
767
824
|
if (isAtom(presetValue)) return this.resolve(presetValue);
|
|
768
825
|
const newEntry = this.getOrCreateEntry(atom$1);
|
|
769
826
|
newEntry.state = "resolved";
|
|
@@ -786,7 +843,9 @@ var ScopeImpl = class {
|
|
|
786
843
|
async doResolve(atom$1) {
|
|
787
844
|
const entry = this.getOrCreateEntry(atom$1);
|
|
788
845
|
if (!(entry.state === "resolving")) {
|
|
789
|
-
for (let i = entry.cleanups.length - 1; i >= 0; i--)
|
|
846
|
+
for (let i = entry.cleanups.length - 1; i >= 0; i--) try {
|
|
847
|
+
await entry.cleanups[i]?.();
|
|
848
|
+
} catch {}
|
|
790
849
|
entry.cleanups = [];
|
|
791
850
|
entry.state = "resolving";
|
|
792
851
|
this.emitStateChange("resolving", atom$1);
|
|
@@ -806,7 +865,7 @@ var ScopeImpl = class {
|
|
|
806
865
|
};
|
|
807
866
|
const factory = atom$1.factory;
|
|
808
867
|
const doResolve = async () => {
|
|
809
|
-
if (atom$1.deps
|
|
868
|
+
if (atom$1.deps) return factory(ctx, resolvedDeps);
|
|
810
869
|
else return factory(ctx);
|
|
811
870
|
};
|
|
812
871
|
try {
|
|
@@ -842,88 +901,101 @@ var ScopeImpl = class {
|
|
|
842
901
|
entry.pendingInvalidate = false;
|
|
843
902
|
this.invalidationChain?.delete(atom$1);
|
|
844
903
|
this.scheduleInvalidation(atom$1);
|
|
845
|
-
}
|
|
904
|
+
} else if (entry.pendingSet && "value" in entry.pendingSet) {
|
|
905
|
+
this.invalidationChain?.delete(atom$1);
|
|
906
|
+
this.scheduleInvalidation(atom$1);
|
|
907
|
+
} else entry.pendingSet = void 0;
|
|
846
908
|
throw entry.error;
|
|
847
909
|
}
|
|
848
910
|
}
|
|
849
911
|
async applyResolveExtensions(event, doResolve) {
|
|
850
912
|
let next = doResolve;
|
|
851
|
-
for (let i = this.
|
|
852
|
-
const ext = this.
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
next = ext.wrapResolve.bind(ext, currentNext, event);
|
|
856
|
-
}
|
|
913
|
+
for (let i = this.resolveExts.length - 1; i >= 0; i--) {
|
|
914
|
+
const ext = this.resolveExts[i];
|
|
915
|
+
const currentNext = next;
|
|
916
|
+
next = ext.wrapResolve.bind(ext, currentNext, event);
|
|
857
917
|
}
|
|
858
918
|
return next();
|
|
859
919
|
}
|
|
860
920
|
async resolveDeps(deps, ctx, dependentAtom) {
|
|
861
921
|
if (!deps) return {};
|
|
862
922
|
const result = {};
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
}
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
if (
|
|
887
|
-
|
|
888
|
-
|
|
923
|
+
const parallel = [];
|
|
924
|
+
const deferredResources = [];
|
|
925
|
+
for (const key in deps) {
|
|
926
|
+
const dep = deps[key];
|
|
927
|
+
if (isAtom(dep)) parallel.push(this.resolve(dep).then((value) => {
|
|
928
|
+
result[key] = value;
|
|
929
|
+
if (dependentAtom) {
|
|
930
|
+
const depEntry = this.getEntry(dep);
|
|
931
|
+
if (depEntry) depEntry.dependents.add(dependentAtom);
|
|
932
|
+
}
|
|
933
|
+
}));
|
|
934
|
+
else if (isControllerDep(dep)) {
|
|
935
|
+
if (dep.watch) {
|
|
936
|
+
if (!dependentAtom) throw new Error("controller({ watch: true }) is only supported in atom dependencies");
|
|
937
|
+
if (!dep.resolve) throw new Error("controller({ watch: true }) requires resolve: true");
|
|
938
|
+
}
|
|
939
|
+
const ctrl = this.controller(dep.atom);
|
|
940
|
+
if (dep.resolve) parallel.push(ctrl.resolve().then(() => {
|
|
941
|
+
result[key] = ctrl;
|
|
942
|
+
if (dependentAtom) {
|
|
943
|
+
const depEntry = this.getEntry(dep.atom);
|
|
944
|
+
if (depEntry) depEntry.dependents.add(dependentAtom);
|
|
945
|
+
}
|
|
946
|
+
if (dep.watch) {
|
|
947
|
+
const eq = dep.eq ?? shallowEqual;
|
|
948
|
+
let prev = ctrl.get();
|
|
949
|
+
const unsub = this.on("resolved", dep.atom, () => {
|
|
950
|
+
const next = ctrl.get();
|
|
951
|
+
if (!eq(prev, next)) this.scheduleInvalidation(dependentAtom);
|
|
952
|
+
prev = next;
|
|
953
|
+
});
|
|
954
|
+
const depEntry = this.getEntry(dependentAtom);
|
|
955
|
+
if (depEntry) depEntry.cleanups.push(unsub);
|
|
956
|
+
else unsub();
|
|
957
|
+
}
|
|
958
|
+
}));
|
|
959
|
+
else {
|
|
960
|
+
result[key] = ctrl;
|
|
961
|
+
if (dependentAtom) {
|
|
962
|
+
const depEntry = this.getEntry(dep.atom);
|
|
963
|
+
if (depEntry) depEntry.dependents.add(dependentAtom);
|
|
889
964
|
}
|
|
890
|
-
});
|
|
891
|
-
const depEntry = this.getEntry(dependentAtom);
|
|
892
|
-
if (depEntry) depEntry.cleanups.push(unsub);
|
|
893
|
-
else unsub();
|
|
894
|
-
}
|
|
895
|
-
} else if (tagExecutorSymbol in dep) {
|
|
896
|
-
const tagExecutor = dep;
|
|
897
|
-
switch (tagExecutor.mode) {
|
|
898
|
-
case "required": {
|
|
899
|
-
const value = ctx ? ctx.data.seekTag(tagExecutor.tag) : tagExecutor.tag.find(this.tags);
|
|
900
|
-
if (value !== void 0) result[key] = value;
|
|
901
|
-
else if (tagExecutor.tag.hasDefault) result[key] = tagExecutor.tag.defaultValue;
|
|
902
|
-
else throw new Error(`Tag "${tagExecutor.tag.label}" not found`);
|
|
903
|
-
break;
|
|
904
965
|
}
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
966
|
+
} else if (tagExecutorSymbol in dep) {
|
|
967
|
+
const tagExecutor = dep;
|
|
968
|
+
switch (tagExecutor.mode) {
|
|
969
|
+
case "required": {
|
|
970
|
+
const value = ctx ? ctx.data.seekTag(tagExecutor.tag) : tagExecutor.tag.find(this.tags);
|
|
971
|
+
if (value !== void 0) result[key] = value;
|
|
972
|
+
else if (tagExecutor.tag.hasDefault) result[key] = tagExecutor.tag.defaultValue;
|
|
973
|
+
else throw new Error(`Tag "${tagExecutor.tag.label}" not found`);
|
|
974
|
+
break;
|
|
975
|
+
}
|
|
976
|
+
case "optional":
|
|
977
|
+
result[key] = (ctx ? ctx.data.seekTag(tagExecutor.tag) : tagExecutor.tag.find(this.tags)) ?? tagExecutor.tag.defaultValue;
|
|
978
|
+
break;
|
|
979
|
+
case "all":
|
|
980
|
+
result[key] = ctx ? this.collectFromHierarchy(ctx, tagExecutor.tag) : tagExecutor.tag.collect(this.tags);
|
|
981
|
+
break;
|
|
982
|
+
}
|
|
983
|
+
} else if (isResource(dep)) deferredResources.push([key, dep]);
|
|
984
|
+
}
|
|
985
|
+
if (parallel.length === 1) await parallel[0];
|
|
986
|
+
else if (parallel.length > 1) await Promise.all(parallel);
|
|
987
|
+
for (const [key, resource$1] of deferredResources) {
|
|
913
988
|
if (!ctx) throw new Error("Resource deps require an ExecutionContext");
|
|
914
|
-
const resource$1 = dep;
|
|
915
989
|
const resourceKey = getResourceKey(resource$1);
|
|
916
990
|
const storeCtx = ctx.parent ?? ctx;
|
|
917
991
|
if (storeCtx.data.has(resourceKey)) {
|
|
918
992
|
result[key] = storeCtx.data.get(resourceKey);
|
|
919
993
|
continue;
|
|
920
994
|
}
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
result[key] = existingSeek;
|
|
995
|
+
if (ctx.data.seekHas(resourceKey)) {
|
|
996
|
+
result[key] = ctx.data.seek(resourceKey);
|
|
924
997
|
continue;
|
|
925
998
|
}
|
|
926
|
-
if (resolvingResources.has(resourceKey)) throw new Error(`Circular resource dependency detected: ${resource$1.name ?? "anonymous"}`);
|
|
927
999
|
let flights = inflightResources.get(storeCtx.data);
|
|
928
1000
|
if (!flights) {
|
|
929
1001
|
flights = /* @__PURE__ */ new Map();
|
|
@@ -934,8 +1006,14 @@ var ScopeImpl = class {
|
|
|
934
1006
|
result[key] = await inflight;
|
|
935
1007
|
continue;
|
|
936
1008
|
}
|
|
1009
|
+
let localResolvingResources = resolvingResourcesMap.get(storeCtx.data);
|
|
1010
|
+
if (!localResolvingResources) {
|
|
1011
|
+
localResolvingResources = /* @__PURE__ */ new Set();
|
|
1012
|
+
resolvingResourcesMap.set(storeCtx.data, localResolvingResources);
|
|
1013
|
+
}
|
|
1014
|
+
if (localResolvingResources.has(resourceKey)) throw new Error(`Circular resource dependency detected: ${resource$1.name ?? "anonymous"}`);
|
|
937
1015
|
const resolve = async () => {
|
|
938
|
-
|
|
1016
|
+
localResolvingResources.add(resourceKey);
|
|
939
1017
|
try {
|
|
940
1018
|
const resourceDeps = await this.resolveDeps(resource$1.deps, ctx);
|
|
941
1019
|
const event = {
|
|
@@ -945,14 +1023,14 @@ var ScopeImpl = class {
|
|
|
945
1023
|
};
|
|
946
1024
|
const doResolve = async () => {
|
|
947
1025
|
const factory = resource$1.factory;
|
|
948
|
-
if (resource$1.deps
|
|
1026
|
+
if (resource$1.deps) return factory(storeCtx, resourceDeps);
|
|
949
1027
|
return factory(storeCtx);
|
|
950
1028
|
};
|
|
951
1029
|
const value = await this.applyResolveExtensions(event, doResolve);
|
|
952
1030
|
storeCtx.data.set(resourceKey, value);
|
|
953
1031
|
return value;
|
|
954
1032
|
} finally {
|
|
955
|
-
|
|
1033
|
+
localResolvingResources.delete(resourceKey);
|
|
956
1034
|
}
|
|
957
1035
|
};
|
|
958
1036
|
const promise = resolve();
|
|
@@ -976,6 +1054,7 @@ var ScopeImpl = class {
|
|
|
976
1054
|
return results;
|
|
977
1055
|
}
|
|
978
1056
|
controller(atom$1, options) {
|
|
1057
|
+
if (this.disposed) throw new Error("Scope is disposed");
|
|
979
1058
|
let ctrl = this.controllers.get(atom$1);
|
|
980
1059
|
if (!ctrl) {
|
|
981
1060
|
ctrl = new ControllerImpl(atom$1, this);
|
|
@@ -985,7 +1064,7 @@ var ScopeImpl = class {
|
|
|
985
1064
|
return ctrl;
|
|
986
1065
|
}
|
|
987
1066
|
select(atom$1, selector, options) {
|
|
988
|
-
return new SelectHandleImpl(this.controller(atom$1), selector, options?.eq ??
|
|
1067
|
+
return new SelectHandleImpl(this.controller(atom$1), selector, options?.eq ?? Object.is);
|
|
989
1068
|
}
|
|
990
1069
|
getFlowPreset(flow$1) {
|
|
991
1070
|
return this.presets.get(flow$1);
|
|
@@ -1029,10 +1108,27 @@ var ScopeImpl = class {
|
|
|
1029
1108
|
const previousValue = entry.value;
|
|
1030
1109
|
const pendingSet = entry.pendingSet;
|
|
1031
1110
|
entry.pendingSet = void 0;
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1111
|
+
if (pendingSet) {
|
|
1112
|
+
entry.state = "resolving";
|
|
1113
|
+
entry.value = previousValue;
|
|
1114
|
+
entry.error = void 0;
|
|
1115
|
+
entry.pendingInvalidate = false;
|
|
1116
|
+
this.pending.delete(atom$1);
|
|
1117
|
+
this.resolving.delete(atom$1);
|
|
1118
|
+
this.emitStateChange("resolving", atom$1);
|
|
1119
|
+
this.notifyListeners(atom$1, "resolving");
|
|
1120
|
+
if ("value" in pendingSet) entry.value = pendingSet.value;
|
|
1121
|
+
else entry.value = pendingSet.fn(previousValue);
|
|
1122
|
+
entry.state = "resolved";
|
|
1123
|
+
entry.hasValue = true;
|
|
1124
|
+
this.emitStateChange("resolved", atom$1);
|
|
1125
|
+
this.notifyListeners(atom$1, "resolved");
|
|
1126
|
+
this.invalidationChain?.delete(atom$1);
|
|
1127
|
+
return;
|
|
1035
1128
|
}
|
|
1129
|
+
for (let i = entry.cleanups.length - 1; i >= 0; i--) try {
|
|
1130
|
+
await entry.cleanups[i]?.();
|
|
1131
|
+
} catch {}
|
|
1036
1132
|
entry.cleanups = [];
|
|
1037
1133
|
entry.state = "resolving";
|
|
1038
1134
|
entry.value = previousValue;
|
|
@@ -1042,16 +1138,11 @@ var ScopeImpl = class {
|
|
|
1042
1138
|
this.resolving.delete(atom$1);
|
|
1043
1139
|
this.emitStateChange("resolving", atom$1);
|
|
1044
1140
|
this.notifyListeners(atom$1, "resolving");
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
entry.
|
|
1049
|
-
entry.hasValue = true;
|
|
1050
|
-
this.emitStateChange("resolved", atom$1);
|
|
1051
|
-
this.notifyListeners(atom$1, "resolved");
|
|
1052
|
-
return;
|
|
1141
|
+
try {
|
|
1142
|
+
await this.resolve(atom$1);
|
|
1143
|
+
} catch (e) {
|
|
1144
|
+
if (!entry.pendingSet && !entry.pendingInvalidate) throw e;
|
|
1053
1145
|
}
|
|
1054
|
-
await this.resolve(atom$1);
|
|
1055
1146
|
}
|
|
1056
1147
|
async release(atom$1) {
|
|
1057
1148
|
const entry = this.cache.get(atom$1);
|
|
@@ -1060,14 +1151,33 @@ var ScopeImpl = class {
|
|
|
1060
1151
|
clearTimeout(entry.gcScheduled);
|
|
1061
1152
|
entry.gcScheduled = null;
|
|
1062
1153
|
}
|
|
1063
|
-
for (let i = entry.cleanups.length - 1; i >= 0; i--) {
|
|
1064
|
-
|
|
1065
|
-
|
|
1154
|
+
for (let i = entry.cleanups.length - 1; i >= 0; i--) try {
|
|
1155
|
+
await entry.cleanups[i]?.();
|
|
1156
|
+
} catch {}
|
|
1157
|
+
if (atom$1.deps) for (const dep of Object.values(atom$1.deps)) {
|
|
1158
|
+
const depAtom = isAtom(dep) ? dep : isControllerDep(dep) ? dep.atom : null;
|
|
1159
|
+
if (!depAtom) continue;
|
|
1160
|
+
const depEntry = this.cache.get(depAtom);
|
|
1161
|
+
if (depEntry) {
|
|
1162
|
+
depEntry.dependents.delete(atom$1);
|
|
1163
|
+
this.maybeScheduleGC(depAtom);
|
|
1164
|
+
}
|
|
1066
1165
|
}
|
|
1067
1166
|
this.cache.delete(atom$1);
|
|
1068
1167
|
this.controllers.delete(atom$1);
|
|
1168
|
+
for (const [state, stateMap] of this.stateListeners) {
|
|
1169
|
+
stateMap.delete(atom$1);
|
|
1170
|
+
if (stateMap.size === 0) this.stateListeners.delete(state);
|
|
1171
|
+
}
|
|
1069
1172
|
}
|
|
1070
1173
|
async dispose() {
|
|
1174
|
+
if (this.chainPromise) try {
|
|
1175
|
+
await this.chainPromise;
|
|
1176
|
+
} catch {}
|
|
1177
|
+
this.disposed = true;
|
|
1178
|
+
this.invalidationQueue.clear();
|
|
1179
|
+
this.invalidationChain = null;
|
|
1180
|
+
this.chainPromise = null;
|
|
1071
1181
|
for (const ext of this.extensions) if (ext.dispose) await ext.dispose(this);
|
|
1072
1182
|
for (const entry of this.cache.values()) if (entry.gcScheduled) {
|
|
1073
1183
|
clearTimeout(entry.gcScheduled);
|
|
@@ -1078,8 +1188,14 @@ var ScopeImpl = class {
|
|
|
1078
1188
|
}
|
|
1079
1189
|
async flush() {
|
|
1080
1190
|
if (this.chainPromise) await this.chainPromise;
|
|
1191
|
+
if (this.chainError !== null) {
|
|
1192
|
+
const error = this.chainError;
|
|
1193
|
+
this.chainError = null;
|
|
1194
|
+
throw error;
|
|
1195
|
+
}
|
|
1081
1196
|
}
|
|
1082
1197
|
createContext(options) {
|
|
1198
|
+
if (this.disposed) throw new Error("Scope is disposed");
|
|
1083
1199
|
const ctx = new ExecutionContextImpl(this, options);
|
|
1084
1200
|
for (const tagged of options?.tags ?? []) ctx.data.set(tagged.key, tagged.value);
|
|
1085
1201
|
for (const tagged of this.tags) if (!ctx.data.has(tagged.key)) ctx.data.set(tagged.key, tagged.value);
|
|
@@ -1173,7 +1289,7 @@ var ExecutionContextImpl = class ExecutionContextImpl {
|
|
|
1173
1289
|
const resolvedDeps = await this.scope.resolveDeps(flow$1.deps, this);
|
|
1174
1290
|
const factory = flow$1.factory;
|
|
1175
1291
|
const doExec = async () => {
|
|
1176
|
-
if (flow$1.deps
|
|
1292
|
+
if (flow$1.deps) return factory(this, resolvedDeps);
|
|
1177
1293
|
else return factory(this);
|
|
1178
1294
|
};
|
|
1179
1295
|
return this.applyExecExtensions(flow$1, doExec);
|
|
@@ -1189,12 +1305,10 @@ var ExecutionContextImpl = class ExecutionContextImpl {
|
|
|
1189
1305
|
}
|
|
1190
1306
|
async applyExecExtensions(target, doExec) {
|
|
1191
1307
|
let next = doExec;
|
|
1192
|
-
for (let i = this.scope.
|
|
1193
|
-
const ext = this.scope.
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
next = ext.wrapExec.bind(ext, currentNext, target, this);
|
|
1197
|
-
}
|
|
1308
|
+
for (let i = this.scope.execExts.length - 1; i >= 0; i--) {
|
|
1309
|
+
const ext = this.scope.execExts[i];
|
|
1310
|
+
const currentNext = next;
|
|
1311
|
+
next = ext.wrapExec.bind(ext, currentNext, target, this);
|
|
1198
1312
|
}
|
|
1199
1313
|
return next();
|
|
1200
1314
|
}
|
|
@@ -1204,10 +1318,9 @@ var ExecutionContextImpl = class ExecutionContextImpl {
|
|
|
1204
1318
|
async close(result = { ok: true }) {
|
|
1205
1319
|
if (this.closed) return;
|
|
1206
1320
|
this.closed = true;
|
|
1207
|
-
for (let i = this.cleanups.length - 1; i >= 0; i--) {
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
}
|
|
1321
|
+
for (let i = this.cleanups.length - 1; i >= 0; i--) try {
|
|
1322
|
+
await this.cleanups[i]?.(result);
|
|
1323
|
+
} catch {}
|
|
1211
1324
|
}
|
|
1212
1325
|
};
|
|
1213
1326
|
/**
|
|
@@ -1268,6 +1381,7 @@ exports.presetSymbol = presetSymbol;
|
|
|
1268
1381
|
exports.resource = resource;
|
|
1269
1382
|
exports.resourceSymbol = resourceSymbol;
|
|
1270
1383
|
exports.service = service;
|
|
1384
|
+
exports.shallowEqual = shallowEqual;
|
|
1271
1385
|
exports.tag = tag;
|
|
1272
1386
|
exports.tagExecutorSymbol = tagExecutorSymbol;
|
|
1273
1387
|
exports.tagSymbol = tagSymbol;
|