@pumped-fn/lite 2.1.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 CHANGED
@@ -1,5 +1,46 @@
1
1
  # @pumped-fn/lite
2
2
 
3
+ ## 2.1.2
4
+
5
+ ### Patch Changes
6
+
7
+ - 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.
8
+ - 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.
9
+
10
+ ## 2.1.1
11
+
12
+ ### Patch Changes
13
+
14
+ - 2ce41fc: Fix 16 bugs found via adversarial triage + 5 rounds of Codex review:
15
+
16
+ **Correctness**
17
+
18
+ - `preset(atom, undefined)` now works — uses `has()` check instead of `!== undefined`
19
+ - `seekHas()` traverses parent chain via interface dispatch, not `instanceof`
20
+ - Error-path `pendingSet` only reschedules value-type sets — `fn(undefined)` no longer produces garbage
21
+ - `doInvalidateSequential` swallows resolve errors when pending operations exist
22
+ - Resource cycle detection moved to per-execution-chain WeakMap — fixes false errors with `ctx.exec()`
23
+ - Resource inflight check runs before circular check — sibling `ctx.exec()` no longer false-positives
24
+
25
+ **Reactive system**
26
+
27
+ - `set()`/`update()` pendingSet path skips cleanups — watch deps preserved since factory doesn't re-run
28
+ - Unconditional `invalidationChain.delete()` in pendingSet fast-path — prevents self-loops
29
+ - Copy-on-iterate on all 4 listener iteration sites — unsub during notification no longer drops siblings
30
+
31
+ **Lifecycle**
32
+
33
+ - `dispose()` awaits `chainPromise` before setting `disposed` — drains pending invalidation chain
34
+ - `resolve()`, `controller()`, `createContext()` throw after dispose
35
+ - `release()` cleans up dependents + schedules GC on freed deps
36
+
37
+ **SelectHandle**
38
+
39
+ - Eager subscription in constructor — tracks changes without active subscribers
40
+ - `dispose()` method for explicit teardown
41
+ - Re-subscribe refreshes cached value after auto-cleanup
42
+ - Added `seekHas()` to `ContextData` interface, `dispose()` to `SelectHandle` interface
43
+
3
44
  ## 2.1.0
4
45
 
