@shivaedev/effect-react 0.10.0 → 0.10.1

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,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.10.1 - 2026-10-19
4
+
5
+ ### Fixed
6
+
7
+ - Wait for an active query loader before a newly mounted `useLoaderQuery` screen renders, so links to cached routes after invalidation show the changed data on their first render. Already mounted screens keep their data during refreshes.
8
+
3
9
  ## 0.10.0 - 2026-10-19
4
10
 
5
11
  ### Added
package/README.md CHANGED
@@ -277,7 +277,7 @@ export function App() {
277
277
 
278
278
  `useLoaderQuery(atom, route.useLoaderData())` subscribes to the same atom and exposes `data` as its success type, with no `Option` or Initial result. Refetches keep the last value visible. For a loader-backed editor, pass the loader value as `useEditor({ ...config, loaded: route.useLoaderData() })`; its form is defined on the first render and `editor.query.data` is the row directly.
279
279
 
280
- `SessionBoundary` gives idle atoms the default 400 ms window used by atom-react’s default registry. `loadQuery` also mounts its atom until a component commits its `useQuery` or `useLoaderQuery` subscription, or the loader’s abort signal ends its route ownership. No per-query `timeToLive` is needed. Leaving an unread route releases that mount; a DOM story verifies the atom is gone after the idle window.
280
+ `SessionBoundary` gives idle atoms the default 400 ms window used by atom-react’s default registry. `loadQuery` also mounts its atom until a component commits its `useQuery` or `useLoaderQuery` subscription, or the loader’s abort signal ends its route ownership. No per-query `timeToLive` is needed. Leaving an unread route releases that mount; a DOM story verifies the atom is gone after the idle window. A new `useLoaderQuery` reader suspends until its active `loadQuery` completes, so revisiting a cached route after invalidation renders the changed data on its first frame. Already mounted readers keep their data during refreshes.
281
281
 
282
282
  `loadQuery` waits through a native query refresh instead of returning its retained success. Typed failures are thrown directly, so the route can inspect `_tag` rather than an Effect fiber wrapper. Defects remain failed Promises. TanStack's loader `abortController` cancels the waiting subscription and rejects with `name: "AbortError"`; other readers of the same query can still complete. The application owns the registry's lifetime and disposes it when its client or session ends. Use the same registry for router context and `RegistryContext.Provider`.
283
283
 
@@ -1 +1 @@
1
- {"version":3,"file":"useLoaderQuery.d.ts","sourceRoot":"","sources":["../../src/client/useLoaderQuery.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,WAAW,MAAM,+BAA+B,CAAC;AAClE,OAAO,KAAK,KAAK,IAAI,MAAM,wBAAwB,CAAC;AAEpD,OAAO,EAAE,KAAK,UAAU,EAAY,MAAM,kBAAkB,CAAC;AAE7D,MAAM,WAAW,gBAAgB,CAAC,CAAC,EAAE,CAAC,CAAE,SAAQ,IAAI,CAAC,UAAU,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,MAAM,GAAG,QAAQ,CAAC;IACxF,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC;CACjB;AAED,wBAAgB,cAAc,CAAC,CAAC,EAAE,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,WAAW,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,GAAG,gBAAgB,CAAC,CAAC,EAAE,CAAC,CAAC,CAE/H"}
1
+ {"version":3,"file":"useLoaderQuery.d.ts","sourceRoot":"","sources":["../../src/client/useLoaderQuery.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,WAAW,MAAM,+BAA+B,CAAC;AAClE,OAAO,KAAK,KAAK,IAAI,MAAM,wBAAwB,CAAC;AAIpD,OAAO,EAAE,KAAK,UAAU,EAAY,MAAM,kBAAkB,CAAC;AAE7D,MAAM,WAAW,gBAAgB,CAAC,CAAC,EAAE,CAAC,CAAE,SAAQ,IAAI,CAAC,UAAU,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,MAAM,GAAG,QAAQ,CAAC;IACxF,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC;CACjB;AAED,wBAAgB,cAAc,CAAC,CAAC,EAAE,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,WAAW,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,GAAG,gBAAgB,CAAC,CAAC,EAAE,CAAC,CAAC,CAG/H"}
@@ -1,6 +1,10 @@
1
+ import { RegistryContext } from "@effect/atom-react";
2
+ import { useContext } from "react";
1
3
  import { loadedQuery } from "#internal/loadedQuery.ts";
4
+ import { useLoaderHandoff } from "#internal/useLoaderHandoff.ts";
2
5
  import { useQuery } from "./resultState.js";
3
6
  export function useLoaderQuery(atom, loaded) {
7
+ useLoaderHandoff(useContext(RegistryContext), atom);
4
8
  return loadedQuery(useQuery(atom), loaded);
5
9
  }
6
10
  //# sourceMappingURL=useLoaderQuery.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"useLoaderQuery.js","sourceRoot":"","sources":["../../src/client/useLoaderQuery.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AACvD,OAAO,EAAmB,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAM7D,MAAM,UAAU,cAAc,CAAO,IAA8C,EAAE,MAAkB;IACtG,OAAO,WAAW,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC,CAAC;AAC5C,CAAC"}
1
+ {"version":3,"file":"useLoaderQuery.js","sourceRoot":"","sources":["../../src/client/useLoaderQuery.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAGrD,OAAO,EAAE,UAAU,EAAE,MAAM,OAAO,CAAC;AACnC,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AACvD,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AACjE,OAAO,EAAmB,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAM7D,MAAM,UAAU,cAAc,CAAO,IAA8C,EAAE,MAAkB;IACtG,gBAAgB,CAAC,UAAU,CAAC,eAAe,CAAC,EAAE,IAAI,CAAC,CAAC;IACpD,OAAO,WAAW,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC,CAAC;AAC5C,CAAC"}
@@ -1,5 +1,9 @@
1
1
  import * as Atom from "effect/reactivity/Atom";
2
2
  import type * as AtomRegistry from "effect/reactivity/AtomRegistry";
3
- export declare function keepLoaderQuery(registry: AtomRegistry.AtomRegistry, atom: Atom.Atom<unknown>, signal: AbortSignal): () => void;
3
+ export declare function keepLoaderQuery(registry: AtomRegistry.AtomRegistry, atom: Atom.Atom<unknown>, signal: AbortSignal): {
4
+ complete: () => void;
5
+ release: () => void;
6
+ };
4
7
  export declare function acceptLoaderQuery(registry: AtomRegistry.AtomRegistry, atom: Atom.Atom<unknown>): void;
8
+ export declare function pendingLoaderQuery(registry: AtomRegistry.AtomRegistry, atom: Atom.Atom<unknown>): Promise<void> | undefined;
5
9
  //# sourceMappingURL=queryHandoff.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"queryHandoff.d.ts","sourceRoot":"","sources":["../../src/internal/queryHandoff.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,wBAAwB,CAAC;AAC/C,OAAO,KAAK,KAAK,YAAY,MAAM,gCAAgC,CAAC;AAIpE,wBAAgB,eAAe,CAAC,QAAQ,EAAE,YAAY,CAAC,YAAY,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,MAAM,EAAE,WAAW,GAAG,MAAM,IAAI,CA0B9H;AAED,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,YAAY,CAAC,YAAY,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,IAAI,CAIrG"}
1
+ {"version":3,"file":"queryHandoff.d.ts","sourceRoot":"","sources":["../../src/internal/queryHandoff.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,wBAAwB,CAAC;AAC/C,OAAO,KAAK,KAAK,YAAY,MAAM,gCAAgC,CAAC;AAUpE,wBAAgB,eAAe,CAAC,QAAQ,EAAE,YAAY,CAAC,YAAY,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,MAAM,EAAE,WAAW;;;EAiCjH;AAED,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,YAAY,CAAC,YAAY,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,IAAI,CAIrG;AAED,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,YAAY,CAAC,YAAY,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,SAAS,CAO3H"}
@@ -14,22 +14,37 @@ export function keepLoaderQuery(registry, atom, signal) {
14
14
  registry.refresh(atom);
15
15
  }
16
16
  const unmount = registry.mount(atom);
17
+ const ready = Promise.withResolvers();
18
+ const handoff = { pending: true, ready: ready.promise, release };
19
+ function complete() {
20
+ handoff.pending = false;
21
+ ready.resolve();
22
+ }
17
23
  function release() {
18
- if (readers.delete(release)) {
24
+ if (readers.delete(handoff)) {
25
+ complete();
19
26
  signal.removeEventListener("abort", release);
20
27
  unmount();
21
28
  }
22
29
  }
23
- readers.add(release);
30
+ readers.add(handoff);
24
31
  signal.addEventListener("abort", release, { once: true });
25
32
  if (signal.aborted) {
26
33
  release();
27
34
  }
28
- return release;
35
+ return { complete, release };
29
36
  }
30
37
  export function acceptLoaderQuery(registry, atom) {
31
- for (const release of handoffs.get(registry)?.get(atom) ?? []) {
32
- release();
38
+ for (const handoff of handoffs.get(registry)?.get(atom) ?? []) {
39
+ handoff.release();
40
+ }
41
+ }
42
+ export function pendingLoaderQuery(registry, atom) {
43
+ for (const handoff of handoffs.get(registry)?.get(atom) ?? []) {
44
+ if (handoff.pending) {
45
+ return handoff.ready;
46
+ }
33
47
  }
48
+ return undefined;
34
49
  }
35
50
  //# sourceMappingURL=queryHandoff.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"queryHandoff.js","sourceRoot":"","sources":["../../src/internal/queryHandoff.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,wBAAwB,CAAC;AAG/C,MAAM,QAAQ,GAAG,IAAI,OAAO,EAA2E,CAAC;AAExG,MAAM,UAAU,eAAe,CAAC,QAAmC,EAAE,IAAwB,EAAE,MAAmB;IACjH,IAAI,KAAK,GAAG,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IACnC,IAAI,CAAC,KAAK,EAAE,CAAC;QACZ,KAAK,GAAG,IAAI,OAAO,EAAE,CAAC;QACtB,QAAQ,CAAC,GAAG,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;IAC/B,CAAC;IACD,MAAM,OAAO,GAAG,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,IAAI,GAAG,EAAc,CAAC;IACzD,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;IACzB,MAAM,GAAG,GAAG,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,kBAAkB,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC;IACjF,IAAI,IAAI,CAAC,kBAAkB,IAAI,CAAC,QAAQ,CAAC,QAAQ,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;QAC9D,yFAAyF;QACzF,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IACxB,CAAC;IACD,MAAM,OAAO,GAAG,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IACrC,SAAS,OAAO;QACf,IAAI,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC;YAC7B,MAAM,CAAC,mBAAmB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;YAC7C,OAAO,EAAE,CAAC;QACX,CAAC;IACF,CAAC;IACD,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;IACrB,MAAM,CAAC,gBAAgB,CAAC,OAAO,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;IAC1D,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;QACpB,OAAO,EAAE,CAAC;IACX,CAAC;IACD,OAAO,OAAO,CAAC;AAChB,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,QAAmC,EAAE,IAAwB;IAC9F,KAAK,MAAM,OAAO,IAAI,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,EAAE,CAAC;QAC/D,OAAO,EAAE,CAAC;IACX,CAAC;AACF,CAAC"}
1
+ {"version":3,"file":"queryHandoff.js","sourceRoot":"","sources":["../../src/internal/queryHandoff.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,wBAAwB,CAAC;AAS/C,MAAM,QAAQ,GAAG,IAAI,OAAO,EAAwE,CAAC;AAErG,MAAM,UAAU,eAAe,CAAC,QAAmC,EAAE,IAAwB,EAAE,MAAmB;IACjH,IAAI,KAAK,GAAG,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IACnC,IAAI,CAAC,KAAK,EAAE,CAAC;QACZ,KAAK,GAAG,IAAI,OAAO,EAAE,CAAC;QACtB,QAAQ,CAAC,GAAG,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;IAC/B,CAAC;IACD,MAAM,OAAO,GAAG,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,IAAI,GAAG,EAAW,CAAC;IACtD,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;IACzB,MAAM,GAAG,GAAG,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,kBAAkB,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC;IACjF,IAAI,IAAI,CAAC,kBAAkB,IAAI,CAAC,QAAQ,CAAC,QAAQ,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;QAC9D,yFAAyF;QACzF,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IACxB,CAAC;IACD,MAAM,OAAO,GAAG,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IACrC,MAAM,KAAK,GAAG,OAAO,CAAC,aAAa,EAAQ,CAAC;IAC5C,MAAM,OAAO,GAAY,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,CAAC,OAAO,EAAE,OAAO,EAAE,CAAC;IAC1E,SAAS,QAAQ;QAChB,OAAO,CAAC,OAAO,GAAG,KAAK,CAAC;QACxB,KAAK,CAAC,OAAO,EAAE,CAAC;IACjB,CAAC;IACD,SAAS,OAAO;QACf,IAAI,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC;YAC7B,QAAQ,EAAE,CAAC;YACX,MAAM,CAAC,mBAAmB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;YAC7C,OAAO,EAAE,CAAC;QACX,CAAC;IACF,CAAC;IACD,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;IACrB,MAAM,CAAC,gBAAgB,CAAC,OAAO,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;IAC1D,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;QACpB,OAAO,EAAE,CAAC;IACX,CAAC;IACD,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC;AAC9B,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,QAAmC,EAAE,IAAwB;IAC9F,KAAK,MAAM,OAAO,IAAI,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,EAAE,CAAC;QAC/D,OAAO,CAAC,OAAO,EAAE,CAAC;IACnB,CAAC;AACF,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,QAAmC,EAAE,IAAwB;IAC/F,KAAK,MAAM,OAAO,IAAI,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,EAAE,CAAC;QAC/D,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC;YACrB,OAAO,OAAO,CAAC,KAAK,CAAC;QACtB,CAAC;IACF,CAAC;IACD,OAAO,SAAS,CAAC;AAClB,CAAC"}
@@ -0,0 +1,4 @@
1
+ import type * as Atom from "effect/reactivity/Atom";
2
+ import type * as AtomRegistry from "effect/reactivity/AtomRegistry";
3
+ export declare function useLoaderHandoff(registry: AtomRegistry.AtomRegistry, atom: Atom.Atom<unknown>): void;
4
+ //# sourceMappingURL=useLoaderHandoff.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useLoaderHandoff.d.ts","sourceRoot":"","sources":["../../src/internal/useLoaderHandoff.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,IAAI,MAAM,wBAAwB,CAAC;AACpD,OAAO,KAAK,KAAK,YAAY,MAAM,gCAAgC,CAAC;AAIpE,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,YAAY,CAAC,YAAY,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,IAAI,CAcpG"}
@@ -0,0 +1,18 @@
1
+ import { useEffect, useRef } from "react";
2
+ import { pendingLoaderQuery } from "./queryHandoff.js";
3
+ export function useLoaderHandoff(registry, atom) {
4
+ const committedRef = useRef(undefined);
5
+ useEffect(() => {
6
+ committedRef.current = { atom, registry };
7
+ return () => {
8
+ committedRef.current = undefined;
9
+ };
10
+ }, [registry, atom]);
11
+ if (committedRef.current?.registry !== registry || committedRef.current.atom !== atom) {
12
+ const pending = pendingLoaderQuery(registry, atom);
13
+ if (pending !== undefined) {
14
+ throw pending;
15
+ }
16
+ }
17
+ }
18
+ //# sourceMappingURL=useLoaderHandoff.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useLoaderHandoff.js","sourceRoot":"","sources":["../../src/internal/useLoaderHandoff.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,OAAO,CAAC;AAC1C,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAEvD,MAAM,UAAU,gBAAgB,CAAC,QAAmC,EAAE,IAAwB;IAC7F,MAAM,YAAY,GAAG,MAAM,CAAkG,SAAS,CAAC,CAAC;IACxI,SAAS,CAAC,GAAG,EAAE;QACd,YAAY,CAAC,OAAO,GAAG,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC;QAC1C,OAAO,GAAG,EAAE;YACX,YAAY,CAAC,OAAO,GAAG,SAAS,CAAC;QAClC,CAAC,CAAC;IACH,CAAC,EAAE,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC,CAAC;IACrB,IAAI,YAAY,CAAC,OAAO,EAAE,QAAQ,KAAK,QAAQ,IAAI,YAAY,CAAC,OAAO,CAAC,IAAI,KAAK,IAAI,EAAE,CAAC;QACvF,MAAM,OAAO,GAAG,kBAAkB,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;QACnD,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;YAC3B,MAAM,OAAO,CAAC;QACf,CAAC;IACF,CAAC;AACF,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"loadQuery.d.ts","sourceRoot":"","sources":["../src/loadQuery.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAE9D,OAAO,KAAK,KAAK,WAAW,MAAM,+BAA+B,CAAC;AAClE,OAAO,KAAK,KAAK,IAAI,MAAM,wBAAwB,CAAC;AACpD,OAAO,KAAK,YAAY,MAAM,gCAAgC,CAAC;AAI/D,wBAAsB,SAAS,CAAC,CAAC,EAAE,CAAC,EACnC,QAAQ,EAAE,YAAY,CAAC,YAAY,EACnC,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,WAAW,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAC9C,EAAE,eAAe,EAAE,EAAE,IAAI,CAAC,eAAe,EAAE,iBAAiB,CAAC,GAC3D,OAAO,CAAC,CAAC,CAAC,CAmBZ"}
1
+ {"version":3,"file":"loadQuery.d.ts","sourceRoot":"","sources":["../src/loadQuery.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAE9D,OAAO,KAAK,KAAK,WAAW,MAAM,+BAA+B,CAAC;AAClE,OAAO,KAAK,KAAK,IAAI,MAAM,wBAAwB,CAAC;AACpD,OAAO,KAAK,YAAY,MAAM,gCAAgC,CAAC;AAI/D,wBAAsB,SAAS,CAAC,CAAC,EAAE,CAAC,EACnC,QAAQ,EAAE,YAAY,CAAC,YAAY,EACnC,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,WAAW,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAC9C,EAAE,eAAe,EAAE,EAAE,IAAI,CAAC,eAAe,EAAE,iBAAiB,CAAC,GAC3D,OAAO,CAAC,CAAC,CAAC,CAqBZ"}
package/dist/loadQuery.js CHANGED
@@ -4,7 +4,7 @@ import { keepLoaderQuery } from "#internal/queryHandoff.ts";
4
4
  import { recheckUnauthorized } from "#internal/sessionRecheck.ts";
5
5
  export async function loadQuery(registry, atom, { abortController }) {
6
6
  const { signal } = abortController;
7
- const release = keepLoaderQuery(registry, atom, signal);
7
+ const handoff = keepLoaderQuery(registry, atom, signal);
8
8
  try {
9
9
  const result = await Effect.runPromise(Effect.result(AtomRegistry.getResult(registry, atom, { suspendOnWaiting: true })), { signal });
10
10
  if (result._tag === "Failure") {
@@ -14,7 +14,7 @@ export async function loadQuery(registry, atom, { abortController }) {
14
14
  return result.success;
15
15
  }
16
16
  catch (error) {
17
- release();
17
+ handoff.release();
18
18
  if (signal.aborted) {
19
19
  const canceled = new Error("Query loading canceled", { cause: error });
20
20
  canceled.name = "AbortError";
@@ -22,5 +22,8 @@ export async function loadQuery(registry, atom, { abortController }) {
22
22
  }
23
23
  throw error;
24
24
  }
25
+ finally {
26
+ handoff.complete();
27
+ }
25
28
  }
26
29
  //# sourceMappingURL=loadQuery.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"loadQuery.js","sourceRoot":"","sources":["../src/loadQuery.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAGhC,OAAO,KAAK,YAAY,MAAM,gCAAgC,CAAC;AAC/D,OAAO,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAC5D,OAAO,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AAElE,MAAM,CAAC,KAAK,UAAU,SAAS,CAC9B,QAAmC,EACnC,IAA8C,EAC9C,EAAE,eAAe,EAA4C;IAE7D,MAAM,EAAE,MAAM,EAAE,GAAG,eAAe,CAAC;IACnC,MAAM,OAAO,GAAG,eAAe,CAAC,QAAQ,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC;IACxD,IAAI,CAAC;QACJ,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,UAAU,CAAC,MAAM,CAAC,MAAM,CAAC,YAAY,CAAC,SAAS,CAAC,QAAQ,EAAE,IAAI,EAAE,EAAE,gBAAgB,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC;QACtI,IAAI,MAAM,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;YAC/B,MAAM,mBAAmB,CAAC,QAAQ,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC;YACpD,MAAM,MAAM,CAAC,OAAO,CAAC;QACtB,CAAC;QACD,OAAO,MAAM,CAAC,OAAO,CAAC;IACvB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QAChB,OAAO,EAAE,CAAC;QACV,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;YACpB,MAAM,QAAQ,GAAG,IAAI,KAAK,CAAC,wBAAwB,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC;YACvE,QAAQ,CAAC,IAAI,GAAG,YAAY,CAAC;YAC7B,MAAM,QAAQ,CAAC;QAChB,CAAC;QACD,MAAM,KAAK,CAAC;IACb,CAAC;AACF,CAAC"}
1
+ {"version":3,"file":"loadQuery.js","sourceRoot":"","sources":["../src/loadQuery.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAGhC,OAAO,KAAK,YAAY,MAAM,gCAAgC,CAAC;AAC/D,OAAO,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAC5D,OAAO,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AAElE,MAAM,CAAC,KAAK,UAAU,SAAS,CAC9B,QAAmC,EACnC,IAA8C,EAC9C,EAAE,eAAe,EAA4C;IAE7D,MAAM,EAAE,MAAM,EAAE,GAAG,eAAe,CAAC;IACnC,MAAM,OAAO,GAAG,eAAe,CAAC,QAAQ,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC;IACxD,IAAI,CAAC;QACJ,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,UAAU,CAAC,MAAM,CAAC,MAAM,CAAC,YAAY,CAAC,SAAS,CAAC,QAAQ,EAAE,IAAI,EAAE,EAAE,gBAAgB,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC;QACtI,IAAI,MAAM,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;YAC/B,MAAM,mBAAmB,CAAC,QAAQ,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC;YACpD,MAAM,MAAM,CAAC,OAAO,CAAC;QACtB,CAAC;QACD,OAAO,MAAM,CAAC,OAAO,CAAC;IACvB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QAChB,OAAO,CAAC,OAAO,EAAE,CAAC;QAClB,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;YACpB,MAAM,QAAQ,GAAG,IAAI,KAAK,CAAC,wBAAwB,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC;YACvE,QAAQ,CAAC,IAAI,GAAG,YAAY,CAAC;YAC7B,MAAM,QAAQ,CAAC;QAChB,CAAC;QACD,MAAM,KAAK,CAAC;IACb,CAAC;YAAS,CAAC;QACV,OAAO,CAAC,QAAQ,EAAE,CAAC;IACpB,CAAC;AACF,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shivaedev/effect-react",
3
- "version": "0.10.0",
3
+ "version": "0.10.1",
4
4
  "description": "React query, action, editor and session hooks for native Effect atoms",
5
5
  "homepage": "https://github.com/ShivaeDev/platform/tree/main/packages/effect-react#readme",
6
6
  "bugs": {
@@ -58,8 +58,8 @@
58
58
  "@shivaedev/effect-test-sql": "0.7.1",
59
59
  "@shivaedev/local-postgres": "0.4.2",
60
60
  "@shivaedev/platform": "0.20.1",
61
- "@shivaedev/quality": "0.28.2",
62
- "@shivaedev/test-story": "0.13.0",
61
+ "@shivaedev/quality": "0.29.0",
62
+ "@shivaedev/test-story": "0.14.0",
63
63
  "@tanstack/react-router": "1.170.42",
64
64
  "@types/node": "24.19.2",
65
65
  "@types/react": "19.3.0",
@@ -1,6 +1,9 @@
1
+ import { RegistryContext } from "@effect/atom-react";
1
2
  import type * as AsyncResult from "effect/reactivity/AsyncResult";
2
3
  import type * as Atom from "effect/reactivity/Atom";
4
+ import { useContext } from "react";
3
5
  import { loadedQuery } from "#internal/loadedQuery.ts";
6
+ import { useLoaderHandoff } from "#internal/useLoaderHandoff.ts";
4
7
  import { type QueryState, useQuery } from "./resultState.ts";
5
8
 
6
9
  export interface LoadedQueryState<A, E> extends Omit<QueryState<A, E>, "data" | "result"> {
@@ -8,5 +11,6 @@ export interface LoadedQueryState<A, E> extends Omit<QueryState<A, E>, "data" |
8
11
  }
9
12
 
10
13
  export function useLoaderQuery<A, E>(atom: Atom.Atom<AsyncResult.AsyncResult<A, E>>, loaded: NoInfer<A>): LoadedQueryState<A, E> {
14
+ useLoaderHandoff(useContext(RegistryContext), atom);
11
15
  return loadedQuery(useQuery(atom), loaded);
12
16
  }
@@ -1,15 +1,21 @@
1
1
  import * as Atom from "effect/reactivity/Atom";
2
2
  import type * as AtomRegistry from "effect/reactivity/AtomRegistry";
3
3
 
4
- const handoffs = new WeakMap<AtomRegistry.AtomRegistry, WeakMap<Atom.Atom<unknown>, Set<() => void>>>();
4
+ interface Handoff {
5
+ pending: boolean;
6
+ readonly ready: Promise<void>;
7
+ readonly release: () => void;
8
+ }
9
+
10
+ const handoffs = new WeakMap<AtomRegistry.AtomRegistry, WeakMap<Atom.Atom<unknown>, Set<Handoff>>>();
5
11
 
6
- export function keepLoaderQuery(registry: AtomRegistry.AtomRegistry, atom: Atom.Atom<unknown>, signal: AbortSignal): () => void {
12
+ export function keepLoaderQuery(registry: AtomRegistry.AtomRegistry, atom: Atom.Atom<unknown>, signal: AbortSignal) {
7
13
  let atoms = handoffs.get(registry);
8
14
  if (!atoms) {
9
15
  atoms = new WeakMap();
10
16
  handoffs.set(registry, atoms);
11
17
  }
12
- const readers = atoms.get(atom) ?? new Set<() => void>();
18
+ const readers = atoms.get(atom) ?? new Set<Handoff>();
13
19
  atoms.set(atom, readers);
14
20
  const key = Atom.isSerializable(atom) ? atom[Atom.SerializableTypeId].key : atom;
15
21
  if (atom.initialValueTarget && !registry.getNodes().has(key)) {
@@ -17,22 +23,38 @@ export function keepLoaderQuery(registry: AtomRegistry.AtomRegistry, atom: Atom.
17
23
  registry.refresh(atom);
18
24
  }
19
25
  const unmount = registry.mount(atom);
26
+ const ready = Promise.withResolvers<void>();
27
+ const handoff: Handoff = { pending: true, ready: ready.promise, release };
28
+ function complete() {
29
+ handoff.pending = false;
30
+ ready.resolve();
31
+ }
20
32
  function release() {
21
- if (readers.delete(release)) {
33
+ if (readers.delete(handoff)) {
34
+ complete();
22
35
  signal.removeEventListener("abort", release);
23
36
  unmount();
24
37
  }
25
38
  }
26
- readers.add(release);
39
+ readers.add(handoff);
27
40
  signal.addEventListener("abort", release, { once: true });
28
41
  if (signal.aborted) {
29
42
  release();
30
43
  }
31
- return release;
44
+ return { complete, release };
32
45
  }
33
46
 
34
47
  export function acceptLoaderQuery(registry: AtomRegistry.AtomRegistry, atom: Atom.Atom<unknown>): void {
35
- for (const release of handoffs.get(registry)?.get(atom) ?? []) {
36
- release();
48
+ for (const handoff of handoffs.get(registry)?.get(atom) ?? []) {
49
+ handoff.release();
50
+ }
51
+ }
52
+
53
+ export function pendingLoaderQuery(registry: AtomRegistry.AtomRegistry, atom: Atom.Atom<unknown>): Promise<void> | undefined {
54
+ for (const handoff of handoffs.get(registry)?.get(atom) ?? []) {
55
+ if (handoff.pending) {
56
+ return handoff.ready;
57
+ }
37
58
  }
59
+ return undefined;
38
60
  }
@@ -0,0 +1,20 @@
1
+ import type * as Atom from "effect/reactivity/Atom";
2
+ import type * as AtomRegistry from "effect/reactivity/AtomRegistry";
3
+ import { useEffect, useRef } from "react";
4
+ import { pendingLoaderQuery } from "./queryHandoff.ts";
5
+
6
+ export function useLoaderHandoff(registry: AtomRegistry.AtomRegistry, atom: Atom.Atom<unknown>): void {
7
+ const committedRef = useRef<{ readonly registry: AtomRegistry.AtomRegistry; readonly atom: Atom.Atom<unknown> } | undefined>(undefined);
8
+ useEffect(() => {
9
+ committedRef.current = { atom, registry };
10
+ return () => {
11
+ committedRef.current = undefined;
12
+ };
13
+ }, [registry, atom]);
14
+ if (committedRef.current?.registry !== registry || committedRef.current.atom !== atom) {
15
+ const pending = pendingLoaderQuery(registry, atom);
16
+ if (pending !== undefined) {
17
+ throw pending;
18
+ }
19
+ }
20
+ }
package/src/loadQuery.ts CHANGED
@@ -12,7 +12,7 @@ export async function loadQuery<A, E>(
12
12
  { abortController }: Pick<LoaderFnContext, "abortController">,
13
13
  ): Promise<A> {
14
14
  const { signal } = abortController;
15
- const release = keepLoaderQuery(registry, atom, signal);
15
+ const handoff = keepLoaderQuery(registry, atom, signal);
16
16
  try {
17
17
  const result = await Effect.runPromise(Effect.result(AtomRegistry.getResult(registry, atom, { suspendOnWaiting: true })), { signal });
18
18
  if (result._tag === "Failure") {
@@ -21,12 +21,14 @@ export async function loadQuery<A, E>(
21
21
  }
22
22
  return result.success;
23
23
  } catch (error) {
24
- release();
24
+ handoff.release();
25
25
  if (signal.aborted) {
26
26
  const canceled = new Error("Query loading canceled", { cause: error });
27
27
  canceled.name = "AbortError";
28
28
  throw canceled;
29
29
  }
30
30
  throw error;
31
+ } finally {
32
+ handoff.complete();
31
33
  }
32
34
  }