raf-effect-atom 0.0.0-stage → 0.1.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 +111 -0
- package/LICENSE +21 -0
- package/README.md +65 -2
- package/dist/Handle.d.mts +90 -0
- package/dist/Hooks.d.mts +89 -0
- package/dist/Hooks.mjs +135 -0
- package/dist/Hydration.d.mts +93 -0
- package/dist/Hydration.mjs +134 -0
- package/dist/Loader.d.mts +192 -0
- package/dist/Loader.mjs +286 -0
- package/dist/Next.d.mts +65 -0
- package/dist/Next.mjs +64 -0
- package/dist/NextClient.d.mts +32 -0
- package/dist/NextClient.mjs +44 -0
- package/dist/ReactHydration.d.mts +52 -0
- package/dist/ReactHydration.mjs +71 -0
- package/dist/ReactRouter.d.mts +63 -0
- package/dist/ReactRouter.mjs +116 -0
- package/dist/RegistryProvider.d.mts +40 -0
- package/dist/RegistryProvider.mjs +55 -0
- package/dist/RequestRegistry.d.mts +54 -0
- package/dist/RequestRegistry.mjs +76 -0
- package/dist/TanStackRouter.d.mts +84 -0
- package/dist/TanStackRouter.mjs +134 -0
- package/dist/Waterfalls.d.mts +99 -0
- package/dist/Waterfalls.mjs +111 -0
- package/dist/_virtual/_rolldown/runtime.mjs +12 -0
- package/dist/index.d.mts +8 -0
- package/dist/index.mjs +7 -0
- package/dist/internal/development.mjs +6 -0
- package/dist/internal/handles.mjs +22 -0
- package/dist/internal/leases.mjs +39 -0
- package/dist/internal/next.mjs +16 -0
- package/dist/internal/pending.mjs +75 -0
- package/dist/internal/registry.mjs +16 -0
- package/dist/internal/sources.mjs +14 -0
- package/dist/internal/waterfalls.d.mts +5 -0
- package/dist/internal/waterfalls.mjs +83 -0
- package/package.json +80 -3
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 (2026-10-23)
|
|
4
|
+
|
|
5
|
+
### ⚠ BREAKING CHANGES
|
|
6
|
+
|
|
7
|
+
* **raf-effect-atom:** `useAtomResult(atom, claims)` is gone. Read
|
|
8
|
+
`useAtomResult(boundary.wrap(atom))` instead. `effect-atom-boundary` is no
|
|
9
|
+
longer a peer dependency.
|
|
10
|
+
|
|
11
|
+
effect-atom-boundary stays a devDependency, so the hook's tests still read
|
|
12
|
+
through a real boundary's wrap (claimed tag thrown to the boundary, others
|
|
13
|
+
returned). The type test now checks that a derived atom with an
|
|
14
|
+
initialValueTarget is read with its own type.
|
|
15
|
+
|
|
16
|
+
The request waterfalls docs sample, written against the claims overload,
|
|
17
|
+
reads through `useIssueBoundary().wrap(atom)` too.
|
|
18
|
+
* **raf-effect-atom:** follow initialValueTarget in useAtomResult and throw defects
|
|
19
|
+
* **raf-effect-atom:** the loader verbs take a source and its arguments and
|
|
20
|
+
return a handle, and `useAtomResult` reads a handle through a source.
|
|
21
|
+
|
|
22
|
+
- `prefetch(issue, id)` returns a `Handle<typeof issue>`; `ensure` and
|
|
23
|
+
`fetch` return a `LoadedHandle`, which adds `loaded: true`. A source is
|
|
24
|
+
an atom family, such as an AtomRpc query, or one AsyncResult atom, whose
|
|
25
|
+
handle has no arguments. The same holds for `Loader.make(registry)`, the
|
|
26
|
+
router contexts' `atoms` and `raf-effect-atom/next`'s verbs.
|
|
27
|
+
- A handle is plain data, `{ args, key }`, so loaders return it in their
|
|
28
|
+
data and server components pass it to client components as a prop. Its
|
|
29
|
+
type names its source through a brand on the key, `HandleKey<S>`, so
|
|
30
|
+
one family's handle doesn't fit another's prop. The brand is on the key
|
|
31
|
+
because React Router's `Serialize` type drops an optional phantom field
|
|
32
|
+
from loader data and keeps branded strings; an atom source's args are a
|
|
33
|
+
mutable `[]` because `Serialize<readonly []>` is `readonly never[]`.
|
|
34
|
+
- `Loader.result(handle)` gives loader code the settled result of an
|
|
35
|
+
`ensure` or `fetch`. The result can't ride on the handle: AsyncResult and
|
|
36
|
+
Schema classes don't survive turbo-stream, seroval, JSON or server
|
|
37
|
+
component props. It's kept in a WeakMap keyed by the handle object, and
|
|
38
|
+
a copy of the handle throws.
|
|
39
|
+
- `useAtomResult(source, handle)` reads `source(...handle.args)`. Its
|
|
40
|
+
handle parameter widens the source's error to `unknown`, so a boundary's
|
|
41
|
+
`wrap(issue)` reads `issue`'s handles with the per-tag narrowing and the
|
|
42
|
+
`Success<A, never>` typing kept. In development it throws when the atom
|
|
43
|
+
it hydrates has a serialization key other than the handle's.
|
|
44
|
+
- `useWaterfall(source, ...args)` returns a handle, the one way to make a
|
|
45
|
+
handle during render.
|
|
46
|
+
|
|
47
|
+
The waterfall warning names the new call shapes.
|
|
48
|
+
|
|
49
|
+
Migrated with it, so no commit has a red tree:
|
|
50
|
+
- The three example apps: loaders return the handles of the atoms they
|
|
51
|
+
start, and pages pass them down (TanStack Start through
|
|
52
|
+
`Route.useLoaderData()`, React Router through `loaderData.handles` next
|
|
53
|
+
to the dehydrated atoms, Next.js as server component props). Components
|
|
54
|
+
take a handle instead of an id. "Load more" keeps the next page's handle
|
|
55
|
+
in state, made by `Loader.make(registry).prefetch(source, ...args)` in
|
|
56
|
+
the click handler.
|
|
57
|
+
- www: the docs route, demo embeds and the loader-verbs, waterfalls,
|
|
58
|
+
hydration, boundary-chain and every-tag-caught demos; the TanStack Start, Next.js and
|
|
59
|
+
React Router doc samples, the error-boundaries and every-tag-caught
|
|
60
|
+
samples included; the home page's steps; and the pages that
|
|
61
|
+
show the verbs and reads (loader verbs gains a Handles section and
|
|
62
|
+
`Loader.result`, reading atoms explains handles as props, request
|
|
63
|
+
waterfalls, coding agents, shared boundaries, error boundaries, loader
|
|
64
|
+
failures).
|
|
65
|
+
`www/src/use-loaded.ts` is gone: a wrapper generic over a family can't
|
|
66
|
+
satisfy `useAtomResult`'s `Readable` constraint, and
|
|
67
|
+
`useAtomResult(source, handle).value` reads as well. The "Fetches in a
|
|
68
|
+
component" mistake on the home page is now caught by the compiler.
|
|
69
|
+
- The package READMEs, the boundary's JSDoc example, and the decision in
|
|
70
|
+
`.agent-docs/decisions/atom-handles.md`.
|
|
71
|
+
|
|
72
|
+
### Features
|
|
73
|
+
|
|
74
|
+
* **raf-effect-atom:** add the next subpath ([92cacd8](https://github.com/KATT/raf-effect-atom/commit/92cacd86cb6d6e1784d9a07f71bd9fc3ec5ca0da))
|
|
75
|
+
* **raf-effect-atom:** add the react-router subpath ([09b65dc](https://github.com/KATT/raf-effect-atom/commit/09b65dcbc6cfde2ae1ba2bb89e580746e9512f8b))
|
|
76
|
+
* **raf-effect-atom:** add the tanstack-router subpath ([0189c83](https://github.com/KATT/raf-effect-atom/commit/0189c834cd46fc05ed866859e7527b3bd2ac47df))
|
|
77
|
+
* **raf-effect-atom:** drop the claims overload and the effect-atom-boundary peer ([48fe91e](https://github.com/KATT/raf-effect-atom/commit/48fe91eabf71a0eb36b49c907fabae7afa5abd90))
|
|
78
|
+
* **raf-effect-atom:** follow initialValueTarget in useAtomResult and throw defects ([7f577ea](https://github.com/KATT/raf-effect-atom/commit/7f577eabbdd567313b542db61d532bb36a2183ca))
|
|
79
|
+
* **raf-effect-atom:** loader verbs return handles that components read ([52a4323](https://github.com/KATT/raf-effect-atom/commit/52a4323bae1507e6b5f524262b7ed1609b1ed5d6))
|
|
80
|
+
* **raf-effect-atom:** loader verbs, streaming hydration and the read hook ([6870b1a](https://github.com/KATT/raf-effect-atom/commit/6870b1a9c7742d2fa15ce1391c8fdac66307d89e))
|
|
81
|
+
* **raf-effect-atom:** make the browser registry's idle TTL configurable ([1c55d0e](https://github.com/KATT/raf-effect-atom/commit/1c55d0e74f54bed7cef895434357355229cff512))
|
|
82
|
+
* **raf-effect-atom:** read the loader's registry under HydrationBoundary during server rendering ([6751792](https://github.com/KATT/raf-effect-atom/commit/675179254563b85c61f9a96e646fa9c72f47cc83))
|
|
83
|
+
* **raf-effect-atom:** report request waterfalls in development, with useWaterfall as the escape hatch ([4b6fe2a](https://github.com/KATT/raf-effect-atom/commit/4b6fe2aebb939ea0a7095a5121558aa0cbc5bac5))
|
|
84
|
+
* **raf-effect-atom:** type useAtomResult as a success when no typed failure is left ([aeb8dc6](https://github.com/KATT/raf-effect-atom/commit/aeb8dc63e0ad874a23c3a992afed540c27c9bb7d))
|
|
85
|
+
|
|
86
|
+
### Bug Fixes
|
|
87
|
+
|
|
88
|
+
* **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)
|
|
89
|
+
* **raf-effect-atom:** point waterfall reports at the URL before the escape hatches ([0d127e3](https://github.com/KATT/raf-effect-atom/commit/0d127e38a10cb3a44340c5540c75b53491faceee))
|
|
90
|
+
* **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))
|
|
91
|
+
* **raf-effect-atom:** wait for results streamed in a server component payload ([00cb981](https://github.com/KATT/raf-effect-atom/commit/00cb98125aad2cda70525fbfdedcd2b5a8f7fc79))
|
|
92
|
+
|
|
93
|
+
### Code Refactoring
|
|
94
|
+
|
|
95
|
+
* **examples:** read boundary claims through wrap ([ae92f67](https://github.com/KATT/raf-effect-atom/commit/ae92f67722417542ee06a7299b327a77a0597814))
|
|
96
|
+
* **raf-effect-atom:** rename files to kebab-case ([285bf4b](https://github.com/KATT/raf-effect-atom/commit/285bf4b4e5e3184ac162129f51c04d9c96921650))
|
|
97
|
+
* **raf-effect-atom:** restore Effect's file names ([92dc1d8](https://github.com/KATT/raf-effect-atom/commit/92dc1d8e52212c28042153cbd7b1146870839831))
|
|
98
|
+
|
|
99
|
+
### Documentation
|
|
100
|
+
|
|
101
|
+
* catch RpcClientError in an app boundary, and read fully claimed atoms as successes ([fe9b485](https://github.com/KATT/raf-effect-atom/commit/fe9b485fd19643a34f1709ebb5b66c90036eea47))
|
|
102
|
+
* point coding agents at the rules from the READMEs ([e8d3047](https://github.com/KATT/raf-effect-atom/commit/e8d30477e840df83c0f47764bdf505bc61181f14))
|
|
103
|
+
* **raf-effect-atom:** add the package README ([69c589e](https://github.com/KATT/raf-effect-atom/commit/69c589e020527ca087d7a54fb99544460ebc7774))
|
|
104
|
+
|
|
105
|
+
### Tests
|
|
106
|
+
|
|
107
|
+
* keep each package's Vitest module cache to itself ([be11618](https://github.com/KATT/raf-effect-atom/commit/be11618b41f9cecaba6968a356677a7dd3bfe734))
|
|
108
|
+
|
|
109
|
+
### Continuous Integration
|
|
110
|
+
|
|
111
|
+
* 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/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Alex / KATT
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,66 @@
|
|
|
1
|
-
#
|
|
1
|
+
# raf-effect-atom
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Render-as-you-fetch for [Effect Atom](https://github.com/Effect-TS/effect/tree/main/packages/atom/react). Route loaders start atoms before anything renders, the server streams their results into the browser, and the browser reads them without requesting them again. Typed failures survive the trip: a server-side `IssueNotFound` is an `IssueNotFound` in the browser.
|
|
4
|
+
|
|
5
|
+
**[GitHub](https://github.com/KATT/raf-effect-atom)**
|
|
6
|
+
|
|
7
|
+
> [!NOTE]
|
|
8
|
+
> raf-effect-atom is early. The API can still change between minor releases.
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install raf-effect-atom effect @effect/atom-react react
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
`effect`, `@effect/atom-react` and `react` (19.2 or newer) are peer dependencies.
|
|
17
|
+
|
|
18
|
+
## Usage
|
|
19
|
+
|
|
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
|
+
|
|
22
|
+
```ts
|
|
23
|
+
import { Loader } from "raf-effect-atom";
|
|
24
|
+
|
|
25
|
+
// issue: (id: number) => an AtomRpc query with a serializationKey
|
|
26
|
+
export async function loader(args: LoaderArgs) {
|
|
27
|
+
const id = Number(args.params.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);
|
|
33
|
+
if (AsyncResult.isFailure(result)) {
|
|
34
|
+
// result.cause holds IssueNotFound | Unauthorized | RpcClientError
|
|
35
|
+
}
|
|
36
|
+
return handles;
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
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:
|
|
41
|
+
|
|
42
|
+
```tsx
|
|
43
|
+
import { type Handle, useAtomResult } from "raf-effect-atom";
|
|
44
|
+
|
|
45
|
+
function IssueTitle(props: { readonly issue: Handle<typeof issue> }) {
|
|
46
|
+
const result = useAtomResult(useIssueBoundary().wrap(issue), props.issue);
|
|
47
|
+
// Success<Issue, never>: its boundaries claim every failure
|
|
48
|
+
return <h1>{result.value.title}</h1>;
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Each framework has a subpath that gives every server request its own registry and sends it to the browser:
|
|
53
|
+
|
|
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.
|
|
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={dehydrate()}>` around them.
|
|
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.
|
|
57
|
+
|
|
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.
|
|
59
|
+
|
|
60
|
+
## For coding agents
|
|
61
|
+
|
|
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).
|
|
63
|
+
|
|
64
|
+
## License
|
|
65
|
+
|
|
66
|
+
MIT
|
|
@@ -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
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { ErrorOf, Handle, Source, SuccessOf } from "./Handle.mjs";
|
|
2
|
+
import { SettledResult } from "./Loader.mjs";
|
|
3
|
+
import * as Atom from "effect/reactivity/Atom";
|
|
4
|
+
import * as AsyncResult from "effect/reactivity/AsyncResult";
|
|
5
|
+
/**
|
|
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.
|
|
14
|
+
*
|
|
15
|
+
* **When to use**
|
|
16
|
+
*
|
|
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)`.
|
|
21
|
+
*
|
|
22
|
+
* **Details**
|
|
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.
|
|
32
|
+
* - A result the server was still loading when it dehydrated is awaited
|
|
33
|
+
* instead of started, so the browser doesn't request it a second time.
|
|
34
|
+
* - The atom is read through `hydrated`, so a hydrated result is kept.
|
|
35
|
+
* - Once the component mounts, the loader's lease ends and the component
|
|
36
|
+
* keeps the atom mounted.
|
|
37
|
+
* - A derived atom is read through the first serializable atom its
|
|
38
|
+
* `initialValueTarget` chain points at: that one is awaited, hydrated and
|
|
39
|
+
* released, so the derived atom doesn't request it again.
|
|
40
|
+
* - The settled result is returned, typed failures included. A defect is
|
|
41
|
+
* thrown to the error boundaries above. With no typed failures left, such
|
|
42
|
+
* as an atom whose boundaries claim all of them, it's typed as a success.
|
|
43
|
+
* - While a failed atom refreshes, such as after a boundary's `reset`, the
|
|
44
|
+
* read suspends instead of returning the old failure again.
|
|
45
|
+
*
|
|
46
|
+
* **Gotchas**
|
|
47
|
+
*
|
|
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.
|
|
59
|
+
*
|
|
60
|
+
* During server rendering, the registry must be a request registry (see
|
|
61
|
+
* `RequestRegistry.make`), or a module-level registry would share results
|
|
62
|
+
* between requests: in development, the hook throws without one.
|
|
63
|
+
*
|
|
64
|
+
* `Atom.transform`'s `initialValueTarget` option points at the end of the
|
|
65
|
+
* target's own chain, which for an AtomRpc query with `reactivityKeys` is an
|
|
66
|
+
* inner atom with no serialization key. A derived atom read here sets
|
|
67
|
+
* `initialValueTarget` to its source itself, as an error boundary's
|
|
68
|
+
* wrapper does, or it requests the source again after hydration.
|
|
69
|
+
*
|
|
70
|
+
* **Example**
|
|
71
|
+
*
|
|
72
|
+
* ```tsx
|
|
73
|
+
* import { AsyncResult } from "effect/reactivity"
|
|
74
|
+
* import { type Handle, useAtomResult } from "raf-effect-atom"
|
|
75
|
+
*
|
|
76
|
+
* function IssueTitle(props: { readonly issue: Handle<typeof issue> }) {
|
|
77
|
+
* const result = useAtomResult(issue, props.issue)
|
|
78
|
+
* return AsyncResult.isSuccess(result) ? <h1>{result.value.title}</h1> : null
|
|
79
|
+
* }
|
|
80
|
+
* ```
|
|
81
|
+
*
|
|
82
|
+
* @see {@link hydrated} for reading hydrated atoms with Atom's own hooks
|
|
83
|
+
*
|
|
84
|
+
* @stability unstable
|
|
85
|
+
* @category hooks
|
|
86
|
+
* @since 0.1.0
|
|
87
|
+
*/
|
|
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
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { stream } from "./internal/pending.mjs";
|
|
3
|
+
import { hydrated } from "./Hydration.mjs";
|
|
4
|
+
import { isDevelopment } from "./internal/development.mjs";
|
|
5
|
+
import { keyOf, release } from "./internal/leases.mjs";
|
|
6
|
+
import { atomOf } from "./internal/handles.mjs";
|
|
7
|
+
import { isRequestRegistry } from "./RequestRegistry.mjs";
|
|
8
|
+
import { isServer } from "./internal/registry.mjs";
|
|
9
|
+
import { ModeContext, check } from "./internal/waterfalls.mjs";
|
|
10
|
+
import * as Atom from "effect/reactivity/Atom";
|
|
11
|
+
import * as Predicate from "effect/Predicate";
|
|
12
|
+
import * as Cause from "effect/Cause";
|
|
13
|
+
import * as Effect from "effect/Effect";
|
|
14
|
+
import * as Option from "effect/Option";
|
|
15
|
+
import * as AsyncResult from "effect/reactivity/AsyncResult";
|
|
16
|
+
import { RegistryContext, useAtomSuspense } from "@effect/atom-react";
|
|
17
|
+
import * as React from "react";
|
|
18
|
+
// src/Hooks.ts
|
|
19
|
+
/**
|
|
20
|
+
* The read hook for atoms started by loaders and streamed from the server.
|
|
21
|
+
*
|
|
22
|
+
* @stability unstable
|
|
23
|
+
* @since 0.1.0
|
|
24
|
+
*/
|
|
25
|
+
/**
|
|
26
|
+
* The atom the loader started and the server sent: the first serializable
|
|
27
|
+
* atom in `atom`'s `initialValueTarget` chain, starting with `atom`. An
|
|
28
|
+
* AtomRpc query is serializable itself, though it points at an inner atom.
|
|
29
|
+
*/
|
|
30
|
+
const sourceOf = (atom) => {
|
|
31
|
+
for (let target = atom; target !== void 0; target = target.initialValueTarget) if (Atom.isSerializable(target)) return target;
|
|
32
|
+
return atom;
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
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.
|
|
37
|
+
*
|
|
38
|
+
* **When to use**
|
|
39
|
+
*
|
|
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)`.
|
|
44
|
+
*
|
|
45
|
+
* **Details**
|
|
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.
|
|
55
|
+
* - A result the server was still loading when it dehydrated is awaited
|
|
56
|
+
* instead of started, so the browser doesn't request it a second time.
|
|
57
|
+
* - The atom is read through `hydrated`, so a hydrated result is kept.
|
|
58
|
+
* - Once the component mounts, the loader's lease ends and the component
|
|
59
|
+
* keeps the atom mounted.
|
|
60
|
+
* - A derived atom is read through the first serializable atom its
|
|
61
|
+
* `initialValueTarget` chain points at: that one is awaited, hydrated and
|
|
62
|
+
* released, so the derived atom doesn't request it again.
|
|
63
|
+
* - The settled result is returned, typed failures included. A defect is
|
|
64
|
+
* thrown to the error boundaries above. With no typed failures left, such
|
|
65
|
+
* as an atom whose boundaries claim all of them, it's typed as a success.
|
|
66
|
+
* - While a failed atom refreshes, such as after a boundary's `reset`, the
|
|
67
|
+
* read suspends instead of returning the old failure again.
|
|
68
|
+
*
|
|
69
|
+
* **Gotchas**
|
|
70
|
+
*
|
|
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.
|
|
82
|
+
*
|
|
83
|
+
* During server rendering, the registry must be a request registry (see
|
|
84
|
+
* `RequestRegistry.make`), or a module-level registry would share results
|
|
85
|
+
* between requests: in development, the hook throws without one.
|
|
86
|
+
*
|
|
87
|
+
* `Atom.transform`'s `initialValueTarget` option points at the end of the
|
|
88
|
+
* target's own chain, which for an AtomRpc query with `reactivityKeys` is an
|
|
89
|
+
* inner atom with no serialization key. A derived atom read here sets
|
|
90
|
+
* `initialValueTarget` to its source itself, as an error boundary's
|
|
91
|
+
* wrapper does, or it requests the source again after hydration.
|
|
92
|
+
*
|
|
93
|
+
* **Example**
|
|
94
|
+
*
|
|
95
|
+
* ```tsx
|
|
96
|
+
* import { AsyncResult } from "effect/reactivity"
|
|
97
|
+
* import { type Handle, useAtomResult } from "raf-effect-atom"
|
|
98
|
+
*
|
|
99
|
+
* function IssueTitle(props: { readonly issue: Handle<typeof issue> }) {
|
|
100
|
+
* const result = useAtomResult(issue, props.issue)
|
|
101
|
+
* return AsyncResult.isSuccess(result) ? <h1>{result.value.title}</h1> : null
|
|
102
|
+
* }
|
|
103
|
+
* ```
|
|
104
|
+
*
|
|
105
|
+
* @see {@link hydrated} for reading hydrated atoms with Atom's own hooks
|
|
106
|
+
*
|
|
107
|
+
* @stability unstable
|
|
108
|
+
* @category hooks
|
|
109
|
+
* @since 0.1.0
|
|
110
|
+
*/
|
|
111
|
+
const useAtomResult = (source, handle) => {
|
|
112
|
+
const registry = React.useContext(RegistryContext);
|
|
113
|
+
const atom = atomOf(source, handle.args);
|
|
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.");
|
|
115
|
+
const target = sourceOf(atom);
|
|
116
|
+
const started = hydrated(target);
|
|
117
|
+
const read = target === atom ? hydrated(atom) : atom;
|
|
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.`);
|
|
120
|
+
const streamed = Predicate.isString(key) ? stream(registry, key) : void 0;
|
|
121
|
+
if (streamed !== void 0) React.use(streamed);
|
|
122
|
+
if (process.env.NODE_ENV !== "production") check(registry, started, React.use(ModeContext));
|
|
123
|
+
if (read !== started) registry.get(started);
|
|
124
|
+
const current = registry.get(read);
|
|
125
|
+
const result = useAtomSuspense(read, {
|
|
126
|
+
includeFailure: true,
|
|
127
|
+
suspendOnWaiting: AsyncResult.isFailure(current) && current.waiting
|
|
128
|
+
});
|
|
129
|
+
React.useEffect(() => {
|
|
130
|
+
release(registry, started);
|
|
131
|
+
}, [registry, started]);
|
|
132
|
+
if (AsyncResult.isFailure(result) && Option.isNone(Cause.findErrorOption(result.cause))) throw Cause.squash(result.cause);
|
|
133
|
+
return result;
|
|
134
|
+
};
|
|
135
|
+
export { useAtomResult };
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import * as Atom from "effect/reactivity/Atom";
|
|
2
|
+
import * as EffectHydration from "effect/reactivity/Hydration";
|
|
3
|
+
import * as AtomRegistry from "effect/reactivity/AtomRegistry";
|
|
4
|
+
declare namespace Hydration_d_exports {
|
|
5
|
+
export { dehydrate, hydrate, hydrated };
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Returns a serializable atom that keeps its hydrated value on its first
|
|
9
|
+
* read, instead of running its effect again.
|
|
10
|
+
*
|
|
11
|
+
* **When to use**
|
|
12
|
+
*
|
|
13
|
+
* The loader verbs and `useAtomResult` already read through it. Use it when
|
|
14
|
+
* reading a hydrated atom with `@effect/atom-react`'s own hooks.
|
|
15
|
+
*
|
|
16
|
+
* **Details**
|
|
17
|
+
*
|
|
18
|
+
* The copy shares the atom's serialization key, so both read the same
|
|
19
|
+
* registry node. It sets `skipInitialRun` on the atom's `Serializable`
|
|
20
|
+
* metadata, and is memoized per atom, so every call returns the same copy.
|
|
21
|
+
* An atom that isn't serializable, or already skips its initial run, is
|
|
22
|
+
* returned as it is.
|
|
23
|
+
*
|
|
24
|
+
* **Gotchas**
|
|
25
|
+
*
|
|
26
|
+
* The copy keeps the hydrated value only for the first read: a refresh, or a
|
|
27
|
+
* change to a dependency, runs the effect as usual. An `Initial` value never
|
|
28
|
+
* skips the effect, which is why a result the server was still loading
|
|
29
|
+
* crosses as a promise that `useAtomResult` waits for.
|
|
30
|
+
*
|
|
31
|
+
* **Example**
|
|
32
|
+
*
|
|
33
|
+
* ```ts
|
|
34
|
+
* import { useAtomValue } from "@effect/atom-react"
|
|
35
|
+
* import { Hydration } from "raf-effect-atom"
|
|
36
|
+
*
|
|
37
|
+
* const result = useAtomValue(Hydration.hydrated(issue(1)))
|
|
38
|
+
* ```
|
|
39
|
+
*
|
|
40
|
+
* @see {@link Atom.serializable} for the metadata it copies
|
|
41
|
+
*
|
|
42
|
+
* @stability unstable
|
|
43
|
+
* @category combinators
|
|
44
|
+
* @since 0.1.0
|
|
45
|
+
*/
|
|
46
|
+
export declare const hydrated: <Self extends Atom.Atom<unknown>>(atom: Self) => Self;
|
|
47
|
+
/**
|
|
48
|
+
* Encodes a request registry's serializable atoms for the browser.
|
|
49
|
+
*
|
|
50
|
+
* **Details**
|
|
51
|
+
*
|
|
52
|
+
* `Hydration.dehydrate` with `encodeInitialAs: "promise"`: a settled result
|
|
53
|
+
* crosses as its encoded value, and a result still loading crosses as a
|
|
54
|
+
* promise of its encoded value, which resolves when it settles. The
|
|
55
|
+
* framework's serializer carries the promises (TanStack Router's dehydrated
|
|
56
|
+
* state, React's server component props, React Router's Single Fetch).
|
|
57
|
+
*
|
|
58
|
+
* **Gotchas**
|
|
59
|
+
*
|
|
60
|
+
* Only atoms in the registry when it's called are included, so call it once
|
|
61
|
+
* the loaders have started theirs.
|
|
62
|
+
*
|
|
63
|
+
* @see {@link hydrate} for the browser side
|
|
64
|
+
*
|
|
65
|
+
* @stability unstable
|
|
66
|
+
* @category dehydration
|
|
67
|
+
* @since 0.1.0
|
|
68
|
+
*/
|
|
69
|
+
export declare const dehydrate: (registry: AtomRegistry.AtomRegistry) => Array<EffectHydration.DehydratedAtom>;
|
|
70
|
+
/**
|
|
71
|
+
* Applies dehydrated state to the browser's registry.
|
|
72
|
+
*
|
|
73
|
+
* **When to use**
|
|
74
|
+
*
|
|
75
|
+
* Use outside React, such as in a router's `hydrate` hook, before the app
|
|
76
|
+
* renders. Inside React, use `HydrationBoundary`.
|
|
77
|
+
*
|
|
78
|
+
* **Details**
|
|
79
|
+
*
|
|
80
|
+
* Settled values go through `Hydration.hydrate`. A streamed result is held
|
|
81
|
+
* until its promise resolves, then hydrated; until then `useAtomResult`
|
|
82
|
+
* suspends on it instead of starting the atom, which would request it again.
|
|
83
|
+
* A streamed result that can't be decoded, or whose promise rejects, is
|
|
84
|
+
* dropped, and the atom runs in the browser.
|
|
85
|
+
*
|
|
86
|
+
* @see {@link dehydrate} for the server side
|
|
87
|
+
*
|
|
88
|
+
* @stability unstable
|
|
89
|
+
* @category hydration
|
|
90
|
+
* @since 0.1.0
|
|
91
|
+
*/
|
|
92
|
+
export declare const hydrate: (registry: AtomRegistry.AtomRegistry, state: Iterable<EffectHydration.DehydratedAtom>) => void;
|
|
93
|
+
export { Hydration_d_exports };
|