raf-effect-atom 0.0.0-stage → 0.0.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/LICENSE +21 -0
- package/README.md +59 -2
- package/dist/Hooks.d.mts +68 -0
- package/dist/Hooks.mjs +119 -0
- package/dist/Hydration.d.mts +93 -0
- package/dist/Hydration.mjs +134 -0
- package/dist/Loader.d.mts +157 -0
- package/dist/Loader.mjs +232 -0
- package/dist/Next.d.mts +66 -0
- package/dist/Next.mjs +63 -0
- package/dist/NextClient.d.mts +33 -0
- package/dist/NextClient.mjs +45 -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 +98 -0
- package/dist/Waterfalls.mjs +107 -0
- package/dist/_virtual/_rolldown/runtime.mjs +12 -0
- package/dist/index.d.mts +7 -0
- package/dist/index.mjs +7 -0
- package/dist/internal/development.mjs +6 -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/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,60 @@
|
|
|
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. `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:
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
// issue: (id: number) => an AtomRpc query with a serializationKey
|
|
24
|
+
export async function loader(args: LoaderArgs) {
|
|
25
|
+
const id = Number(args.params.id);
|
|
26
|
+
args.context.atoms.prefetch(comments(id));
|
|
27
|
+
const result = await args.context.atoms.ensure(issue(id));
|
|
28
|
+
if (AsyncResult.isFailure(result)) {
|
|
29
|
+
// result.cause holds IssueNotFound | Unauthorized | RpcClientError
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
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:
|
|
35
|
+
|
|
36
|
+
```tsx
|
|
37
|
+
import { useAtomResult } from "raf-effect-atom";
|
|
38
|
+
|
|
39
|
+
function IssueTitle(props: { readonly id: number }) {
|
|
40
|
+
const result = useAtomResult(useIssueBoundary().wrap(issue(props.id)));
|
|
41
|
+
// Success<Issue, never>: its boundaries claim every failure
|
|
42
|
+
return <h1>{result.value.title}</h1>;
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Each framework has a subpath that gives every server request its own registry and sends it to the browser:
|
|
47
|
+
|
|
48
|
+
- `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.
|
|
51
|
+
|
|
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.
|
|
53
|
+
|
|
54
|
+
## For coding agents
|
|
55
|
+
|
|
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).
|
|
57
|
+
|
|
58
|
+
## License
|
|
59
|
+
|
|
60
|
+
MIT
|
package/dist/Hooks.d.mts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { SettledResult } from "./Loader.mjs";
|
|
2
|
+
import * as Atom from "effect/reactivity/Atom";
|
|
3
|
+
import * as AsyncResult from "effect/reactivity/AsyncResult";
|
|
4
|
+
/**
|
|
5
|
+
* Reads an `AsyncResult` atom through React Suspense: a result streamed from
|
|
6
|
+
* the server, a hydrated one, or one a loader started.
|
|
7
|
+
*
|
|
8
|
+
* **When to use**
|
|
9
|
+
*
|
|
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.
|
|
13
|
+
*
|
|
14
|
+
* **Details**
|
|
15
|
+
*
|
|
16
|
+
* - A result the server was still loading when it dehydrated is awaited
|
|
17
|
+
* instead of started, so the browser doesn't request it a second time.
|
|
18
|
+
* - The atom is read through `hydrated`, so a hydrated result is kept.
|
|
19
|
+
* - Once the component mounts, the loader's lease ends and the component
|
|
20
|
+
* keeps the atom mounted.
|
|
21
|
+
* - A derived atom is read through the first serializable atom its
|
|
22
|
+
* `initialValueTarget` chain points at: that one is awaited, hydrated and
|
|
23
|
+
* released, so the derived atom doesn't request it again.
|
|
24
|
+
* - The settled result is returned, typed failures included. A defect is
|
|
25
|
+
* thrown to the error boundaries above. With no typed failures left, such
|
|
26
|
+
* as an atom whose boundaries claim all of them, it's typed as a success.
|
|
27
|
+
* - While a failed atom refreshes, such as after a boundary's `reset`, the
|
|
28
|
+
* read suspends instead of returning the old failure again.
|
|
29
|
+
*
|
|
30
|
+
* **Gotchas**
|
|
31
|
+
*
|
|
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.
|
|
39
|
+
*
|
|
40
|
+
* During server rendering, the registry must be a request registry (see
|
|
41
|
+
* `RequestRegistry.make`), or a module-level registry would share results
|
|
42
|
+
* between requests: in development, the hook throws without one.
|
|
43
|
+
*
|
|
44
|
+
* `Atom.transform`'s `initialValueTarget` option points at the end of the
|
|
45
|
+
* target's own chain, which for an AtomRpc query with `reactivityKeys` is an
|
|
46
|
+
* inner atom with no serialization key. A derived atom read here sets
|
|
47
|
+
* `initialValueTarget` to its source itself, as an error boundary's
|
|
48
|
+
* wrapper does, or it requests the source again after hydration.
|
|
49
|
+
*
|
|
50
|
+
* **Example**
|
|
51
|
+
*
|
|
52
|
+
* ```tsx
|
|
53
|
+
* import { AsyncResult } from "effect/reactivity"
|
|
54
|
+
* import { useAtomResult } from "raf-effect-atom"
|
|
55
|
+
*
|
|
56
|
+
* function IssueTitle(props: { readonly id: number }) {
|
|
57
|
+
* const result = useAtomResult(issue(props.id))
|
|
58
|
+
* return AsyncResult.isSuccess(result) ? <h1>{result.value.title}</h1> : null
|
|
59
|
+
* }
|
|
60
|
+
* ```
|
|
61
|
+
*
|
|
62
|
+
* @see {@link hydrated} for reading hydrated atoms with Atom's own hooks
|
|
63
|
+
*
|
|
64
|
+
* @stability unstable
|
|
65
|
+
* @category hooks
|
|
66
|
+
* @since 0.1.0
|
|
67
|
+
*/
|
|
68
|
+
export declare const useAtomResult: <A, E>(atom: Atom.Atom<AsyncResult.AsyncResult<A, E>>) => [E] extends [never] ? AsyncResult.Success<A, never> : SettledResult<A, E>;
|
package/dist/Hooks.mjs
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
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 { isRequestRegistry } from "./RequestRegistry.mjs";
|
|
7
|
+
import { isServer } from "./internal/registry.mjs";
|
|
8
|
+
import { ModeContext, check } from "./internal/waterfalls.mjs";
|
|
9
|
+
import * as Atom from "effect/reactivity/Atom";
|
|
10
|
+
import * as Predicate from "effect/Predicate";
|
|
11
|
+
import * as Cause from "effect/Cause";
|
|
12
|
+
import * as Effect from "effect/Effect";
|
|
13
|
+
import * as Option from "effect/Option";
|
|
14
|
+
import * as AsyncResult from "effect/reactivity/AsyncResult";
|
|
15
|
+
import { RegistryContext, useAtomSuspense } from "@effect/atom-react";
|
|
16
|
+
import * as React from "react";
|
|
17
|
+
// src/Hooks.ts
|
|
18
|
+
/**
|
|
19
|
+
* The read hook for atoms started by loaders and streamed from the server.
|
|
20
|
+
*
|
|
21
|
+
* @stability unstable
|
|
22
|
+
* @since 0.1.0
|
|
23
|
+
*/
|
|
24
|
+
/**
|
|
25
|
+
* The atom the loader started and the server sent: the first serializable
|
|
26
|
+
* atom in `atom`'s `initialValueTarget` chain, starting with `atom`. An
|
|
27
|
+
* AtomRpc query is serializable itself, though it points at an inner atom.
|
|
28
|
+
*/
|
|
29
|
+
const sourceOf = (atom) => {
|
|
30
|
+
for (let target = atom; target !== void 0; target = target.initialValueTarget) if (Atom.isSerializable(target)) return target;
|
|
31
|
+
return atom;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Reads an `AsyncResult` atom through React Suspense: a result streamed from
|
|
35
|
+
* the server, a hydrated one, or one a loader started.
|
|
36
|
+
*
|
|
37
|
+
* **When to use**
|
|
38
|
+
*
|
|
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.
|
|
42
|
+
*
|
|
43
|
+
* **Details**
|
|
44
|
+
*
|
|
45
|
+
* - A result the server was still loading when it dehydrated is awaited
|
|
46
|
+
* instead of started, so the browser doesn't request it a second time.
|
|
47
|
+
* - The atom is read through `hydrated`, so a hydrated result is kept.
|
|
48
|
+
* - Once the component mounts, the loader's lease ends and the component
|
|
49
|
+
* keeps the atom mounted.
|
|
50
|
+
* - A derived atom is read through the first serializable atom its
|
|
51
|
+
* `initialValueTarget` chain points at: that one is awaited, hydrated and
|
|
52
|
+
* released, so the derived atom doesn't request it again.
|
|
53
|
+
* - The settled result is returned, typed failures included. A defect is
|
|
54
|
+
* thrown to the error boundaries above. With no typed failures left, such
|
|
55
|
+
* as an atom whose boundaries claim all of them, it's typed as a success.
|
|
56
|
+
* - While a failed atom refreshes, such as after a boundary's `reset`, the
|
|
57
|
+
* read suspends instead of returning the old failure again.
|
|
58
|
+
*
|
|
59
|
+
* **Gotchas**
|
|
60
|
+
*
|
|
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.
|
|
68
|
+
*
|
|
69
|
+
* During server rendering, the registry must be a request registry (see
|
|
70
|
+
* `RequestRegistry.make`), or a module-level registry would share results
|
|
71
|
+
* between requests: in development, the hook throws without one.
|
|
72
|
+
*
|
|
73
|
+
* `Atom.transform`'s `initialValueTarget` option points at the end of the
|
|
74
|
+
* target's own chain, which for an AtomRpc query with `reactivityKeys` is an
|
|
75
|
+
* inner atom with no serialization key. A derived atom read here sets
|
|
76
|
+
* `initialValueTarget` to its source itself, as an error boundary's
|
|
77
|
+
* wrapper does, or it requests the source again after hydration.
|
|
78
|
+
*
|
|
79
|
+
* **Example**
|
|
80
|
+
*
|
|
81
|
+
* ```tsx
|
|
82
|
+
* import { AsyncResult } from "effect/reactivity"
|
|
83
|
+
* import { useAtomResult } from "raf-effect-atom"
|
|
84
|
+
*
|
|
85
|
+
* function IssueTitle(props: { readonly id: number }) {
|
|
86
|
+
* const result = useAtomResult(issue(props.id))
|
|
87
|
+
* return AsyncResult.isSuccess(result) ? <h1>{result.value.title}</h1> : null
|
|
88
|
+
* }
|
|
89
|
+
* ```
|
|
90
|
+
*
|
|
91
|
+
* @see {@link hydrated} for reading hydrated atoms with Atom's own hooks
|
|
92
|
+
*
|
|
93
|
+
* @stability unstable
|
|
94
|
+
* @category hooks
|
|
95
|
+
* @since 0.1.0
|
|
96
|
+
*/
|
|
97
|
+
const useAtomResult = (atom) => {
|
|
98
|
+
const registry = React.useContext(RegistryContext);
|
|
99
|
+
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
|
+
const target = sourceOf(atom);
|
|
101
|
+
const source = hydrated(target);
|
|
102
|
+
const read = target === atom ? hydrated(atom) : atom;
|
|
103
|
+
const key = keyOf(source);
|
|
104
|
+
const streamed = Predicate.isString(key) ? stream(registry, key) : void 0;
|
|
105
|
+
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);
|
|
108
|
+
const current = registry.get(read);
|
|
109
|
+
const result = useAtomSuspense(read, {
|
|
110
|
+
includeFailure: true,
|
|
111
|
+
suspendOnWaiting: AsyncResult.isFailure(current) && current.waiting
|
|
112
|
+
});
|
|
113
|
+
React.useEffect(() => {
|
|
114
|
+
release(registry, source);
|
|
115
|
+
}, [registry, source]);
|
|
116
|
+
if (AsyncResult.isFailure(result) && Option.isNone(Cause.findErrorOption(result.cause))) throw Cause.squash(result.cause);
|
|
117
|
+
return result;
|
|
118
|
+
};
|
|
119
|
+
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 };
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { __exportAll } from "./_virtual/_rolldown/runtime.mjs";
|
|
2
|
+
import { split } from "./internal/pending.mjs";
|
|
3
|
+
import { remember } from "./internal/sources.mjs";
|
|
4
|
+
import * as Atom from "effect/reactivity/Atom";
|
|
5
|
+
import * as EffectHydration from "effect/reactivity/Hydration";
|
|
6
|
+
// src/Hydration.ts
|
|
7
|
+
/**
|
|
8
|
+
* Streams a server registry into the browser's, typed failures intact, and
|
|
9
|
+
* hydrates it without requesting anything again.
|
|
10
|
+
*
|
|
11
|
+
* `dehydrate` encodes the request registry's serializable atoms. Settled
|
|
12
|
+
* results cross as values; results still loading cross as promises that
|
|
13
|
+
* resolve when they settle, carried by the framework's own streaming.
|
|
14
|
+
* `hydrate` applies settled values at once and holds each streamed result
|
|
15
|
+
* until its promise resolves, so `useAtomResult` waits for it instead of
|
|
16
|
+
* starting the atom. `hydrated` keeps a hydrated value on the atom's first
|
|
17
|
+
* read instead of running its effect again.
|
|
18
|
+
*
|
|
19
|
+
* Typed failures survive because a serializable `AsyncResult` atom, such as
|
|
20
|
+
* an `AtomRpc` query with a `serializationKey`, encodes its `Cause` with the
|
|
21
|
+
* procedure's error Schemas: a server-side `IssueNotFound` decodes as an
|
|
22
|
+
* `IssueNotFound` in the browser.
|
|
23
|
+
*
|
|
24
|
+
* @stability unstable
|
|
25
|
+
* @since 0.1.0
|
|
26
|
+
*/
|
|
27
|
+
var Hydration_exports = /* @__PURE__ */ __exportAll({
|
|
28
|
+
dehydrate: () => dehydrate,
|
|
29
|
+
hydrate: () => hydrate,
|
|
30
|
+
hydrated: () => hydrated
|
|
31
|
+
});
|
|
32
|
+
const copies = /* @__PURE__ */ new WeakMap();
|
|
33
|
+
/**
|
|
34
|
+
* Returns a serializable atom that keeps its hydrated value on its first
|
|
35
|
+
* read, instead of running its effect again.
|
|
36
|
+
*
|
|
37
|
+
* **When to use**
|
|
38
|
+
*
|
|
39
|
+
* The loader verbs and `useAtomResult` already read through it. Use it when
|
|
40
|
+
* reading a hydrated atom with `@effect/atom-react`'s own hooks.
|
|
41
|
+
*
|
|
42
|
+
* **Details**
|
|
43
|
+
*
|
|
44
|
+
* The copy shares the atom's serialization key, so both read the same
|
|
45
|
+
* registry node. It sets `skipInitialRun` on the atom's `Serializable`
|
|
46
|
+
* metadata, and is memoized per atom, so every call returns the same copy.
|
|
47
|
+
* An atom that isn't serializable, or already skips its initial run, is
|
|
48
|
+
* returned as it is.
|
|
49
|
+
*
|
|
50
|
+
* **Gotchas**
|
|
51
|
+
*
|
|
52
|
+
* The copy keeps the hydrated value only for the first read: a refresh, or a
|
|
53
|
+
* change to a dependency, runs the effect as usual. An `Initial` value never
|
|
54
|
+
* skips the effect, which is why a result the server was still loading
|
|
55
|
+
* crosses as a promise that `useAtomResult` waits for.
|
|
56
|
+
*
|
|
57
|
+
* **Example**
|
|
58
|
+
*
|
|
59
|
+
* ```ts
|
|
60
|
+
* import { useAtomValue } from "@effect/atom-react"
|
|
61
|
+
* import { Hydration } from "raf-effect-atom"
|
|
62
|
+
*
|
|
63
|
+
* const result = useAtomValue(Hydration.hydrated(issue(1)))
|
|
64
|
+
* ```
|
|
65
|
+
*
|
|
66
|
+
* @see {@link Atom.serializable} for the metadata it copies
|
|
67
|
+
*
|
|
68
|
+
* @stability unstable
|
|
69
|
+
* @category combinators
|
|
70
|
+
* @since 0.1.0
|
|
71
|
+
*/
|
|
72
|
+
const hydrated = (atom) => {
|
|
73
|
+
if (!Atom.isSerializable(atom) || atom[Atom.SerializableTypeId].skipInitialRun === true) return atom;
|
|
74
|
+
const cached = copies.get(atom);
|
|
75
|
+
if (cached !== void 0) return cached;
|
|
76
|
+
const copy = Object.assign(Object.create(Object.getPrototypeOf(atom)), {
|
|
77
|
+
...atom,
|
|
78
|
+
[Atom.SerializableTypeId]: {
|
|
79
|
+
...atom[Atom.SerializableTypeId],
|
|
80
|
+
skipInitialRun: true
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
copies.set(atom, copy);
|
|
84
|
+
return copy;
|
|
85
|
+
};
|
|
86
|
+
/**
|
|
87
|
+
* Encodes a request registry's serializable atoms for the browser.
|
|
88
|
+
*
|
|
89
|
+
* **Details**
|
|
90
|
+
*
|
|
91
|
+
* `Hydration.dehydrate` with `encodeInitialAs: "promise"`: a settled result
|
|
92
|
+
* crosses as its encoded value, and a result still loading crosses as a
|
|
93
|
+
* promise of its encoded value, which resolves when it settles. The
|
|
94
|
+
* framework's serializer carries the promises (TanStack Router's dehydrated
|
|
95
|
+
* state, React's server component props, React Router's Single Fetch).
|
|
96
|
+
*
|
|
97
|
+
* **Gotchas**
|
|
98
|
+
*
|
|
99
|
+
* Only atoms in the registry when it's called are included, so call it once
|
|
100
|
+
* the loaders have started theirs.
|
|
101
|
+
*
|
|
102
|
+
* @see {@link hydrate} for the browser side
|
|
103
|
+
*
|
|
104
|
+
* @stability unstable
|
|
105
|
+
* @category dehydration
|
|
106
|
+
* @since 0.1.0
|
|
107
|
+
*/
|
|
108
|
+
const dehydrate = (registry) => remember(EffectHydration.dehydrate(registry, { encodeInitialAs: "promise" }), registry);
|
|
109
|
+
/**
|
|
110
|
+
* Applies dehydrated state to the browser's registry.
|
|
111
|
+
*
|
|
112
|
+
* **When to use**
|
|
113
|
+
*
|
|
114
|
+
* Use outside React, such as in a router's `hydrate` hook, before the app
|
|
115
|
+
* renders. Inside React, use `HydrationBoundary`.
|
|
116
|
+
*
|
|
117
|
+
* **Details**
|
|
118
|
+
*
|
|
119
|
+
* Settled values go through `Hydration.hydrate`. A streamed result is held
|
|
120
|
+
* until its promise resolves, then hydrated; until then `useAtomResult`
|
|
121
|
+
* suspends on it instead of starting the atom, which would request it again.
|
|
122
|
+
* A streamed result that can't be decoded, or whose promise rejects, is
|
|
123
|
+
* dropped, and the atom runs in the browser.
|
|
124
|
+
*
|
|
125
|
+
* @see {@link dehydrate} for the server side
|
|
126
|
+
*
|
|
127
|
+
* @stability unstable
|
|
128
|
+
* @category hydration
|
|
129
|
+
* @since 0.1.0
|
|
130
|
+
*/
|
|
131
|
+
const hydrate = (registry, state) => {
|
|
132
|
+
EffectHydration.hydrate(registry, split(registry, state));
|
|
133
|
+
};
|
|
134
|
+
export { Hydration_exports, dehydrate, hydrate, hydrated };
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import * as Atom from "effect/reactivity/Atom";
|
|
2
|
+
import * as Context from "effect/Context";
|
|
3
|
+
import * as Duration from "effect/Duration";
|
|
4
|
+
import * as Effect from "effect/Effect";
|
|
5
|
+
import * as AsyncResult from "effect/reactivity/AsyncResult";
|
|
6
|
+
import * as AtomRegistry from "effect/reactivity/AtomRegistry";
|
|
7
|
+
declare namespace Loader_d_exports {
|
|
8
|
+
export { LeaseTTL, Loader, SettledResult, ensure, fetch, make, prefetch };
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* A settled `AsyncResult`: what `ensure` and `fetch` return.
|
|
12
|
+
*
|
|
13
|
+
* @stability unstable
|
|
14
|
+
* @category models
|
|
15
|
+
* @since 0.1.0
|
|
16
|
+
*/
|
|
17
|
+
export type SettledResult<A, E> = AsyncResult.Failure<A, E> | AsyncResult.Success<A, E>;
|
|
18
|
+
/**
|
|
19
|
+
* How long a loader's lease keeps an atom mounted when no component takes it
|
|
20
|
+
* over. Default 30 seconds.
|
|
21
|
+
*
|
|
22
|
+
* **Details**
|
|
23
|
+
*
|
|
24
|
+
* A lease ends when `useAtomResult` mounts the atom, or when this time has
|
|
25
|
+
* passed, after which the registry's idle TTL applies as usual. On a request
|
|
26
|
+
* registry, leases last until the registry is disposed.
|
|
27
|
+
*
|
|
28
|
+
* @stability unstable
|
|
29
|
+
* @category references
|
|
30
|
+
* @since 0.1.0
|
|
31
|
+
*/
|
|
32
|
+
export declare const LeaseTTL: Context.Reference<Duration.Duration>;
|
|
33
|
+
/**
|
|
34
|
+
* Starts an atom without waiting for it.
|
|
35
|
+
*
|
|
36
|
+
* **When to use**
|
|
37
|
+
*
|
|
38
|
+
* Use for data the route needs but its loader doesn't: it streams in while
|
|
39
|
+
* the route renders.
|
|
40
|
+
*
|
|
41
|
+
* **Details**
|
|
42
|
+
*
|
|
43
|
+
* Starts the atom if it isn't in the registry, and leaves it alone if it is.
|
|
44
|
+
* Takes a lease either way.
|
|
45
|
+
*
|
|
46
|
+
* **Example**
|
|
47
|
+
*
|
|
48
|
+
* ```ts
|
|
49
|
+
* import { Effect } from "effect"
|
|
50
|
+
* import { Loader } from "raf-effect-atom"
|
|
51
|
+
*
|
|
52
|
+
* const program = Loader.prefetch(comments(1))
|
|
53
|
+
* ```
|
|
54
|
+
*
|
|
55
|
+
* @stability unstable
|
|
56
|
+
* @category verbs
|
|
57
|
+
* @since 0.1.0
|
|
58
|
+
*/
|
|
59
|
+
export declare const prefetch: <A, E>(atom: Atom.Atom<AsyncResult.AsyncResult<A, E>>) => Effect.Effect<void, never, AtomRegistry.AtomRegistry>;
|
|
60
|
+
/**
|
|
61
|
+
* Starts an atom and returns its settled result, waiting only while it has
|
|
62
|
+
* none.
|
|
63
|
+
*
|
|
64
|
+
* **When to use**
|
|
65
|
+
*
|
|
66
|
+
* Use when the loader needs the result, such as to redirect on a tag, or
|
|
67
|
+
* when the route shouldn't render without it.
|
|
68
|
+
*
|
|
69
|
+
* **Details**
|
|
70
|
+
*
|
|
71
|
+
* An atom with a result returns it at once, without refetching. On a request
|
|
72
|
+
* registry, it also waits out a refresh in flight, so the server never
|
|
73
|
+
* renders a result that's about to change. A typed failure is returned as
|
|
74
|
+
* the result; a defect or an interruption fails the Effect.
|
|
75
|
+
*
|
|
76
|
+
* **Example**
|
|
77
|
+
*
|
|
78
|
+
* ```ts
|
|
79
|
+
* import { Effect } from "effect"
|
|
80
|
+
* import { AsyncResult } from "effect/reactivity"
|
|
81
|
+
* import { Loader } from "raf-effect-atom"
|
|
82
|
+
*
|
|
83
|
+
* const program = Effect.gen(function* () {
|
|
84
|
+
* const result = yield* Loader.ensure(issue(1))
|
|
85
|
+
* return AsyncResult.isSuccess(result) ? result.value.title : "missing"
|
|
86
|
+
* })
|
|
87
|
+
* ```
|
|
88
|
+
*
|
|
89
|
+
* @stability unstable
|
|
90
|
+
* @category verbs
|
|
91
|
+
* @since 0.1.0
|
|
92
|
+
*/
|
|
93
|
+
export declare const ensure: <A, E>(atom: Atom.Atom<AsyncResult.AsyncResult<A, E>>) => Effect.Effect<SettledResult<A, E>, never, AtomRegistry.AtomRegistry>;
|
|
94
|
+
/**
|
|
95
|
+
* Starts an atom, or refreshes it if it has a result, and returns the new
|
|
96
|
+
* settled result.
|
|
97
|
+
*
|
|
98
|
+
* **When to use**
|
|
99
|
+
*
|
|
100
|
+
* Use when the loader must not see a result from before it ran, such as
|
|
101
|
+
* after a mutation.
|
|
102
|
+
*
|
|
103
|
+
* **Details**
|
|
104
|
+
*
|
|
105
|
+
* Waits until the result settles and isn't refreshing. A typed failure is
|
|
106
|
+
* returned as the result; a defect or an interruption fails the Effect.
|
|
107
|
+
*
|
|
108
|
+
* @stability unstable
|
|
109
|
+
* @category verbs
|
|
110
|
+
* @since 0.1.0
|
|
111
|
+
*/
|
|
112
|
+
export declare const fetch: <A, E>(atom: Atom.Atom<AsyncResult.AsyncResult<A, E>>) => Effect.Effect<SettledResult<A, E>, never, AtomRegistry.AtomRegistry>;
|
|
113
|
+
/**
|
|
114
|
+
* The loader verbs as Promises over one registry, as the framework
|
|
115
|
+
* integrations put them on the router context.
|
|
116
|
+
*
|
|
117
|
+
* @stability unstable
|
|
118
|
+
* @category models
|
|
119
|
+
* @since 0.1.0
|
|
120
|
+
*/
|
|
121
|
+
export interface Loader {
|
|
122
|
+
/** {@link ensure}, as a Promise that rejects only on a defect. */
|
|
123
|
+
readonly ensure: <A, E>(atom: Atom.Atom<AsyncResult.AsyncResult<A, E>>) => Promise<SettledResult<A, E>>;
|
|
124
|
+
/** {@link fetch}, as a Promise that rejects only on a defect. */
|
|
125
|
+
readonly fetch: <A, E>(atom: Atom.Atom<AsyncResult.AsyncResult<A, E>>) => Promise<SettledResult<A, E>>;
|
|
126
|
+
/** {@link prefetch}, run at once. */
|
|
127
|
+
readonly prefetch: <A, E>(atom: Atom.Atom<AsyncResult.AsyncResult<A, E>>) => void;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Creates the Promise verbs over one registry.
|
|
131
|
+
*
|
|
132
|
+
* **When to use**
|
|
133
|
+
*
|
|
134
|
+
* Use in a framework integration, in loaders that are async functions, or
|
|
135
|
+
* in an event handler that starts an atom before the state change that
|
|
136
|
+
* renders its reader, for state that can't be in the URL, such as a
|
|
137
|
+
* search-as-you-type query. In Effect code, use
|
|
138
|
+
* the verbs directly with the `AtomRegistry` provided.
|
|
139
|
+
*
|
|
140
|
+
* **Example**
|
|
141
|
+
*
|
|
142
|
+
* ```ts
|
|
143
|
+
* import { Loader, RequestRegistry } from "raf-effect-atom"
|
|
144
|
+
*
|
|
145
|
+
* const loader = Loader.make(RequestRegistry.make())
|
|
146
|
+
* loader.prefetch(comments(1))
|
|
147
|
+
* const result = await loader.ensure(issue(1))
|
|
148
|
+
* ```
|
|
149
|
+
*
|
|
150
|
+
* @stability unstable
|
|
151
|
+
* @category constructors
|
|
152
|
+
* @since 0.1.0
|
|
153
|
+
*/
|
|
154
|
+
export declare const make: (registry: AtomRegistry.AtomRegistry, options?: {
|
|
155
|
+
readonly leaseTTL?: Duration.Input | undefined;
|
|
156
|
+
}) => Loader;
|
|
157
|
+
export { Loader_d_exports };
|