@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 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.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;
@@ -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 `Object.is` by default). Replaces
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
- return {
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 resolvingResources = /* @__PURE__ */ new Set();
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.chainPromise = new Promise((resolve, reject) => {
598
- queueMicrotask(() => {
599
- this.processInvalidationChain().then(resolve).catch(reject);
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
- const presetValue = this.presets.get(atom$1);
766
- if (presetValue !== void 0) {
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--) await entry.cleanups[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 && Object.keys(atom$1.deps).length > 0) return factory(ctx, resolvedDeps);
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.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
- }
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
- 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);
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
- 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)) {
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
- const existingSeek = ctx.data.seek(resourceKey);
922
- if (existingSeek !== void 0 || ctx.data.has(resourceKey)) {
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
- resolvingResources.add(resourceKey);
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 && Object.keys(resource$1.deps).length > 0) return factory(storeCtx, resourceDeps);
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
- resolvingResources.delete(resourceKey);
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 ?? ((a, b) => a === b));
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
- for (let i = entry.cleanups.length - 1; i >= 0; i--) {
1033
- const cleanup = entry.cleanups[i];
1034
- if (cleanup) await cleanup();
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
- 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;
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
- const cleanup = entry.cleanups[i];
1065
- if (cleanup) await cleanup();
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 && Object.keys(flow$1.deps).length > 0) return factory(this, resolvedDeps);
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.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
- }
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
- const cleanup = this.cleanups[i];
1209
- if (cleanup) await cleanup(result);
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;