raf-effect-atom 0.0.0 → 0.2.0

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 ADDED
@@ -0,0 +1,122 @@
1
+ # Changelog
2
+
3
+ ## [0.2.0](https://github.com/KATT/raf-effect-atom/compare/raf-effect-atom%400.1.0...raf-effect-atom%400.2.0) (2026-10-23)
4
+
5
+ ### ⚠ BREAKING CHANGES
6
+
7
+ * **raf-effect-atom:** `dehydrate()` from `raf-effect-atom/next` returns a
8
+
9
+ ### Features
10
+
11
+ * **raf-effect-atom:** seed each request registry, async seeds included ([62b6180](https://github.com/KATT/raf-effect-atom/commit/62b618098df87e711ddb4c1d633e191ba3353438))
12
+
13
+ ## 0.1.0 (2026-10-23)
14
+
15
+ ### ⚠ BREAKING CHANGES
16
+
17
+ - **raf-effect-atom:** `useAtomResult(atom, claims)` is gone. Read
18
+ `useAtomResult(boundary.wrap(atom))` instead. `effect-atom-boundary` is no
19
+ longer a peer dependency.
20
+
21
+ effect-atom-boundary stays a devDependency, so the hook's tests still read
22
+ through a real boundary's wrap (claimed tag thrown to the boundary, others
23
+ returned). The type test now checks that a derived atom with an
24
+ initialValueTarget is read with its own type.
25
+
26
+ The request waterfalls docs sample, written against the claims overload,
27
+ reads through `useIssueBoundary().wrap(atom)` too.
28
+
29
+ - **raf-effect-atom:** follow initialValueTarget in useAtomResult and throw defects
30
+ - **raf-effect-atom:** the loader verbs take a source and its arguments and
31
+ return a handle, and `useAtomResult` reads a handle through a source.
32
+
33
+ - `prefetch(issue, id)` returns a `Handle<typeof issue>`; `ensure` and
34
+ `fetch` return a `LoadedHandle`, which adds `loaded: true`. A source is
35
+ an atom family, such as an AtomRpc query, or one AsyncResult atom, whose
36
+ handle has no arguments. The same holds for `Loader.make(registry)`, the
37
+ router contexts' `atoms` and `raf-effect-atom/next`'s verbs.
38
+ - A handle is plain data, `{ args, key }`, so loaders return it in their
39
+ data and server components pass it to client components as a prop. Its
40
+ type names its source through a brand on the key, `HandleKey<S>`, so
41
+ one family's handle doesn't fit another's prop. The brand is on the key
42
+ because React Router's `Serialize` type drops an optional phantom field
43
+ from loader data and keeps branded strings; an atom source's args are a
44
+ mutable `[]` because `Serialize<readonly []>` is `readonly never[]`.
45
+ - `Loader.result(handle)` gives loader code the settled result of an
46
+ `ensure` or `fetch`. The result can't ride on the handle: AsyncResult and
47
+ Schema classes don't survive turbo-stream, seroval, JSON or server
48
+ component props. It's kept in a WeakMap keyed by the handle object, and
49
+ a copy of the handle throws.
50
+ - `useAtomResult(source, handle)` reads `source(...handle.args)`. Its
51
+ handle parameter widens the source's error to `unknown`, so a boundary's
52
+ `wrap(issue)` reads `issue`'s handles with the per-tag narrowing and the
53
+ `Success<A, never>` typing kept. In development it throws when the atom
54
+ it hydrates has a serialization key other than the handle's.
55
+ - `useWaterfall(source, ...args)` returns a handle, the one way to make a
56
+ handle during render.
57
+
58
+ The waterfall warning names the new call shapes.
59
+
60
+ Migrated with it, so no commit has a red tree:
61
+ - The three example apps: loaders return the handles of the atoms they
62
+ start, and pages pass them down (TanStack Start through
63
+ `Route.useLoaderData()`, React Router through `loaderData.handles` next
64
+ to the dehydrated atoms, Next.js as server component props). Components
65
+ take a handle instead of an id. "Load more" keeps the next page's handle
66
+ in state, made by `Loader.make(registry).prefetch(source, ...args)` in
67
+ the click handler.
68
+ - www: the docs route, demo embeds and the loader-verbs, waterfalls,
69
+ hydration, boundary-chain and every-tag-caught demos; the TanStack Start, Next.js and
70
+ React Router doc samples, the error-boundaries and every-tag-caught
71
+ samples included; the home page's steps; and the pages that
72
+ show the verbs and reads (loader verbs gains a Handles section and
73
+ `Loader.result`, reading atoms explains handles as props, request
74
+ waterfalls, coding agents, shared boundaries, error boundaries, loader
75
+ failures).
76
+ `www/src/use-loaded.ts` is gone: a wrapper generic over a family can't
77
+ satisfy `useAtomResult`'s `Readable` constraint, and
78
+ `useAtomResult(source, handle).value` reads as well. The "Fetches in a
79
+ component" mistake on the home page is now caught by the compiler.
80
+ - The package READMEs, the boundary's JSDoc example, and the decision in
81
+ `.agent-docs/decisions/atom-handles.md`.
82
+
83
+ ### Features
84
+
85
+ - **raf-effect-atom:** add the next subpath ([92cacd8](https://github.com/KATT/raf-effect-atom/commit/92cacd86cb6d6e1784d9a07f71bd9fc3ec5ca0da))
86
+ - **raf-effect-atom:** add the react-router subpath ([09b65dc](https://github.com/KATT/raf-effect-atom/commit/09b65dcbc6cfde2ae1ba2bb89e580746e9512f8b))
87
+ - **raf-effect-atom:** add the tanstack-router subpath ([0189c83](https://github.com/KATT/raf-effect-atom/commit/0189c834cd46fc05ed866859e7527b3bd2ac47df))
88
+ - **raf-effect-atom:** drop the claims overload and the effect-atom-boundary peer ([48fe91e](https://github.com/KATT/raf-effect-atom/commit/48fe91eabf71a0eb36b49c907fabae7afa5abd90))
89
+ - **raf-effect-atom:** follow initialValueTarget in useAtomResult and throw defects ([7f577ea](https://github.com/KATT/raf-effect-atom/commit/7f577eabbdd567313b542db61d532bb36a2183ca))
90
+ - **raf-effect-atom:** loader verbs return handles that components read ([52a4323](https://github.com/KATT/raf-effect-atom/commit/52a4323bae1507e6b5f524262b7ed1609b1ed5d6))
91
+ - **raf-effect-atom:** loader verbs, streaming hydration and the read hook ([6870b1a](https://github.com/KATT/raf-effect-atom/commit/6870b1a9c7742d2fa15ce1391c8fdac66307d89e))
92
+ - **raf-effect-atom:** make the browser registry's idle TTL configurable ([1c55d0e](https://github.com/KATT/raf-effect-atom/commit/1c55d0e74f54bed7cef895434357355229cff512))
93
+ - **raf-effect-atom:** read the loader's registry under HydrationBoundary during server rendering ([6751792](https://github.com/KATT/raf-effect-atom/commit/675179254563b85c61f9a96e646fa9c72f47cc83))
94
+ - **raf-effect-atom:** report request waterfalls in development, with useWaterfall as the escape hatch ([4b6fe2a](https://github.com/KATT/raf-effect-atom/commit/4b6fe2aebb939ea0a7095a5121558aa0cbc5bac5))
95
+ - **raf-effect-atom:** type useAtomResult as a success when no typed failure is left ([aeb8dc6](https://github.com/KATT/raf-effect-atom/commit/aeb8dc63e0ad874a23c3a992afed540c27c9bb7d))
96
+
97
+ ### Bug Fixes
98
+
99
+ - **raf-effect-atom:** call use() on a streamed result's promise on every render that reads it ([36d2aaa](https://github.com/KATT/raf-effect-atom/commit/36d2aaa8bacf18a60507e4997fd61476d7a908c0)), references [#41](https://github.com/KATT/raf-effect-atom/issues/41)
100
+ - **raf-effect-atom:** point waterfall reports at the URL before the escape hatches ([0d127e3](https://github.com/KATT/raf-effect-atom/commit/0d127e38a10cb3a44340c5540c75b53491faceee))
101
+ - **raf-effect-atom:** refresh the atom the registry runs for a key in Loader.fetch ([0b35c97](https://github.com/KATT/raf-effect-atom/commit/0b35c97b4de60de254c7fc921b89aed7be753ec8))
102
+ - **raf-effect-atom:** wait for results streamed in a server component payload ([00cb981](https://github.com/KATT/raf-effect-atom/commit/00cb98125aad2cda70525fbfdedcd2b5a8f7fc79))
103
+
104
+ ### Code Refactoring
105
+
106
+ - **examples:** read boundary claims through wrap ([ae92f67](https://github.com/KATT/raf-effect-atom/commit/ae92f67722417542ee06a7299b327a77a0597814))
107
+ - **raf-effect-atom:** rename files to kebab-case ([285bf4b](https://github.com/KATT/raf-effect-atom/commit/285bf4b4e5e3184ac162129f51c04d9c96921650))
108
+ - **raf-effect-atom:** restore Effect's file names ([92dc1d8](https://github.com/KATT/raf-effect-atom/commit/92dc1d8e52212c28042153cbd7b1146870839831))
109
+
110
+ ### Documentation
111
+
112
+ - catch RpcClientError in an app boundary, and read fully claimed atoms as successes ([fe9b485](https://github.com/KATT/raf-effect-atom/commit/fe9b485fd19643a34f1709ebb5b66c90036eea47))
113
+ - point coding agents at the rules from the READMEs ([e8d3047](https://github.com/KATT/raf-effect-atom/commit/e8d30477e840df83c0f47764bdf505bc61181f14))
114
+ - **raf-effect-atom:** add the package README ([69c589e](https://github.com/KATT/raf-effect-atom/commit/69c589e020527ca087d7a54fb99544460ebc7774))
115
+
116
+ ### Tests
117
+
118
+ - keep each package's Vitest module cache to itself ([be11618](https://github.com/KATT/raf-effect-atom/commit/be11618b41f9cecaba6968a356677a7dd3bfe734))
119
+
120
+ ### Continuous Integration
121
+
122
+ - release each changed package after CI passes on main, and pack-check the tarballs on every push ([8ab3b90](https://github.com/KATT/raf-effect-atom/commit/8ab3b90e4ac2e7d6c25c028aace7167279bdb2b3))
package/README.md CHANGED
@@ -17,27 +17,33 @@ npm install raf-effect-atom effect @effect/atom-react react
17
17
 
18
18
  ## Usage
19
19
 
20
- A loader starts atoms with a verb per call. `prefetch` starts one and streams it, `ensure` waits for its first result, and `fetch` waits for a fresh one. Typed failures come back as results, so a loader can redirect on a tag:
20
+ A loader starts atoms with a verb per call, given an atom family and its arguments. `prefetch` starts one and streams it, `ensure` waits for its first result, and `fetch` waits for a fresh one. Each returns a handle, plain data naming the atom, which the loader returns for the page to pass down. Typed failures come back as results, so a loader can redirect on a tag:
21
21
 
22
22
  ```ts
23
+ import { Loader } from "raf-effect-atom";
24
+
23
25
  // issue: (id: number) => an AtomRpc query with a serializationKey
24
26
  export async function loader(args: LoaderArgs) {
25
27
  const id = Number(args.params.id);
26
- args.context.atoms.prefetch(comments(id));
27
- const result = await args.context.atoms.ensure(issue(id));
28
+ const handles = {
29
+ comments: args.context.atoms.prefetch(comments, id),
30
+ issue: await args.context.atoms.ensure(issue, id),
31
+ };
32
+ const result = Loader.result(handles.issue);
28
33
  if (AsyncResult.isFailure(result)) {
29
34
  // result.cause holds IssueNotFound | Unauthorized | RpcClientError
30
35
  }
36
+ return handles;
31
37
  }
32
38
  ```
33
39
 
34
- A component reads the atom with `useAtomResult`, which waits for a streamed result instead of starting the atom again, and returns typed failures. It also reads an atom derived from one, such as the atom an [`effect-atom-boundary`](https://github.com/KATT/raf-effect-atom/tree/main/packages/effect-atom-boundary) boundary's `wrap` returns, which throws the errors that boundary claims to it:
40
+ A component takes the handle as a prop and reads it with `useAtomResult(source, handle)`, which waits for a streamed result instead of starting the atom again, and returns typed failures. Given a handle instead of an id, it can't start a request of its own. The source can be a family derived from the loader's, such as the one an [`effect-atom-boundary`](https://github.com/KATT/raf-effect-atom/tree/main/packages/effect-atom-boundary) boundary's `wrap` returns, which throws the errors that boundary claims to it:
35
41
 
36
42
  ```tsx
37
- import { useAtomResult } from "raf-effect-atom";
43
+ import { type Handle, useAtomResult } from "raf-effect-atom";
38
44
 
39
- function IssueTitle(props: { readonly id: number }) {
40
- const result = useAtomResult(useIssueBoundary().wrap(issue(props.id)));
45
+ function IssueTitle(props: { readonly issue: Handle<typeof issue> }) {
46
+ const result = useAtomResult(useIssueBoundary().wrap(issue), props.issue);
41
47
  // Success<Issue, never>: its boundaries claim every failure
42
48
  return <h1>{result.value.title}</h1>;
43
49
  }
@@ -46,14 +52,14 @@ function IssueTitle(props: { readonly id: number }) {
46
52
  Each framework has a subpath that gives every server request its own registry and sends it to the browser:
47
53
 
48
54
  - `raf-effect-atom/tanstack-router`, for TanStack Router and TanStack Start: `setupRouterAtoms({ router, registry })` in `getRouter`, with `Loader.make(registry)` on the router context.
49
- - `raf-effect-atom/next`, for the App Router: `prefetch`, `ensure` and `fetch` in server components, and `<HydrateAtoms state={dehydrate()}>` around the client components that read them.
50
- - `raf-effect-atom/react-router`, for Framework Mode: `atomsMiddleware(atomsContext)` as the root middleware, loaders that return `atoms.dehydrate()`, and `HydrationBoundary` in the route component.
55
+ - `raf-effect-atom/next`, for the App Router: `prefetch`, `ensure` and `fetch` in server components, which pass the handles to client components as props, and `<HydrateAtoms state={await dehydrate()}>` around them. `makeRequestAtoms({ seed })` makes the same functions over a registry seeded from the request, such as with its cookie.
56
+ - `raf-effect-atom/react-router`, for Framework Mode: `atomsMiddleware(atomsContext)` as the root middleware, loaders that return `atoms.dehydrate()` with their handles, and `HydrationBoundary` in the route component.
51
57
 
52
- In development, `useAtomResult` reports a read that starts an atom nothing started, a request waterfall. `<WaterfallDetection mode="throw">` at the root makes it throw to the nearest error boundary. `useWaterfall(atom)` starts an atom whose input exists only in render, and marks it as a known waterfall.
58
+ In development, `useAtomResult` reports a read that starts an atom nothing started, a request waterfall. `<WaterfallDetection mode="throw">` at the root makes it throw to the nearest error boundary. `useWaterfall(source, ...args)` starts an atom whose input exists only in render, marks it as a known waterfall, and returns its handle.
53
59
 
54
60
  ## For coding agents
55
61
 
56
- Start every atom in a loader or a server component, never in a component or `useEffect`, and put the state that picks data (pages, filters, open dialogs) in the URL so the loader can. Event handlers and `useWaterfall` are escape hatches for state that can't be in the URL. Read atoms with `useAtomResult`, not `useAtomValue` or `useAtomSuspense`. Keep `<WaterfallDetection mode="throw">` at the root and fix what it reports instead of turning it off. The full rules, with the reason for each: [For coding agents](https://raf-effect-atom.katt.dev/docs/coding-agents). The docs as markdown: [llms.txt](https://raf-effect-atom.katt.dev/llms.txt).
62
+ Start every atom in a loader or a server component, never in a component or `useEffect`, and put the state that picks data (pages, filters, open dialogs) in the URL so the loader can. Event handlers and `useWaterfall` are escape hatches for state that can't be in the URL. Pass components the handles the loader returns, not ids, and read them with `useAtomResult`, not `useAtomValue` or `useAtomSuspense`. Keep `<WaterfallDetection mode="throw">` at the root and fix what it reports instead of turning it off. The full rules, with the reason for each: [For coding agents](https://raf-effect-atom.katt.dev/docs/coding-agents). The docs as markdown: [llms.txt](https://raf-effect-atom.katt.dev/llms.txt).
57
63
 
58
64
  ## License
59
65
 
@@ -0,0 +1,90 @@
1
+ import * as Atom from "effect/reactivity/Atom";
2
+ import * as AsyncResult from "effect/reactivity/AsyncResult";
3
+ /**
4
+ * What the verbs start and `useAtomResult` reads: an atom family, a function
5
+ * from arguments to an `AsyncResult` atom such as an `AtomRpc` query, or an
6
+ * `AsyncResult` atom that takes no arguments.
7
+ *
8
+ * @stability unstable
9
+ * @category models
10
+ * @since 0.1.0
11
+ */
12
+ export type Source = Atom.Atom<AsyncResult.AsyncResult<any, any>> | ((...args: any) => Atom.Atom<AsyncResult.AsyncResult<any, any>>);
13
+ /**
14
+ * The arguments a source takes: a family's parameters, or none for an atom.
15
+ *
16
+ * @stability unstable
17
+ * @category utility types
18
+ * @since 0.1.0
19
+ */
20
+ export type ArgsOf<S> = S extends ((...args: infer Args) => unknown) ? Args : [];
21
+ /**
22
+ * The success type of a source's `AsyncResult`.
23
+ *
24
+ * @stability unstable
25
+ * @category utility types
26
+ * @since 0.1.0
27
+ */
28
+ export type SuccessOf<S> = ResultOf<S> extends AsyncResult.AsyncResult<infer A, infer _E> ? A : never;
29
+ /**
30
+ * The error type of a source's `AsyncResult`.
31
+ *
32
+ * @stability unstable
33
+ * @category utility types
34
+ * @since 0.1.0
35
+ */
36
+ export type ErrorOf<S> = ResultOf<S> extends AsyncResult.AsyncResult<infer _A, infer E> ? E : never;
37
+ type ResultOf<S> = S extends ((...args: ReadonlyArray<never>) => infer Built) ? Built extends Atom.Atom<infer Result> ? Result : never : S extends Atom.Atom<infer Result> ? Result : never;
38
+ declare const SourceType: unique symbol;
39
+ /**
40
+ * A handle's serialization key, branded with its source. The brand sits on
41
+ * a string, so types that map loader data to what survives serialization,
42
+ * such as React Router's, keep it as is.
43
+ *
44
+ * @stability unstable
45
+ * @category models
46
+ * @since 0.1.0
47
+ */
48
+ export type HandleKey<S> = string & {
49
+ /** Type-only: ties the handle to its source. Never present at runtime. */
50
+ readonly [SourceType]?: S;
51
+ };
52
+ /**
53
+ * An atom a loader started: its source's arguments, and the atom's
54
+ * serialization key. Its type names its source, so `useAtomResult` reads it
55
+ * only through a source with the same arguments and success type.
56
+ *
57
+ * **Details**
58
+ *
59
+ * `Loader.prefetch` and `useWaterfall` return it, without waiting: the
60
+ * component that reads it suspends until the result settles.
61
+ *
62
+ * @see {@link LoadedHandle} for the handle `ensure` and `fetch` return
63
+ *
64
+ * @stability unstable
65
+ * @category models
66
+ * @since 0.1.0
67
+ */
68
+ export interface Handle<S> {
69
+ readonly args: ArgsOf<S>;
70
+ /** The atom's serialization key; `undefined` for an atom without one. */
71
+ readonly key: HandleKey<S> | undefined;
72
+ }
73
+ /**
74
+ * A handle from `Loader.ensure` or `Loader.fetch`: they waited for the
75
+ * result, so the component that reads it renders without suspending.
76
+ *
77
+ * **Details**
78
+ *
79
+ * A `LoadedHandle` fits a `Handle` prop, and a `Handle` doesn't fit a
80
+ * `LoadedHandle` prop. In the loader that ran the verb, `Loader.result`
81
+ * returns the result it waited for.
82
+ *
83
+ * @stability unstable
84
+ * @category models
85
+ * @since 0.1.0
86
+ */
87
+ export interface LoadedHandle<S> extends Handle<S> {
88
+ readonly loaded: true;
89
+ }
90
+ export {};
package/dist/Hooks.d.mts CHANGED
@@ -1,18 +1,34 @@
1
+ import { ErrorOf, Handle, Source, SuccessOf } from "./Handle.mjs";
1
2
  import { SettledResult } from "./Loader.mjs";
2
3
  import * as Atom from "effect/reactivity/Atom";
3
4
  import * as AsyncResult from "effect/reactivity/AsyncResult";
4
5
  /**
5
- * Reads an `AsyncResult` atom through React Suspense: a result streamed from
6
- * the server, a hydrated one, or one a loader started.
6
+ * A source with the same arguments and success type as `S`, and any error:
7
+ * the sources whose handles `S` reads, such as the source an error
8
+ * boundary's `wrap(S)` wraps.
9
+ */
10
+ type Readable<S> = S extends ((...args: infer Args) => Atom.Atom<AsyncResult.AsyncResult<infer A, infer _E>>) ? (...args: Args) => Atom.Atom<AsyncResult.AsyncResult<A, unknown>> : S extends Atom.Atom<AsyncResult.AsyncResult<infer A, infer _E>> ? Atom.Atom<AsyncResult.AsyncResult<A, unknown>> : never;
11
+ /**
12
+ * Reads a handle through React Suspense, through the source that made it: a
13
+ * result streamed from the server, a hydrated one, or one a loader started.
7
14
  *
8
15
  * **When to use**
9
16
  *
10
- * Use for every atom a loader starts or the server renders, or for an atom
11
- * derived from one with `initialValueTarget` pointing at it, such as an error
12
- * boundary's wrapper.
17
+ * Use in the component a loader, a server component or `useWaterfall` gave
18
+ * the handle to. The source can be the one the verb started, or one derived
19
+ * from it whose atoms point at its atoms with `initialValueTarget`, such as
20
+ * an error boundary's `wrap(source)`.
13
21
  *
14
22
  * **Details**
15
23
  *
24
+ * - The atom is `source(...handle.args)`, or `source` itself for an atom.
25
+ * In development, the read throws when the atom it hydrates has another
26
+ * serialization key than the handle's: the handle came from another
27
+ * source.
28
+ * - A handle from `ensure` or `fetch` renders without suspending, unless
29
+ * the registry dropped the result since; one from `prefetch` or
30
+ * `useWaterfall` suspends until the result settles. The registry is the
31
+ * source of truth either way: the read follows refreshes and mutations.
16
32
  * - A result the server was still loading when it dehydrated is awaited
17
33
  * instead of started, so the browser doesn't request it a second time.
18
34
  * - The atom is read through `hydrated`, so a hydrated result is kept.
@@ -29,13 +45,17 @@ import * as AsyncResult from "effect/reactivity/AsyncResult";
29
45
  *
30
46
  * **Gotchas**
31
47
  *
32
- * A read of an atom that nothing started starts it, and its request waits
33
- * for the render: a request waterfall. In development, the read reports it
34
- * as `WaterfallDetection` says, by default with a warning. Start the atom in
35
- * a loader or a server component, with the state that picks it in the URL.
36
- * For state that can't be in the URL, start it in the event handler that
37
- * changes the state, or read it through `useWaterfall` when only the render
38
- * knows its input.
48
+ * A read of an atom that nothing started, such as one whose loader result the
49
+ * registry dropped, starts it, and its request waits for the render: a
50
+ * request waterfall. In development, the read reports it as
51
+ * `WaterfallDetection` says, by default with a warning. Start the atom in a
52
+ * loader or a server component, with the state that picks it in the URL,
53
+ * and pass its handle down. For state that can't be in the URL, start it in
54
+ * the event handler that changes the state, or make its handle with
55
+ * `useWaterfall` when only the render knows its input.
56
+ *
57
+ * Two sources with the same arguments and success type take each other's
58
+ * handles as far as the types go; the development check catches it.
39
59
  *
40
60
  * During server rendering, the registry must be a request registry (see
41
61
  * `RequestRegistry.make`), or a module-level registry would share results
@@ -51,10 +71,10 @@ import * as AsyncResult from "effect/reactivity/AsyncResult";
51
71
  *
52
72
  * ```tsx
53
73
  * import { AsyncResult } from "effect/reactivity"
54
- * import { useAtomResult } from "raf-effect-atom"
74
+ * import { type Handle, useAtomResult } from "raf-effect-atom"
55
75
  *
56
- * function IssueTitle(props: { readonly id: number }) {
57
- * const result = useAtomResult(issue(props.id))
76
+ * function IssueTitle(props: { readonly issue: Handle<typeof issue> }) {
77
+ * const result = useAtomResult(issue, props.issue)
58
78
  * return AsyncResult.isSuccess(result) ? <h1>{result.value.title}</h1> : null
59
79
  * }
60
80
  * ```
@@ -65,4 +85,5 @@ import * as AsyncResult from "effect/reactivity/AsyncResult";
65
85
  * @category hooks
66
86
  * @since 0.1.0
67
87
  */
68
- export declare const useAtomResult: <A, E>(atom: Atom.Atom<AsyncResult.AsyncResult<A, E>>) => [E] extends [never] ? AsyncResult.Success<A, never> : SettledResult<A, E>;
88
+ export declare const useAtomResult: <S extends Source>(source: S, handle: NoInfer<Handle<Readable<S>>>) => [ErrorOf<S>] extends [never] ? AsyncResult.Success<SuccessOf<S>, never> : SettledResult<SuccessOf<S>, ErrorOf<S>>;
89
+ export {};
package/dist/Hooks.mjs CHANGED
@@ -3,6 +3,7 @@ import { stream } from "./internal/pending.mjs";
3
3
  import { hydrated } from "./Hydration.mjs";
4
4
  import { isDevelopment } from "./internal/development.mjs";
5
5
  import { keyOf, release } from "./internal/leases.mjs";
6
+ import { atomOf } from "./internal/handles.mjs";
6
7
  import { isRequestRegistry } from "./RequestRegistry.mjs";
7
8
  import { isServer } from "./internal/registry.mjs";
8
9
  import { ModeContext, check } from "./internal/waterfalls.mjs";
@@ -31,17 +32,26 @@ const sourceOf = (atom) => {
31
32
  return atom;
32
33
  };
33
34
  /**
34
- * Reads an `AsyncResult` atom through React Suspense: a result streamed from
35
- * the server, a hydrated one, or one a loader started.
35
+ * Reads a handle through React Suspense, through the source that made it: a
36
+ * result streamed from the server, a hydrated one, or one a loader started.
36
37
  *
37
38
  * **When to use**
38
39
  *
39
- * Use for every atom a loader starts or the server renders, or for an atom
40
- * derived from one with `initialValueTarget` pointing at it, such as an error
41
- * boundary's wrapper.
40
+ * Use in the component a loader, a server component or `useWaterfall` gave
41
+ * the handle to. The source can be the one the verb started, or one derived
42
+ * from it whose atoms point at its atoms with `initialValueTarget`, such as
43
+ * an error boundary's `wrap(source)`.
42
44
  *
43
45
  * **Details**
44
46
  *
47
+ * - The atom is `source(...handle.args)`, or `source` itself for an atom.
48
+ * In development, the read throws when the atom it hydrates has another
49
+ * serialization key than the handle's: the handle came from another
50
+ * source.
51
+ * - A handle from `ensure` or `fetch` renders without suspending, unless
52
+ * the registry dropped the result since; one from `prefetch` or
53
+ * `useWaterfall` suspends until the result settles. The registry is the
54
+ * source of truth either way: the read follows refreshes and mutations.
45
55
  * - A result the server was still loading when it dehydrated is awaited
46
56
  * instead of started, so the browser doesn't request it a second time.
47
57
  * - The atom is read through `hydrated`, so a hydrated result is kept.
@@ -58,13 +68,17 @@ const sourceOf = (atom) => {
58
68
  *
59
69
  * **Gotchas**
60
70
  *
61
- * A read of an atom that nothing started starts it, and its request waits
62
- * for the render: a request waterfall. In development, the read reports it
63
- * as `WaterfallDetection` says, by default with a warning. Start the atom in
64
- * a loader or a server component, with the state that picks it in the URL.
65
- * For state that can't be in the URL, start it in the event handler that
66
- * changes the state, or read it through `useWaterfall` when only the render
67
- * knows its input.
71
+ * A read of an atom that nothing started, such as one whose loader result the
72
+ * registry dropped, starts it, and its request waits for the render: a
73
+ * request waterfall. In development, the read reports it as
74
+ * `WaterfallDetection` says, by default with a warning. Start the atom in a
75
+ * loader or a server component, with the state that picks it in the URL,
76
+ * and pass its handle down. For state that can't be in the URL, start it in
77
+ * the event handler that changes the state, or make its handle with
78
+ * `useWaterfall` when only the render knows its input.
79
+ *
80
+ * Two sources with the same arguments and success type take each other's
81
+ * handles as far as the types go; the development check catches it.
68
82
  *
69
83
  * During server rendering, the registry must be a request registry (see
70
84
  * `RequestRegistry.make`), or a module-level registry would share results
@@ -80,10 +94,10 @@ const sourceOf = (atom) => {
80
94
  *
81
95
  * ```tsx
82
96
  * import { AsyncResult } from "effect/reactivity"
83
- * import { useAtomResult } from "raf-effect-atom"
97
+ * import { type Handle, useAtomResult } from "raf-effect-atom"
84
98
  *
85
- * function IssueTitle(props: { readonly id: number }) {
86
- * const result = useAtomResult(issue(props.id))
99
+ * function IssueTitle(props: { readonly issue: Handle<typeof issue> }) {
100
+ * const result = useAtomResult(issue, props.issue)
87
101
  * return AsyncResult.isSuccess(result) ? <h1>{result.value.title}</h1> : null
88
102
  * }
89
103
  * ```
@@ -94,25 +108,27 @@ const sourceOf = (atom) => {
94
108
  * @category hooks
95
109
  * @since 0.1.0
96
110
  */
97
- const useAtomResult = (atom) => {
111
+ const useAtomResult = (source, handle) => {
98
112
  const registry = React.useContext(RegistryContext);
113
+ const atom = atomOf(source, handle.args);
99
114
  if (isServer() && !isRequestRegistry(registry) && Effect.runSync(isDevelopment)) throw new Error("raf-effect-atom: useAtomResult rendered on the server without a request registry, so results would be shared between requests. Provide RequestRegistry.make() through RegistryContext, as the framework integrations do.");
100
115
  const target = sourceOf(atom);
101
- const source = hydrated(target);
116
+ const started = hydrated(target);
102
117
  const read = target === atom ? hydrated(atom) : atom;
103
- const key = keyOf(source);
118
+ const key = keyOf(started);
119
+ if (process.env.NODE_ENV !== "production" && handle.key !== void 0 && Predicate.isString(key) && handle.key !== key) throw new Error(`raf-effect-atom: useAtomResult read the handle of ${handle.key} through a source that makes ${key}. Read a handle through the source that made it.`);
104
120
  const streamed = Predicate.isString(key) ? stream(registry, key) : void 0;
105
121
  if (streamed !== void 0) React.use(streamed);
106
- if (process.env.NODE_ENV !== "production") check(registry, source, React.use(ModeContext));
107
- if (read !== source) registry.get(source);
122
+ if (process.env.NODE_ENV !== "production") check(registry, started, React.use(ModeContext));
123
+ if (read !== started) registry.get(started);
108
124
  const current = registry.get(read);
109
125
  const result = useAtomSuspense(read, {
110
126
  includeFailure: true,
111
127
  suspendOnWaiting: AsyncResult.isFailure(current) && current.waiting
112
128
  });
113
129
  React.useEffect(() => {
114
- release(registry, source);
115
- }, [registry, source]);
130
+ release(registry, started);
131
+ }, [registry, started]);
116
132
  if (AsyncResult.isFailure(result) && Option.isNone(Cause.findErrorOption(result.cause))) throw Cause.squash(result.cause);
117
133
  return result;
118
134
  };