@pumped-fn/lite 2.0.0 → 2.1.2
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 +49 -0
- package/PATTERNS.md +12 -11
- package/README.md +8 -1
- package/dist/index.cjs +224 -99
- package/dist/index.d.cts +46 -9
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +46 -9
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +224 -99
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
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;
|
|
@@ -263,20 +263,33 @@ function isAtom(value) {
|
|
|
263
263
|
* The Controller provides full lifecycle control: get, resolve, release, invalidate, and subscribe.
|
|
264
264
|
*
|
|
265
265
|
* @param atom - The Atom to wrap
|
|
266
|
-
* @param options - Optional configuration
|
|
266
|
+
* @param options - Optional configuration:
|
|
267
|
+
* - `resolve: true` — auto-resolves the dep before the parent factory runs; `config.get()` is safe.
|
|
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 `Object.is` by default). Replaces
|
|
270
|
+
* manual `ctx.cleanup(ctx.scope.on('resolved', dep, () => ctx.invalidate()))` wiring. Watch
|
|
271
|
+
* listeners are auto-cleaned on re-resolve, release, and dispose.
|
|
272
|
+
* - `eq` — custom equality function `(a: T, b: T) => boolean`; only used with `watch: true`.
|
|
267
273
|
* @returns A ControllerDep that resolves to a Controller for the Atom
|
|
268
274
|
*
|
|
269
275
|
* @example
|
|
270
276
|
* ```typescript
|
|
271
|
-
*
|
|
277
|
+
* // resolve only
|
|
272
278
|
* const serverAtom = atom({
|
|
273
279
|
* deps: { config: controller(configAtom, { resolve: true }) },
|
|
274
|
-
* factory: (
|
|
275
|
-
*
|
|
276
|
-
*
|
|
277
|
-
*
|
|
278
|
-
*
|
|
279
|
-
* }
|
|
280
|
+
* factory: (_, { config }) => createServer(config.get().port),
|
|
281
|
+
* })
|
|
282
|
+
*
|
|
283
|
+
* // watch: re-runs parent when dep value changes
|
|
284
|
+
* const profileAtom = atom({
|
|
285
|
+
* deps: { token: controller(tokenAtom, { resolve: true, watch: true }) },
|
|
286
|
+
* factory: (_, { token }) => ({ id: `user-${token.get().jwt}` }),
|
|
287
|
+
* })
|
|
288
|
+
*
|
|
289
|
+
* // watch with custom equality
|
|
290
|
+
* const derivedAtom = atom({
|
|
291
|
+
* deps: { src: controller(srcAtom, { resolve: true, watch: true, eq: (a, b) => a.id === b.id }) },
|
|
292
|
+
* factory: (_, { src }) => src.get().name,
|
|
280
293
|
* })
|
|
281
294
|
* ```
|
|
282
295
|
*/
|
|
@@ -284,7 +297,9 @@ function controller(atom$1, options) {
|
|
|
284
297
|
return {
|
|
285
298
|
[controllerDepSymbol]: true,
|
|
286
299
|
atom: atom$1,
|
|
287
|
-
resolve: options?.resolve
|
|
300
|
+
resolve: options?.resolve,
|
|
301
|
+
watch: options?.watch,
|
|
302
|
+
eq: options?.eq
|
|
288
303
|
};
|
|
289
304
|
}
|
|
290
305
|
/**
|
|
@@ -410,12 +425,14 @@ function isResource(value) {
|
|
|
410
425
|
//#endregion
|
|
411
426
|
//#region src/service.ts
|
|
412
427
|
function service(config) {
|
|
413
|
-
|
|
428
|
+
const atomInstance = {
|
|
414
429
|
[atomSymbol]: true,
|
|
415
430
|
factory: config.factory,
|
|
416
431
|
deps: config.deps,
|
|
417
432
|
tags: config.tags
|
|
418
433
|
};
|
|
434
|
+
if (config.tags?.length) registerAtomToTags(atomInstance, config.tags);
|
|
435
|
+
return atomInstance;
|
|
419
436
|
}
|
|
420
437
|
|
|
421
438
|
//#endregion
|
|
@@ -431,7 +448,7 @@ function getResourceKey(resource$1) {
|
|
|
431
448
|
return key;
|
|
432
449
|
}
|
|
433
450
|
const inflightResources = /* @__PURE__ */ new WeakMap();
|
|
434
|
-
const
|
|
451
|
+
const resolvingResourcesMap = /* @__PURE__ */ new WeakMap();
|
|
435
452
|
var ContextDataImpl = class {
|
|
436
453
|
map = /* @__PURE__ */ new Map();
|
|
437
454
|
constructor(parentData) {
|
|
@@ -456,6 +473,10 @@ var ContextDataImpl = class {
|
|
|
456
473
|
if (this.map.has(key)) return this.map.get(key);
|
|
457
474
|
return this.parentData?.seek(key);
|
|
458
475
|
}
|
|
476
|
+
seekHas(key) {
|
|
477
|
+
if (this.map.has(key)) return true;
|
|
478
|
+
return this.parentData?.seekHas(key) ?? false;
|
|
479
|
+
}
|
|
459
480
|
getTag(tag$1) {
|
|
460
481
|
return this.map.get(tag$1.key);
|
|
461
482
|
}
|
|
@@ -501,6 +522,16 @@ var SelectHandleImpl = class {
|
|
|
501
522
|
return this.currentValue;
|
|
502
523
|
}
|
|
503
524
|
subscribe(listener) {
|
|
525
|
+
if (!this.ctrlUnsub) {
|
|
526
|
+
this.currentValue = this.selector(this.ctrl.get());
|
|
527
|
+
this.ctrlUnsub = this.ctrl.on("resolved", () => {
|
|
528
|
+
const nextValue = this.selector(this.ctrl.get());
|
|
529
|
+
if (!this.eq(this.currentValue, nextValue)) {
|
|
530
|
+
this.currentValue = nextValue;
|
|
531
|
+
this.notifyListeners();
|
|
532
|
+
}
|
|
533
|
+
});
|
|
534
|
+
}
|
|
504
535
|
this.listeners.add(listener);
|
|
505
536
|
return () => {
|
|
506
537
|
this.listeners.delete(listener);
|
|
@@ -508,12 +539,15 @@ var SelectHandleImpl = class {
|
|
|
508
539
|
};
|
|
509
540
|
}
|
|
510
541
|
notifyListeners() {
|
|
511
|
-
for (const listener of this.listeners) listener();
|
|
542
|
+
for (const listener of [...this.listeners]) listener();
|
|
543
|
+
}
|
|
544
|
+
dispose() {
|
|
545
|
+
this.listeners.clear();
|
|
546
|
+
this.cleanup();
|
|
512
547
|
}
|
|
513
548
|
cleanup() {
|
|
514
549
|
this.ctrlUnsub?.();
|
|
515
550
|
this.ctrlUnsub = null;
|
|
516
|
-
this.listeners.clear();
|
|
517
551
|
}
|
|
518
552
|
};
|
|
519
553
|
var ControllerImpl = class {
|
|
@@ -562,11 +596,15 @@ var ScopeImpl = class {
|
|
|
562
596
|
invalidationScheduled = false;
|
|
563
597
|
invalidationChain = null;
|
|
564
598
|
chainPromise = null;
|
|
599
|
+
chainError = null;
|
|
565
600
|
initialized = false;
|
|
601
|
+
disposed = false;
|
|
566
602
|
controllers = /* @__PURE__ */ new Map();
|
|
567
603
|
gcOptions;
|
|
568
604
|
extensions;
|
|
569
605
|
tags;
|
|
606
|
+
resolveExts;
|
|
607
|
+
execExts;
|
|
570
608
|
ready;
|
|
571
609
|
scheduleInvalidation(atom$1) {
|
|
572
610
|
const entry = this.cache.get(atom$1);
|
|
@@ -579,16 +617,22 @@ var ScopeImpl = class {
|
|
|
579
617
|
if (!this.chainPromise) {
|
|
580
618
|
this.invalidationChain = /* @__PURE__ */ new Set();
|
|
581
619
|
this.invalidationScheduled = true;
|
|
582
|
-
this.
|
|
583
|
-
|
|
584
|
-
|
|
620
|
+
this.chainError = null;
|
|
621
|
+
this.chainPromise = (async () => {
|
|
622
|
+
await new Promise((resolve) => {
|
|
623
|
+
queueMicrotask(resolve);
|
|
585
624
|
});
|
|
586
|
-
|
|
625
|
+
try {
|
|
626
|
+
await this.processInvalidationChain();
|
|
627
|
+
} catch (error) {
|
|
628
|
+
if (this.chainError === null) this.chainError = error;
|
|
629
|
+
}
|
|
630
|
+
})();
|
|
587
631
|
}
|
|
588
632
|
}
|
|
589
633
|
async processInvalidationChain() {
|
|
590
634
|
try {
|
|
591
|
-
while (this.invalidationQueue.size > 0) {
|
|
635
|
+
while (this.invalidationQueue.size > 0 && !this.disposed) {
|
|
592
636
|
const atom$1 = this.invalidationQueue.values().next().value;
|
|
593
637
|
this.invalidationQueue.delete(atom$1);
|
|
594
638
|
if (this.invalidationChain.has(atom$1)) {
|
|
@@ -609,6 +653,8 @@ var ScopeImpl = class {
|
|
|
609
653
|
constructor(options) {
|
|
610
654
|
this.extensions = options?.extensions ?? [];
|
|
611
655
|
this.tags = options?.tags ?? [];
|
|
656
|
+
this.resolveExts = this.extensions.filter((e) => e.wrapResolve);
|
|
657
|
+
this.execExts = this.extensions.filter((e) => e.wrapExec);
|
|
612
658
|
for (const p of options?.presets ?? []) this.presets.set(p.target, p.value);
|
|
613
659
|
this.gcOptions = {
|
|
614
660
|
enabled: options?.gc?.enabled ?? true,
|
|
@@ -701,21 +747,21 @@ var ScopeImpl = class {
|
|
|
701
747
|
const entry = this.cache.get(atom$1);
|
|
702
748
|
if (!entry) return;
|
|
703
749
|
const eventListeners = entry.listeners.get(event);
|
|
704
|
-
if (eventListeners) for (const listener of eventListeners) listener();
|
|
750
|
+
if (eventListeners?.size) for (const listener of [...eventListeners]) listener();
|
|
705
751
|
const allListeners = entry.listeners.get("*");
|
|
706
|
-
if (allListeners) for (const listener of allListeners) listener();
|
|
752
|
+
if (allListeners?.size) for (const listener of [...allListeners]) listener();
|
|
707
753
|
}
|
|
708
754
|
notifyAllListeners(atom$1) {
|
|
709
755
|
const entry = this.cache.get(atom$1);
|
|
710
756
|
if (!entry) return;
|
|
711
757
|
const allListeners = entry.listeners.get("*");
|
|
712
|
-
if (allListeners) for (const listener of allListeners) listener();
|
|
758
|
+
if (allListeners?.size) for (const listener of [...allListeners]) listener();
|
|
713
759
|
}
|
|
714
760
|
emitStateChange(state, atom$1) {
|
|
715
761
|
const stateMap = this.stateListeners.get(state);
|
|
716
762
|
if (stateMap) {
|
|
717
763
|
const listeners = stateMap.get(atom$1);
|
|
718
|
-
if (listeners) for (const listener of listeners) listener();
|
|
764
|
+
if (listeners?.size) for (const listener of [...listeners]) listener();
|
|
719
765
|
}
|
|
720
766
|
}
|
|
721
767
|
on(event, atom$1, listener) {
|
|
@@ -741,14 +787,15 @@ var ScopeImpl = class {
|
|
|
741
787
|
};
|
|
742
788
|
}
|
|
743
789
|
async resolve(atom$1) {
|
|
790
|
+
if (this.disposed) throw new Error("Scope is disposed");
|
|
744
791
|
if (!this.initialized) await this.ready;
|
|
745
792
|
const entry = this.cache.get(atom$1);
|
|
746
793
|
if (entry?.state === "resolved") return entry.value;
|
|
747
794
|
const pendingPromise = this.pending.get(atom$1);
|
|
748
795
|
if (pendingPromise) return pendingPromise;
|
|
749
796
|
if (this.resolving.has(atom$1)) throw new Error("Circular dependency detected");
|
|
750
|
-
|
|
751
|
-
|
|
797
|
+
if (this.presets.has(atom$1)) {
|
|
798
|
+
const presetValue = this.presets.get(atom$1);
|
|
752
799
|
if (isAtom(presetValue)) return this.resolve(presetValue);
|
|
753
800
|
const newEntry = this.getOrCreateEntry(atom$1);
|
|
754
801
|
newEntry.state = "resolved";
|
|
@@ -771,6 +818,10 @@ var ScopeImpl = class {
|
|
|
771
818
|
async doResolve(atom$1) {
|
|
772
819
|
const entry = this.getOrCreateEntry(atom$1);
|
|
773
820
|
if (!(entry.state === "resolving")) {
|
|
821
|
+
for (let i = entry.cleanups.length - 1; i >= 0; i--) try {
|
|
822
|
+
await entry.cleanups[i]?.();
|
|
823
|
+
} catch {}
|
|
824
|
+
entry.cleanups = [];
|
|
774
825
|
entry.state = "resolving";
|
|
775
826
|
this.emitStateChange("resolving", atom$1);
|
|
776
827
|
this.notifyListeners(atom$1, "resolving");
|
|
@@ -789,7 +840,7 @@ var ScopeImpl = class {
|
|
|
789
840
|
};
|
|
790
841
|
const factory = atom$1.factory;
|
|
791
842
|
const doResolve = async () => {
|
|
792
|
-
if (atom$1.deps
|
|
843
|
+
if (atom$1.deps) return factory(ctx, resolvedDeps);
|
|
793
844
|
else return factory(ctx);
|
|
794
845
|
};
|
|
795
846
|
try {
|
|
@@ -825,70 +876,103 @@ var ScopeImpl = class {
|
|
|
825
876
|
entry.pendingInvalidate = false;
|
|
826
877
|
this.invalidationChain?.delete(atom$1);
|
|
827
878
|
this.scheduleInvalidation(atom$1);
|
|
828
|
-
}
|
|
879
|
+
} else if (entry.pendingSet && "value" in entry.pendingSet) {
|
|
880
|
+
this.invalidationChain?.delete(atom$1);
|
|
881
|
+
this.scheduleInvalidation(atom$1);
|
|
882
|
+
} else entry.pendingSet = void 0;
|
|
829
883
|
throw entry.error;
|
|
830
884
|
}
|
|
831
885
|
}
|
|
832
886
|
async applyResolveExtensions(event, doResolve) {
|
|
833
887
|
let next = doResolve;
|
|
834
|
-
for (let i = this.
|
|
835
|
-
const ext = this.
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
next = ext.wrapResolve.bind(ext, currentNext, event);
|
|
839
|
-
}
|
|
888
|
+
for (let i = this.resolveExts.length - 1; i >= 0; i--) {
|
|
889
|
+
const ext = this.resolveExts[i];
|
|
890
|
+
const currentNext = next;
|
|
891
|
+
next = ext.wrapResolve.bind(ext, currentNext, event);
|
|
840
892
|
}
|
|
841
893
|
return next();
|
|
842
894
|
}
|
|
843
895
|
async resolveDeps(deps, ctx, dependentAtom) {
|
|
844
896
|
if (!deps) return {};
|
|
845
897
|
const result = {};
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
result[key] = ctrl;
|
|
856
|
-
if (dependentAtom) {
|
|
857
|
-
const depEntry = this.getEntry(dep.atom);
|
|
858
|
-
if (depEntry) depEntry.dependents.add(dependentAtom);
|
|
859
|
-
}
|
|
860
|
-
} else if (tagExecutorSymbol in dep) {
|
|
861
|
-
const tagExecutor = dep;
|
|
862
|
-
switch (tagExecutor.mode) {
|
|
863
|
-
case "required": {
|
|
864
|
-
const value = ctx ? ctx.data.seekTag(tagExecutor.tag) : tagExecutor.tag.find(this.tags);
|
|
865
|
-
if (value !== void 0) result[key] = value;
|
|
866
|
-
else if (tagExecutor.tag.hasDefault) result[key] = tagExecutor.tag.defaultValue;
|
|
867
|
-
else throw new Error(`Tag "${tagExecutor.tag.label}" not found`);
|
|
868
|
-
break;
|
|
898
|
+
const parallel = [];
|
|
899
|
+
const deferredResources = [];
|
|
900
|
+
for (const key in deps) {
|
|
901
|
+
const dep = deps[key];
|
|
902
|
+
if (isAtom(dep)) parallel.push(this.resolve(dep).then((value) => {
|
|
903
|
+
result[key] = value;
|
|
904
|
+
if (dependentAtom) {
|
|
905
|
+
const depEntry = this.getEntry(dep);
|
|
906
|
+
if (depEntry) depEntry.dependents.add(dependentAtom);
|
|
869
907
|
}
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
908
|
+
}));
|
|
909
|
+
else if (isControllerDep(dep)) {
|
|
910
|
+
if (dep.watch) {
|
|
911
|
+
if (!dependentAtom) throw new Error("controller({ watch: true }) is only supported in atom dependencies");
|
|
912
|
+
if (!dep.resolve) throw new Error("controller({ watch: true }) requires resolve: true");
|
|
913
|
+
}
|
|
914
|
+
const ctrl = this.controller(dep.atom);
|
|
915
|
+
if (dep.resolve) parallel.push(ctrl.resolve().then(() => {
|
|
916
|
+
result[key] = ctrl;
|
|
917
|
+
if (dependentAtom) {
|
|
918
|
+
const depEntry = this.getEntry(dep.atom);
|
|
919
|
+
if (depEntry) depEntry.dependents.add(dependentAtom);
|
|
920
|
+
}
|
|
921
|
+
if (dep.watch) {
|
|
922
|
+
const eq = dep.eq ?? Object.is;
|
|
923
|
+
let prev = ctrl.get();
|
|
924
|
+
const unsub = this.on("resolved", dep.atom, () => {
|
|
925
|
+
const next = ctrl.get();
|
|
926
|
+
if (!eq(prev, next)) {
|
|
927
|
+
prev = next;
|
|
928
|
+
this.scheduleInvalidation(dependentAtom);
|
|
929
|
+
}
|
|
930
|
+
});
|
|
931
|
+
const depEntry = this.getEntry(dependentAtom);
|
|
932
|
+
if (depEntry) depEntry.cleanups.push(unsub);
|
|
933
|
+
else unsub();
|
|
934
|
+
}
|
|
935
|
+
}));
|
|
936
|
+
else {
|
|
937
|
+
result[key] = ctrl;
|
|
938
|
+
if (dependentAtom) {
|
|
939
|
+
const depEntry = this.getEntry(dep.atom);
|
|
940
|
+
if (depEntry) depEntry.dependents.add(dependentAtom);
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
} else if (tagExecutorSymbol in dep) {
|
|
944
|
+
const tagExecutor = dep;
|
|
945
|
+
switch (tagExecutor.mode) {
|
|
946
|
+
case "required": {
|
|
947
|
+
const value = ctx ? ctx.data.seekTag(tagExecutor.tag) : tagExecutor.tag.find(this.tags);
|
|
948
|
+
if (value !== void 0) result[key] = value;
|
|
949
|
+
else if (tagExecutor.tag.hasDefault) result[key] = tagExecutor.tag.defaultValue;
|
|
950
|
+
else throw new Error(`Tag "${tagExecutor.tag.label}" not found`);
|
|
951
|
+
break;
|
|
952
|
+
}
|
|
953
|
+
case "optional":
|
|
954
|
+
result[key] = (ctx ? ctx.data.seekTag(tagExecutor.tag) : tagExecutor.tag.find(this.tags)) ?? tagExecutor.tag.defaultValue;
|
|
955
|
+
break;
|
|
956
|
+
case "all":
|
|
957
|
+
result[key] = ctx ? this.collectFromHierarchy(ctx, tagExecutor.tag) : tagExecutor.tag.collect(this.tags);
|
|
958
|
+
break;
|
|
959
|
+
}
|
|
960
|
+
} else if (isResource(dep)) deferredResources.push([key, dep]);
|
|
961
|
+
}
|
|
962
|
+
if (parallel.length === 1) await parallel[0];
|
|
963
|
+
else if (parallel.length > 1) await Promise.all(parallel);
|
|
964
|
+
for (const [key, resource$1] of deferredResources) {
|
|
878
965
|
if (!ctx) throw new Error("Resource deps require an ExecutionContext");
|
|
879
|
-
const resource$1 = dep;
|
|
880
966
|
const resourceKey = getResourceKey(resource$1);
|
|
881
967
|
const storeCtx = ctx.parent ?? ctx;
|
|
882
968
|
if (storeCtx.data.has(resourceKey)) {
|
|
883
969
|
result[key] = storeCtx.data.get(resourceKey);
|
|
884
970
|
continue;
|
|
885
971
|
}
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
result[key] = existingSeek;
|
|
972
|
+
if (ctx.data.seekHas(resourceKey)) {
|
|
973
|
+
result[key] = ctx.data.seek(resourceKey);
|
|
889
974
|
continue;
|
|
890
975
|
}
|
|
891
|
-
if (resolvingResources.has(resourceKey)) throw new Error(`Circular resource dependency detected: ${resource$1.name ?? "anonymous"}`);
|
|
892
976
|
let flights = inflightResources.get(storeCtx.data);
|
|
893
977
|
if (!flights) {
|
|
894
978
|
flights = /* @__PURE__ */ new Map();
|
|
@@ -899,8 +983,14 @@ var ScopeImpl = class {
|
|
|
899
983
|
result[key] = await inflight;
|
|
900
984
|
continue;
|
|
901
985
|
}
|
|
986
|
+
let localResolvingResources = resolvingResourcesMap.get(storeCtx.data);
|
|
987
|
+
if (!localResolvingResources) {
|
|
988
|
+
localResolvingResources = /* @__PURE__ */ new Set();
|
|
989
|
+
resolvingResourcesMap.set(storeCtx.data, localResolvingResources);
|
|
990
|
+
}
|
|
991
|
+
if (localResolvingResources.has(resourceKey)) throw new Error(`Circular resource dependency detected: ${resource$1.name ?? "anonymous"}`);
|
|
902
992
|
const resolve = async () => {
|
|
903
|
-
|
|
993
|
+
localResolvingResources.add(resourceKey);
|
|
904
994
|
try {
|
|
905
995
|
const resourceDeps = await this.resolveDeps(resource$1.deps, ctx);
|
|
906
996
|
const event = {
|
|
@@ -910,14 +1000,14 @@ var ScopeImpl = class {
|
|
|
910
1000
|
};
|
|
911
1001
|
const doResolve = async () => {
|
|
912
1002
|
const factory = resource$1.factory;
|
|
913
|
-
if (resource$1.deps
|
|
1003
|
+
if (resource$1.deps) return factory(storeCtx, resourceDeps);
|
|
914
1004
|
return factory(storeCtx);
|
|
915
1005
|
};
|
|
916
1006
|
const value = await this.applyResolveExtensions(event, doResolve);
|
|
917
1007
|
storeCtx.data.set(resourceKey, value);
|
|
918
1008
|
return value;
|
|
919
1009
|
} finally {
|
|
920
|
-
|
|
1010
|
+
localResolvingResources.delete(resourceKey);
|
|
921
1011
|
}
|
|
922
1012
|
};
|
|
923
1013
|
const promise = resolve();
|
|
@@ -941,6 +1031,7 @@ var ScopeImpl = class {
|
|
|
941
1031
|
return results;
|
|
942
1032
|
}
|
|
943
1033
|
controller(atom$1, options) {
|
|
1034
|
+
if (this.disposed) throw new Error("Scope is disposed");
|
|
944
1035
|
let ctrl = this.controllers.get(atom$1);
|
|
945
1036
|
if (!ctrl) {
|
|
946
1037
|
ctrl = new ControllerImpl(atom$1, this);
|
|
@@ -950,7 +1041,7 @@ var ScopeImpl = class {
|
|
|
950
1041
|
return ctrl;
|
|
951
1042
|
}
|
|
952
1043
|
select(atom$1, selector, options) {
|
|
953
|
-
return new SelectHandleImpl(this.controller(atom$1), selector, options?.eq ??
|
|
1044
|
+
return new SelectHandleImpl(this.controller(atom$1), selector, options?.eq ?? Object.is);
|
|
954
1045
|
}
|
|
955
1046
|
getFlowPreset(flow$1) {
|
|
956
1047
|
return this.presets.get(flow$1);
|
|
@@ -994,10 +1085,27 @@ var ScopeImpl = class {
|
|
|
994
1085
|
const previousValue = entry.value;
|
|
995
1086
|
const pendingSet = entry.pendingSet;
|
|
996
1087
|
entry.pendingSet = void 0;
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1088
|
+
if (pendingSet) {
|
|
1089
|
+
entry.state = "resolving";
|
|
1090
|
+
entry.value = previousValue;
|
|
1091
|
+
entry.error = void 0;
|
|
1092
|
+
entry.pendingInvalidate = false;
|
|
1093
|
+
this.pending.delete(atom$1);
|
|
1094
|
+
this.resolving.delete(atom$1);
|
|
1095
|
+
this.emitStateChange("resolving", atom$1);
|
|
1096
|
+
this.notifyListeners(atom$1, "resolving");
|
|
1097
|
+
if ("value" in pendingSet) entry.value = pendingSet.value;
|
|
1098
|
+
else entry.value = pendingSet.fn(previousValue);
|
|
1099
|
+
entry.state = "resolved";
|
|
1100
|
+
entry.hasValue = true;
|
|
1101
|
+
this.emitStateChange("resolved", atom$1);
|
|
1102
|
+
this.notifyListeners(atom$1, "resolved");
|
|
1103
|
+
this.invalidationChain?.delete(atom$1);
|
|
1104
|
+
return;
|
|
1000
1105
|
}
|
|
1106
|
+
for (let i = entry.cleanups.length - 1; i >= 0; i--) try {
|
|
1107
|
+
await entry.cleanups[i]?.();
|
|
1108
|
+
} catch {}
|
|
1001
1109
|
entry.cleanups = [];
|
|
1002
1110
|
entry.state = "resolving";
|
|
1003
1111
|
entry.value = previousValue;
|
|
@@ -1007,16 +1115,11 @@ var ScopeImpl = class {
|
|
|
1007
1115
|
this.resolving.delete(atom$1);
|
|
1008
1116
|
this.emitStateChange("resolving", atom$1);
|
|
1009
1117
|
this.notifyListeners(atom$1, "resolving");
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
entry.
|
|
1014
|
-
entry.hasValue = true;
|
|
1015
|
-
this.emitStateChange("resolved", atom$1);
|
|
1016
|
-
this.notifyListeners(atom$1, "resolved");
|
|
1017
|
-
return;
|
|
1118
|
+
try {
|
|
1119
|
+
await this.resolve(atom$1);
|
|
1120
|
+
} catch (e) {
|
|
1121
|
+
if (!entry.pendingSet && !entry.pendingInvalidate) throw e;
|
|
1018
1122
|
}
|
|
1019
|
-
await this.resolve(atom$1);
|
|
1020
1123
|
}
|
|
1021
1124
|
async release(atom$1) {
|
|
1022
1125
|
const entry = this.cache.get(atom$1);
|
|
@@ -1025,14 +1128,33 @@ var ScopeImpl = class {
|
|
|
1025
1128
|
clearTimeout(entry.gcScheduled);
|
|
1026
1129
|
entry.gcScheduled = null;
|
|
1027
1130
|
}
|
|
1028
|
-
for (let i = entry.cleanups.length - 1; i >= 0; i--) {
|
|
1029
|
-
|
|
1030
|
-
|
|
1131
|
+
for (let i = entry.cleanups.length - 1; i >= 0; i--) try {
|
|
1132
|
+
await entry.cleanups[i]?.();
|
|
1133
|
+
} catch {}
|
|
1134
|
+
if (atom$1.deps) for (const dep of Object.values(atom$1.deps)) {
|
|
1135
|
+
const depAtom = isAtom(dep) ? dep : isControllerDep(dep) ? dep.atom : null;
|
|
1136
|
+
if (!depAtom) continue;
|
|
1137
|
+
const depEntry = this.cache.get(depAtom);
|
|
1138
|
+
if (depEntry) {
|
|
1139
|
+
depEntry.dependents.delete(atom$1);
|
|
1140
|
+
this.maybeScheduleGC(depAtom);
|
|
1141
|
+
}
|
|
1031
1142
|
}
|
|
1032
1143
|
this.cache.delete(atom$1);
|
|
1033
1144
|
this.controllers.delete(atom$1);
|
|
1145
|
+
for (const [state, stateMap] of this.stateListeners) {
|
|
1146
|
+
stateMap.delete(atom$1);
|
|
1147
|
+
if (stateMap.size === 0) this.stateListeners.delete(state);
|
|
1148
|
+
}
|
|
1034
1149
|
}
|
|
1035
1150
|
async dispose() {
|
|
1151
|
+
if (this.chainPromise) try {
|
|
1152
|
+
await this.chainPromise;
|
|
1153
|
+
} catch {}
|
|
1154
|
+
this.disposed = true;
|
|
1155
|
+
this.invalidationQueue.clear();
|
|
1156
|
+
this.invalidationChain = null;
|
|
1157
|
+
this.chainPromise = null;
|
|
1036
1158
|
for (const ext of this.extensions) if (ext.dispose) await ext.dispose(this);
|
|
1037
1159
|
for (const entry of this.cache.values()) if (entry.gcScheduled) {
|
|
1038
1160
|
clearTimeout(entry.gcScheduled);
|
|
@@ -1043,8 +1165,14 @@ var ScopeImpl = class {
|
|
|
1043
1165
|
}
|
|
1044
1166
|
async flush() {
|
|
1045
1167
|
if (this.chainPromise) await this.chainPromise;
|
|
1168
|
+
if (this.chainError !== null) {
|
|
1169
|
+
const error = this.chainError;
|
|
1170
|
+
this.chainError = null;
|
|
1171
|
+
throw error;
|
|
1172
|
+
}
|
|
1046
1173
|
}
|
|
1047
1174
|
createContext(options) {
|
|
1175
|
+
if (this.disposed) throw new Error("Scope is disposed");
|
|
1048
1176
|
const ctx = new ExecutionContextImpl(this, options);
|
|
1049
1177
|
for (const tagged of options?.tags ?? []) ctx.data.set(tagged.key, tagged.value);
|
|
1050
1178
|
for (const tagged of this.tags) if (!ctx.data.has(tagged.key)) ctx.data.set(tagged.key, tagged.value);
|
|
@@ -1138,7 +1266,7 @@ var ExecutionContextImpl = class ExecutionContextImpl {
|
|
|
1138
1266
|
const resolvedDeps = await this.scope.resolveDeps(flow$1.deps, this);
|
|
1139
1267
|
const factory = flow$1.factory;
|
|
1140
1268
|
const doExec = async () => {
|
|
1141
|
-
if (flow$1.deps
|
|
1269
|
+
if (flow$1.deps) return factory(this, resolvedDeps);
|
|
1142
1270
|
else return factory(this);
|
|
1143
1271
|
};
|
|
1144
1272
|
return this.applyExecExtensions(flow$1, doExec);
|
|
@@ -1154,12 +1282,10 @@ var ExecutionContextImpl = class ExecutionContextImpl {
|
|
|
1154
1282
|
}
|
|
1155
1283
|
async applyExecExtensions(target, doExec) {
|
|
1156
1284
|
let next = doExec;
|
|
1157
|
-
for (let i = this.scope.
|
|
1158
|
-
const ext = this.scope.
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
next = ext.wrapExec.bind(ext, currentNext, target, this);
|
|
1162
|
-
}
|
|
1285
|
+
for (let i = this.scope.execExts.length - 1; i >= 0; i--) {
|
|
1286
|
+
const ext = this.scope.execExts[i];
|
|
1287
|
+
const currentNext = next;
|
|
1288
|
+
next = ext.wrapExec.bind(ext, currentNext, target, this);
|
|
1163
1289
|
}
|
|
1164
1290
|
return next();
|
|
1165
1291
|
}
|
|
@@ -1169,10 +1295,9 @@ var ExecutionContextImpl = class ExecutionContextImpl {
|
|
|
1169
1295
|
async close(result = { ok: true }) {
|
|
1170
1296
|
if (this.closed) return;
|
|
1171
1297
|
this.closed = true;
|
|
1172
|
-
for (let i = this.cleanups.length - 1; i >= 0; i--) {
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
}
|
|
1298
|
+
for (let i = this.cleanups.length - 1; i >= 0; i--) try {
|
|
1299
|
+
await this.cleanups[i]?.(result);
|
|
1300
|
+
} catch {}
|
|
1176
1301
|
}
|
|
1177
1302
|
};
|
|
1178
1303
|
/**
|
package/dist/index.d.cts
CHANGED
|
@@ -89,6 +89,10 @@ declare namespace Lite {
|
|
|
89
89
|
* Returns first match or undefined (ignores tag defaults).
|
|
90
90
|
*/
|
|
91
91
|
seekTag<T>(tag: Tag<T, boolean>): T | undefined;
|
|
92
|
+
/**
|
|
93
|
+
* Check if key exists locally or in parent chain.
|
|
94
|
+
*/
|
|
95
|
+
seekHas(key: string | symbol): boolean;
|
|
92
96
|
/** Get value by tag, returns undefined if not stored */
|
|
93
97
|
getTag<T>(tag: Tag<T, boolean>): T | undefined;
|
|
94
98
|
/** Set value by tag */
|
|
@@ -198,6 +202,7 @@ declare namespace Lite {
|
|
|
198
202
|
interface SelectHandle<S> {
|
|
199
203
|
get(): S;
|
|
200
204
|
subscribe(listener: () => void): () => void;
|
|
205
|
+
dispose(): void;
|
|
201
206
|
}
|
|
202
207
|
interface Tag<T, HasDefault extends boolean = false> {
|
|
203
208
|
readonly [tagSymbol]: true;
|
|
@@ -230,10 +235,29 @@ declare namespace Lite {
|
|
|
230
235
|
readonly [controllerDepSymbol]: true;
|
|
231
236
|
readonly atom: Atom<T>;
|
|
232
237
|
readonly resolve?: boolean;
|
|
238
|
+
readonly watch?: boolean;
|
|
239
|
+
readonly eq?: (a: any, b: any) => boolean;
|
|
233
240
|
}
|
|
234
241
|
interface ControllerOptions {
|
|
235
242
|
resolve?: boolean;
|
|
236
243
|
}
|
|
244
|
+
type ControllerDepOptions<T> = {
|
|
245
|
+
resolve: true;
|
|
246
|
+
watch: true;
|
|
247
|
+
eq: (a: T, b: T) => boolean;
|
|
248
|
+
} | {
|
|
249
|
+
resolve: true;
|
|
250
|
+
watch: true;
|
|
251
|
+
eq?: never;
|
|
252
|
+
} | {
|
|
253
|
+
resolve: true;
|
|
254
|
+
watch?: never;
|
|
255
|
+
eq?: never;
|
|
256
|
+
} | {
|
|
257
|
+
resolve?: never;
|
|
258
|
+
watch?: never;
|
|
259
|
+
eq?: never;
|
|
260
|
+
};
|
|
237
261
|
interface Typed<T> {
|
|
238
262
|
readonly [typedSymbol]: true;
|
|
239
263
|
}
|
|
@@ -572,24 +596,37 @@ declare function isAtom(value: unknown): value is Lite.Atom<unknown>;
|
|
|
572
596
|
* The Controller provides full lifecycle control: get, resolve, release, invalidate, and subscribe.
|
|
573
597
|
*
|
|
574
598
|
* @param atom - The Atom to wrap
|
|
575
|
-
* @param options - Optional configuration
|
|
599
|
+
* @param options - Optional configuration:
|
|
600
|
+
* - `resolve: true` — auto-resolves the dep before the parent factory runs; `config.get()` is safe.
|
|
601
|
+
* - `watch: true` — atom deps only; requires `resolve: true`; automatically re-runs the parent factory
|
|
602
|
+
* when the dep resolves to a new value (value-equality gated via `Object.is` by default). Replaces
|
|
603
|
+
* manual `ctx.cleanup(ctx.scope.on('resolved', dep, () => ctx.invalidate()))` wiring. Watch
|
|
604
|
+
* listeners are auto-cleaned on re-resolve, release, and dispose.
|
|
605
|
+
* - `eq` — custom equality function `(a: T, b: T) => boolean`; only used with `watch: true`.
|
|
576
606
|
* @returns A ControllerDep that resolves to a Controller for the Atom
|
|
577
607
|
*
|
|
578
608
|
* @example
|
|
579
609
|
* ```typescript
|
|
580
|
-
*
|
|
610
|
+
* // resolve only
|
|
581
611
|
* const serverAtom = atom({
|
|
582
612
|
* deps: { config: controller(configAtom, { resolve: true }) },
|
|
583
|
-
* factory: (
|
|
584
|
-
*
|
|
585
|
-
*
|
|
586
|
-
*
|
|
587
|
-
*
|
|
588
|
-
* }
|
|
613
|
+
* factory: (_, { config }) => createServer(config.get().port),
|
|
614
|
+
* })
|
|
615
|
+
*
|
|
616
|
+
* // watch: re-runs parent when dep value changes
|
|
617
|
+
* const profileAtom = atom({
|
|
618
|
+
* deps: { token: controller(tokenAtom, { resolve: true, watch: true }) },
|
|
619
|
+
* factory: (_, { token }) => ({ id: `user-${token.get().jwt}` }),
|
|
620
|
+
* })
|
|
621
|
+
*
|
|
622
|
+
* // watch with custom equality
|
|
623
|
+
* const derivedAtom = atom({
|
|
624
|
+
* deps: { src: controller(srcAtom, { resolve: true, watch: true, eq: (a, b) => a.id === b.id }) },
|
|
625
|
+
* factory: (_, { src }) => src.get().name,
|
|
589
626
|
* })
|
|
590
627
|
* ```
|
|
591
628
|
*/
|
|
592
|
-
declare function controller<T>(atom: Lite.Atom<T>, options?: Lite.
|
|
629
|
+
declare function controller<T>(atom: Lite.Atom<T>, options?: Lite.ControllerDepOptions<T>): Lite.ControllerDep<T>;
|
|
593
630
|
/**
|
|
594
631
|
* Type guard to check if a value is a ControllerDep wrapper.
|
|
595
632
|
*
|