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