5
46
  ### Minor Changes
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.for(`@pumped-fn/lite/tag/${options.label}`);
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;
@@ -425,12 +425,14 @@ function isResource(value) {
425
425
  //#endregion
426
426
  //#region src/service.ts
427
427
  function service(config) {
428
- return {
428
+ const atomInstance = {
429
429
  [atomSymbol]: true,
430
430
  factory: config.factory,
431
431
  deps: config.deps,
432
432
  tags: config.tags
433
433
  };
434
+ if (config.tags?.length) registerAtomToTags(atomInstance, config.tags);
435
+ return atomInstance;
434
436
  }
435
437
 
436
438
  //#endregion
@@ -446,7 +448,7 @@ function getResourceKey(resource$1) {
446
448
  return key;
447
449
  }
448
450
  const inflightResources = /* @__PURE__ */ new WeakMap();
449
- const resolvingResources = /* @__PURE__ */ new Set();
451
+ const resolvingResourcesMap = /* @__PURE__ */ new WeakMap();
450
452
  var ContextDataImpl = class {
451
453
  map = /* @__PURE__ */ new Map();
452
454
  constructor(parentData) {
@@ -471,6 +473,10 @@ var ContextDataImpl = class {
471
473
  if (this.map.has(key)) return this.map.get(key);
472
474
  return this.parentData?.seek(key);
473
475
  }
476
+ seekHas(key) {
477
+ if (this.map.has(key)) return true;
478
+ return this.parentData?.seekHas(key) ?? false;
479
+ }
474
480
  getTag(tag$1) {
475
481
  return this.map.get(tag$1.key);
476
482
  }
@@ -516,6 +522,16 @@ var SelectHandleImpl = class {
516
522
  return this.currentValue;
517
523
  }
518
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
+ }
519
535
  this.listeners.add(listener);
520
536
  return () => {
521
537
  this.listeners.delete(listener);
@@ -523,12 +539,15 @@ var SelectHandleImpl = class {
523
539
  };
524
540
  }
525
541
  notifyListeners() {
526
- 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();
527
547
  }
528
548
  cleanup() {
529
549
  this.ctrlUnsub?.();
530
550
  this.ctrlUnsub = null;
531
- this.listeners.clear();
532
551
  }
533
552
  };
534
553
  var ControllerImpl = class {
@@ -577,11 +596,15 @@ var ScopeImpl = class {
577
596
  invalidationScheduled = false;
578
597
  invalidationChain = null;
579
598
  chainPromise = null;
599
+ chainError = null;
580
600
  initialized = false;
601
+ disposed = false;
581
602
  controllers = /* @__PURE__ */ new Map();
582
603
  gcOptions;
583
604
  extensions;
584
605
  tags;
606
+ resolveExts;
607
+ execExts;
585
608
  ready;
586
609
  scheduleInvalidation(atom$1) {
587
610
  const entry = this.cache.get(atom$1);
@@ -594,16 +617,22 @@ var ScopeImpl = class {
594
617
  if (!this.chainPromise) {
595
618
  this.invalidationChain = /* @__PURE__ */ new Set();
596
619
  this.invalidationScheduled = true;
597
- this.chainPromise = new Promise((resolve, reject) => {
598
- queueMicrotask(() => {
599
- this.processInvalidationChain().then(resolve).catch(reject);
620
+ this.chainError = null;
621
+ this.chainPromise = (async () => {
622
+ await new Promise((resolve) => {
623
+ queueMicrotask(resolve);
600
624
  });
601
- });
625
+ try {
626
+ await this.processInvalidationChain();
627
+ } catch (error) {
628
+ if (this.chainError === null) this.chainError = error;
629
+ }
630
+ })();
602
631
  }
603
632
  }
604
633
  async processInvalidationChain() {
605
634
  try {
606
- while (this.invalidationQueue.size > 0) {
635
+ while (this.invalidationQueue.size > 0 && !this.disposed) {
607
636
  const atom$1 = this.invalidationQueue.values().next().value;
608
637
  this.invalidationQueue.delete(atom$1);
609
638
  if (this.invalidationChain.has(atom$1)) {
@@ -624,6 +653,8 @@ var ScopeImpl = class {
624
653
  constructor(options) {
625
654
  this.extensions = options?.extensions ?? [];
626
655
  this.tags = options?.tags ?? [];
656
+ this.resolveExts = this.extensions.filter((e) => e.wrapResolve);
657
+ this.execExts = this.extensions.filter((e) => e.wrapExec);
627
658
  for (const p of options?.presets ?? []) this.presets.set(p.target, p.value);
628
659
  this.gcOptions = {
629
660
  enabled: options?.gc?.enabled ?? true,
@@ -716,21 +747,21 @@ var ScopeImpl = class {
716
747
  const entry = this.cache.get(atom$1);
717
748
  if (!entry) return;
718
749
  const eventListeners = entry.listeners.get(event);
719
- if (eventListeners) for (const listener of eventListeners) listener();
750
+ if (eventListeners?.size) for (const listener of [...eventListeners]) listener();
720
751
  const allListeners = entry.listeners.get("*");
721
- if (allListeners) for (const listener of allListeners) listener();
752
+ if (allListeners?.size) for (const listener of [...allListeners]) listener();
722
753
  }
723
754
  notifyAllListeners(atom$1) {
724
755
  const entry = this.cache.get(atom$1);
725
756
  if (!entry) return;
726
757
  const allListeners = entry.listeners.get("*");
727
- if (allListeners) for (const listener of allListeners) listener();
758
+ if (allListeners?.size) for (const listener of [...allListeners]) listener();
728
759
  }
729
760
  emitStateChange(state, atom$1) {
730
761
  const stateMap = this.stateListeners.get(state);
731
762
  if (stateMap) {
732
763
  const listeners = stateMap.get(atom$1);
733
- if (listeners) for (const listener of listeners) listener();
764
+ if (listeners?.size) for (const listener of [...listeners]) listener();
734
765
  }
735
766
  }
736
767
  on(event, atom$1, listener) {
@@ -756,14 +787,15 @@ var ScopeImpl = class {
756
787
  };
757
788
  }
758
789
  async resolve(atom$1) {
790
+ if (this.disposed) throw new Error("Scope is disposed");
759
791
  if (!this.initialized) await this.ready;
760
792
  const entry = this.cache.get(atom$1);
761
793
  if (entry?.state === "resolved") return entry.value;
762
794
  const pendingPromise = this.pending.get(atom$1);
763
795
  if (pendingPromise) return pendingPromise;
764
796
  if (this.resolving.has(atom$1)) throw new Error("Circular dependency detected");
765
- const presetValue = this.presets.get(atom$1);
766
- if (presetValue !== void 0) {
797
+ if (this.presets.has(atom$1)) {
798
+ const presetValue = this.presets.get(atom$1);
767
799
  if (isAtom(presetValue)) return this.resolve(presetValue);
768
800
  const newEntry = this.getOrCreateEntry(atom$1);
769
801
  newEntry.state = "resolved";
@@ -786,7 +818,9 @@ var ScopeImpl = class {
786
818
  async doResolve(atom$1) {
787
819
  const entry = this.getOrCreateEntry(atom$1);
788
820
  if (!(entry.state === "resolving")) {
789
- for (let i = entry.cleanups.length - 1; i >= 0; i--) await entry.cleanups[i]?.();
821
+ for (let i = entry.cleanups.length - 1; i >= 0; i--) try {
822
+ await entry.cleanups[i]?.();
823
+ } catch {}
790
824
  entry.cleanups = [];
791
825
  entry.state = "resolving";
792
826
  this.emitStateChange("resolving", atom$1);
@@ -806,7 +840,7 @@ var ScopeImpl = class {
806
840
  };
807
841
  const factory = atom$1.factory;
808
842
  const doResolve = async () => {
809
- if (atom$1.deps && Object.keys(atom$1.deps).length > 0) return factory(ctx, resolvedDeps);
843
+ if (atom$1.deps) return factory(ctx, resolvedDeps);
810
844
  else return factory(ctx);
811
845
  };
812
846
  try {
@@ -842,88 +876,103 @@ var ScopeImpl = class {
842
876
  entry.pendingInvalidate = false;
843
877
  this.invalidationChain?.delete(atom$1);
844
878
  this.scheduleInvalidation(atom$1);
845
- }
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;
846
883
  throw entry.error;
847
884
  }
848
885
  }
849
886
  async applyResolveExtensions(event, doResolve) {
850
887
  let next = doResolve;
851
- for (let i = this.extensions.length - 1; i >= 0; i--) {
852
- const ext = this.extensions[i];
853
- if (ext?.wrapResolve) {
854
- const currentNext = next;
855
- next = ext.wrapResolve.bind(ext, currentNext, event);
856
- }
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);
857
892
  }
858
893
  return next();
859
894
  }
860
895
  async resolveDeps(deps, ctx, dependentAtom) {
861
896
  if (!deps) return {};
862
897
  const result = {};
863
- for (const [key, dep] of Object.entries(deps)) if (isAtom(dep)) {
864
- result[key] = await this.resolve(dep);
865
- if (dependentAtom) {
866
- const depEntry = this.getEntry(dep);
867
- if (depEntry) depEntry.dependents.add(dependentAtom);
868
- }
869
- } else if (isControllerDep(dep)) {
870
- if (dep.watch) {
871
- if (!dependentAtom) throw new Error("controller({ watch: true }) is only supported in atom dependencies");
872
- if (!dep.resolve) throw new Error("controller({ watch: true }) requires resolve: true");
873
- }
874
- const ctrl = this.controller(dep.atom);
875
- if (dep.resolve) await ctrl.resolve();
876
- result[key] = ctrl;
877
- if (dependentAtom) {
878
- const depEntry = this.getEntry(dep.atom);
879
- if (depEntry) depEntry.dependents.add(dependentAtom);
880
- }
881
- if (dep.watch) {
882
- const eq = dep.eq ?? Object.is;
883
- let prev = ctrl.get();
884
- const unsub = this.on("resolved", dep.atom, () => {
885
- const next = ctrl.get();
886
- if (!eq(prev, next)) {
887
- prev = next;
888
- this.scheduleInvalidation(dependentAtom);
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);
907
+ }
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);
889
941
  }
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
942
  }
905
- case "optional":
906
- result[key] = (ctx ? ctx.data.seekTag(tagExecutor.tag) : tagExecutor.tag.find(this.tags)) ?? tagExecutor.tag.defaultValue;
907
- break;
908
- case "all":
909
- result[key] = ctx ? this.collectFromHierarchy(ctx, tagExecutor.tag) : tagExecutor.tag.collect(this.tags);
910
- break;
911
- }
912
- } else if (isResource(dep)) {
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) {
913
965
  if (!ctx) throw new Error("Resource deps require an ExecutionContext");
914
- const resource$1 = dep;
915
966
  const resourceKey = getResourceKey(resource$1);
916
967
  const storeCtx = ctx.parent ?? ctx;
917
968
  if (storeCtx.data.has(resourceKey)) {
918
969
  result[key] = storeCtx.data.get(resourceKey);
919
970
  continue;
920
971
  }
921
- const existingSeek = ctx.data.seek(resourceKey);
922
- if (existingSeek !== void 0 || ctx.data.has(resourceKey)) {
923
- result[key] = existingSeek;
972
+ if (ctx.data.seekHas(resourceKey)) {
973
+ result[key] = ctx.data.seek(resourceKey);
924
974
  continue;
925
975
  }
926
- if (resolvingResources.has(resourceKey)) throw new Error(`Circular resource dependency detected: ${resource$1.name ?? "anonymous"}`);
927
976
  let flights = inflightResources.get(storeCtx.data);
928
977
  if (!flights) {
929
978
  flights = /* @__PURE__ */ new Map();
@@ -934,8 +983,14 @@ var ScopeImpl = class {
934
983
  result[key] = await inflight;
935
984
  continue;
936
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"}`);
937
992
  const resolve = async () => {
938
- resolvingResources.add(resourceKey);
993
+ localResolvingResources.add(resourceKey);
939
994
  try {
940
995
  const resourceDeps = await this.resolveDeps(resource$1.deps, ctx);
941
996
  const event = {
@@ -945,14 +1000,14 @@ var ScopeImpl = class {
945
1000
  };
946
1001
  const doResolve = async () => {
947
1002
  const factory = resource$1.factory;
948
- if (resource$1.deps && Object.keys(resource$1.deps).length > 0) return factory(storeCtx, resourceDeps);
1003
+ if (resource$1.deps) return factory(storeCtx, resourceDeps);
949
1004
  return factory(storeCtx);
950
1005
  };
951
1006
  const value = await this.applyResolveExtensions(event, doResolve);
952
1007
  storeCtx.data.set(resourceKey, value);
953
1008
  return value;
954
1009
  } finally {
955
- resolvingResources.delete(resourceKey);
1010
+ localResolvingResources.delete(resourceKey);
956
1011
  }
957
1012
  };
958
1013
  const promise = resolve();
@@ -976,6 +1031,7 @@ var ScopeImpl = class {
976
1031
  return results;
977
1032
  }
978
1033
  controller(atom$1, options) {
1034
+ if (this.disposed) throw new Error("Scope is disposed");
979
1035
  let ctrl = this.controllers.get(atom$1);
980
1036
  if (!ctrl) {
981
1037
  ctrl = new ControllerImpl(atom$1, this);
@@ -985,7 +1041,7 @@ var ScopeImpl = class {
985
1041
  return ctrl;
986
1042
  }
987
1043
  select(atom$1, selector, options) {
988
- return new SelectHandleImpl(this.controller(atom$1), selector, options?.eq ?? ((a, b) => a === b));
1044
+ return new SelectHandleImpl(this.controller(atom$1), selector, options?.eq ?? Object.is);
989
1045
  }
990
1046
  getFlowPreset(flow$1) {
991
1047
  return this.presets.get(flow$1);
@@ -1029,10 +1085,27 @@ var ScopeImpl = class {
1029
1085
  const previousValue = entry.value;
1030
1086
  const pendingSet = entry.pendingSet;
1031
1087
  entry.pendingSet = void 0;
1032
- for (let i = entry.cleanups.length - 1; i >= 0; i--) {
1033
- const cleanup = entry.cleanups[i];
1034
- if (cleanup) await cleanup();
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;
1035
1105
  }
1106
+ for (let i = entry.cleanups.length - 1; i >= 0; i--) try {
1107
+ await entry.cleanups[i]?.();
1108
+ } catch {}
1036
1109
  entry.cleanups = [];
1037
1110
  entry.state = "resolving";
1038
1111
  entry.value = previousValue;
@@ -1042,16 +1115,11 @@ var ScopeImpl = class {
1042
1115
  this.resolving.delete(atom$1);
1043
1116
  this.emitStateChange("resolving", atom$1);
1044
1117
  this.notifyListeners(atom$1, "resolving");
1045
- if (pendingSet) {
1046
- if ("value" in pendingSet) entry.value = pendingSet.value;
1047
- else entry.value = pendingSet.fn(previousValue);
1048
- entry.state = "resolved";
1049
- entry.hasValue = true;
1050
- this.emitStateChange("resolved", atom$1);
1051
- this.notifyListeners(atom$1, "resolved");
1052
- return;
1118
+ try {
1119
+ await this.resolve(atom$1);
1120
+ } catch (e) {
1121
+ if (!entry.pendingSet && !entry.pendingInvalidate) throw e;
1053
1122
  }
1054
- await this.resolve(atom$1);
1055
1123
  }
1056
1124
  async release(atom$1) {
1057
1125
  const entry = this.cache.get(atom$1);
@@ -1060,14 +1128,33 @@ var ScopeImpl = class {
1060
1128
  clearTimeout(entry.gcScheduled);
1061
1129
  entry.gcScheduled = null;
1062
1130
  }
1063
- for (let i = entry.cleanups.length - 1; i >= 0; i--) {
1064
- const cleanup = entry.cleanups[i];
1065
- if (cleanup) await cleanup();
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
+ }
1066
1142
  }
1067
1143
  this.cache.delete(atom$1);
1068
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
+ }
1069
1149
  }
1070
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;
1071
1158
  for (const ext of this.extensions) if (ext.dispose) await ext.dispose(this);
1072
1159
  for (const entry of this.cache.values()) if (entry.gcScheduled) {
1073
1160
  clearTimeout(entry.gcScheduled);
@@ -1078,8 +1165,14 @@ var ScopeImpl = class {
1078
1165
  }
1079
1166
  async flush() {
1080
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
+ }
1081
1173
  }
1082
1174
  createContext(options) {
1175
+ if (this.disposed) throw new Error("Scope is disposed");
1083
1176
  const ctx = new ExecutionContextImpl(this, options);
1084
1177
  for (const tagged of options?.tags ?? []) ctx.data.set(tagged.key, tagged.value);
1085
1178
  for (const tagged of this.tags) if (!ctx.data.has(tagged.key)) ctx.data.set(tagged.key, tagged.value);
@@ -1173,7 +1266,7 @@ var ExecutionContextImpl = class ExecutionContextImpl {
1173
1266
  const resolvedDeps = await this.scope.resolveDeps(flow$1.deps, this);
1174
1267
  const factory = flow$1.factory;
1175
1268
  const doExec = async () => {
1176
- if (flow$1.deps && Object.keys(flow$1.deps).length > 0) return factory(this, resolvedDeps);
1269
+ if (flow$1.deps) return factory(this, resolvedDeps);
1177
1270
  else return factory(this);
1178
1271
  };
1179
1272
  return this.applyExecExtensions(flow$1, doExec);
@@ -1189,12 +1282,10 @@ var ExecutionContextImpl = class ExecutionContextImpl {
1189
1282
  }
1190
1283
  async applyExecExtensions(target, doExec) {
1191
1284
  let next = doExec;
1192
- for (let i = this.scope.extensions.length - 1; i >= 0; i--) {
1193
- const ext = this.scope.extensions[i];
1194
- if (ext?.wrapExec) {
1195
- const currentNext = next;
1196
- next = ext.wrapExec.bind(ext, currentNext, target, this);
1197
- }
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);
1198
1289
  }
1199
1290
  return next();
1200
1291
  }
@@ -1204,10 +1295,9 @@ var ExecutionContextImpl = class ExecutionContextImpl {
1204
1295
  async close(result = { ok: true }) {
1205
1296
  if (this.closed) return;
1206
1297
  this.closed = true;
1207
- for (let i = this.cleanups.length - 1; i >= 0; i--) {
1208
- const cleanup = this.cleanups[i];
1209
- if (cleanup) await cleanup(result);
1210
- }
1298
+ for (let i = this.cleanups.length - 1; i >= 0; i--) try {
1299
+ await this.cleanups[i]?.(result);
1300
+ } catch {}
1211
1301
  }
1212
1302
  };
1213
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;
@@ -236,11 +241,23 @@ declare namespace Lite {
236
241
  interface ControllerOptions {
237
242
  resolve?: boolean;
238
243
  }
239
- interface ControllerDepOptions<T> {
240
- resolve?: boolean;
241
- watch?: boolean;
242
- eq?: (a: T, b: T) => boolean;
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
+ };
244
261
  interface Typed<T> {
245
262
  readonly [typedSymbol]: true;
246
263
  }