raf-effect-atom 0.2.0 → 0.2.2
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 +23 -2
- package/README.md +1 -1
- package/dist/Hooks.mjs +2 -5
- package/dist/Loader.d.mts +29 -7
- package/dist/Loader.mjs +16 -5
- package/dist/Next.d.mts +3 -1
- package/dist/Next.mjs +3 -1
- package/dist/ReactHydration.d.mts +6 -0
- package/dist/ReactHydration.mjs +8 -1
- package/dist/ReactRouter.d.mts +53 -3
- package/dist/ReactRouter.mjs +72 -7
- package/dist/RegistryProvider.d.mts +3 -0
- package/dist/RegistryProvider.mjs +5 -1
- package/dist/internal/next.mjs +5 -2
- package/dist/internal/registry.mjs +26 -2
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,14 +1,35 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.2.2](https://github.com/KATT/raf-effect-atom/compare/raf-effect-atom%400.2.1...raf-effect-atom%400.2.2) (2026-10-23)
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
* **raf-effect-atom:** stop a loader's wait when its request is aborted ([b1f2f16](https://github.com/KATT/raf-effect-atom/commit/b1f2f16fcce9cd5dd1ba18fa653b40c805eebd93)), references [#41](https://github.com/KATT/raf-effect-atom/issues/41) [#40](https://github.com/KATT/raf-effect-atom/issues/40) [#40](https://github.com/KATT/raf-effect-atom/issues/40) [#41](https://github.com/KATT/raf-effect-atom/issues/41)
|
|
8
|
+
|
|
9
|
+
### Bug Fixes
|
|
10
|
+
|
|
11
|
+
* **raf-effect-atom:** refuse to hydrate a shared registry during server rendering ([2f793a6](https://github.com/KATT/raf-effect-atom/commit/2f793a63627482bdf2f0e2891829f5191077cfc7))
|
|
12
|
+
|
|
13
|
+
### Documentation
|
|
14
|
+
|
|
15
|
+
* **raf-effect-atom:** date clientAtomsMiddleware to 0.2.1, the release it shipped in ([c2a4288](https://github.com/KATT/raf-effect-atom/commit/c2a4288f4c5b63575e019712edc9fdc49a052bd2))
|
|
16
|
+
* record that the packages' module state is keyed by what one request owns ([22b23f9](https://github.com/KATT/raf-effect-atom/commit/22b23f947544f8c4459ef172e8b21499403059c4))
|
|
17
|
+
|
|
18
|
+
## [0.2.1](https://github.com/KATT/raf-effect-atom/compare/raf-effect-atom%400.2.0...raf-effect-atom%400.2.1) (2026-10-23)
|
|
19
|
+
|
|
20
|
+
### Features
|
|
21
|
+
|
|
22
|
+
- **raf-effect-atom:** run React Router loaders in the browser after the first page ([bee00a9](https://github.com/KATT/raf-effect-atom/commit/bee00a981fd24fcdc9d746053dcd73dbc7ebc853)), references [#41](https://github.com/KATT/raf-effect-atom/issues/41) [#41](https://github.com/KATT/raf-effect-atom/issues/41)
|
|
23
|
+
|
|
3
24
|
## [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
25
|
|
|
5
26
|
### ⚠ BREAKING CHANGES
|
|
6
27
|
|
|
7
|
-
|
|
28
|
+
- **raf-effect-atom:** `dehydrate()` from `raf-effect-atom/next` returns a
|
|
8
29
|
|
|
9
30
|
### Features
|
|
10
31
|
|
|
11
|
-
|
|
32
|
+
- **raf-effect-atom:** seed each request registry, async seeds included ([62b6180](https://github.com/KATT/raf-effect-atom/commit/62b618098df87e711ddb4c1d633e191ba3353438))
|
|
12
33
|
|
|
13
34
|
## 0.1.0 (2026-10-23)
|
|
14
35
|
|
package/README.md
CHANGED
|
@@ -53,7 +53,7 @@ Each framework has a subpath that gives every server request its own registry an
|
|
|
53
53
|
|
|
54
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
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.
|
|
56
|
+
- `raf-effect-atom/react-router`, for Framework Mode: `atomsMiddleware(atomsContext)` as the root middleware and `clientAtomsMiddleware(atomsContext)` as its client middleware, loaders that return `atoms.dehydrate()` with their handles and double as client loaders, and `HydrationBoundary` in the route component.
|
|
57
57
|
|
|
58
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
59
|
|
package/dist/Hooks.mjs
CHANGED
|
@@ -1,16 +1,13 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { stream } from "./internal/pending.mjs";
|
|
3
3
|
import { hydrated } from "./Hydration.mjs";
|
|
4
|
-
import { isDevelopment } from "./internal/development.mjs";
|
|
5
4
|
import { keyOf, release } from "./internal/leases.mjs";
|
|
6
5
|
import { atomOf } from "./internal/handles.mjs";
|
|
7
|
-
import {
|
|
8
|
-
import { isServer } from "./internal/registry.mjs";
|
|
6
|
+
import { requireRequestRegistry } from "./internal/registry.mjs";
|
|
9
7
|
import { ModeContext, check } from "./internal/waterfalls.mjs";
|
|
10
8
|
import * as Atom from "effect/reactivity/Atom";
|
|
11
9
|
import * as Predicate from "effect/Predicate";
|
|
12
10
|
import * as Cause from "effect/Cause";
|
|
13
|
-
import * as Effect from "effect/Effect";
|
|
14
11
|
import * as Option from "effect/Option";
|
|
15
12
|
import * as AsyncResult from "effect/reactivity/AsyncResult";
|
|
16
13
|
import { RegistryContext, useAtomSuspense } from "@effect/atom-react";
|
|
@@ -111,7 +108,7 @@ const sourceOf = (atom) => {
|
|
|
111
108
|
const useAtomResult = (source, handle) => {
|
|
112
109
|
const registry = React.useContext(RegistryContext);
|
|
113
110
|
const atom = atomOf(source, handle.args);
|
|
114
|
-
|
|
111
|
+
requireRequestRegistry(registry, "useAtomResult");
|
|
115
112
|
const target = sourceOf(atom);
|
|
116
113
|
const started = hydrated(target);
|
|
117
114
|
const read = target === atom ? hydrated(atom) : atom;
|
package/dist/Loader.d.mts
CHANGED
|
@@ -5,7 +5,7 @@ import * as Effect from "effect/Effect";
|
|
|
5
5
|
import * as AsyncResult from "effect/reactivity/AsyncResult";
|
|
6
6
|
import * as AtomRegistry from "effect/reactivity/AtomRegistry";
|
|
7
7
|
declare namespace Loader_d_exports {
|
|
8
|
-
export { LeaseTTL, Loader, SettledResult, ensure, fetch, make, prefetch, result };
|
|
8
|
+
export { LeaseTTL, Loader, MakeOptions, SettledResult, ensure, fetch, make, prefetch, result };
|
|
9
9
|
}
|
|
10
10
|
/**
|
|
11
11
|
* A settled `AsyncResult`: what `ensure` and `fetch` return.
|
|
@@ -166,6 +166,23 @@ export interface Loader {
|
|
|
166
166
|
*/
|
|
167
167
|
readonly prefetch: <S extends Source>(source: S, ...args: ArgsOf<S>) => Handle<S>;
|
|
168
168
|
}
|
|
169
|
+
/**
|
|
170
|
+
* Options for `make`.
|
|
171
|
+
*
|
|
172
|
+
* @stability unstable
|
|
173
|
+
* @category models
|
|
174
|
+
* @since 0.3.0
|
|
175
|
+
*/
|
|
176
|
+
export interface MakeOptions {
|
|
177
|
+
/** How long a lease lasts in the browser. See {@link LeaseTTL}. */
|
|
178
|
+
readonly leaseTTL?: Duration.Input | undefined;
|
|
179
|
+
/**
|
|
180
|
+
* The request's or the navigation's abort signal. Aborting it interrupts
|
|
181
|
+
* the `ensure` and `fetch` calls still waiting, which reject with its
|
|
182
|
+
* reason. The atoms they started keep loading.
|
|
183
|
+
*/
|
|
184
|
+
readonly signal?: AbortSignal | undefined;
|
|
185
|
+
}
|
|
169
186
|
/**
|
|
170
187
|
* Creates the Promise verbs over one registry.
|
|
171
188
|
*
|
|
@@ -174,15 +191,22 @@ export interface Loader {
|
|
|
174
191
|
* Use in a framework integration, in loaders that are async functions, or
|
|
175
192
|
* in an event handler that starts an atom before the state change that
|
|
176
193
|
* renders its reader, for state that can't be in the URL, such as a
|
|
177
|
-
* search-as-you-type query. In Effect code, use
|
|
178
|
-
*
|
|
194
|
+
* search-as-you-type query. In Effect code, use the verbs directly with the
|
|
195
|
+
* `AtomRegistry` provided.
|
|
196
|
+
*
|
|
197
|
+
* **Details**
|
|
198
|
+
*
|
|
199
|
+
* With a `signal`, an aborted request or navigation stops waiting: `ensure`
|
|
200
|
+
* and `fetch` reject with the signal's reason, and the atoms they started
|
|
201
|
+
* keep loading. The React Router and Next.js integrations pass the
|
|
202
|
+
* request's.
|
|
179
203
|
*
|
|
180
204
|
* **Example**
|
|
181
205
|
*
|
|
182
206
|
* ```ts
|
|
183
207
|
* import { Loader, RequestRegistry } from "raf-effect-atom"
|
|
184
208
|
*
|
|
185
|
-
* const loader = Loader.make(RequestRegistry.make())
|
|
209
|
+
* const loader = Loader.make(RequestRegistry.make(), { signal: request.signal })
|
|
186
210
|
* const comments = loader.prefetch(commentPage, 1, 0)
|
|
187
211
|
* const issueHandle = await loader.ensure(issue, 1)
|
|
188
212
|
* ```
|
|
@@ -191,7 +215,5 @@ export interface Loader {
|
|
|
191
215
|
* @category constructors
|
|
192
216
|
* @since 0.1.0
|
|
193
217
|
*/
|
|
194
|
-
export declare const make: (registry: AtomRegistry.AtomRegistry, options?:
|
|
195
|
-
readonly leaseTTL?: Duration.Input | undefined;
|
|
196
|
-
}) => Loader;
|
|
218
|
+
export declare const make: (registry: AtomRegistry.AtomRegistry, options?: MakeOptions) => Loader;
|
|
197
219
|
export { Loader_d_exports };
|
package/dist/Loader.mjs
CHANGED
|
@@ -250,6 +250,9 @@ const result = (handle) => {
|
|
|
250
250
|
if (settledResult === void 0) throw new Error("raf-effect-atom: Loader.result got a handle that no ensure or fetch returned here. Only the code that ran the verb has its result: a handle that crossed the network is a copy. Read it with useAtomResult instead.");
|
|
251
251
|
return settledResult;
|
|
252
252
|
};
|
|
253
|
+
const runPromise = (effect, signal) => Effect.runPromise(effect, { signal }).catch((error) => {
|
|
254
|
+
throw signal?.aborted ? signal.reason : error;
|
|
255
|
+
});
|
|
253
256
|
/**
|
|
254
257
|
* Creates the Promise verbs over one registry.
|
|
255
258
|
*
|
|
@@ -258,15 +261,22 @@ const result = (handle) => {
|
|
|
258
261
|
* Use in a framework integration, in loaders that are async functions, or
|
|
259
262
|
* in an event handler that starts an atom before the state change that
|
|
260
263
|
* renders its reader, for state that can't be in the URL, such as a
|
|
261
|
-
* search-as-you-type query. In Effect code, use
|
|
262
|
-
*
|
|
264
|
+
* search-as-you-type query. In Effect code, use the verbs directly with the
|
|
265
|
+
* `AtomRegistry` provided.
|
|
266
|
+
*
|
|
267
|
+
* **Details**
|
|
268
|
+
*
|
|
269
|
+
* With a `signal`, an aborted request or navigation stops waiting: `ensure`
|
|
270
|
+
* and `fetch` reject with the signal's reason, and the atoms they started
|
|
271
|
+
* keep loading. The React Router and Next.js integrations pass the
|
|
272
|
+
* request's.
|
|
263
273
|
*
|
|
264
274
|
* **Example**
|
|
265
275
|
*
|
|
266
276
|
* ```ts
|
|
267
277
|
* import { Loader, RequestRegistry } from "raf-effect-atom"
|
|
268
278
|
*
|
|
269
|
-
* const loader = Loader.make(RequestRegistry.make())
|
|
279
|
+
* const loader = Loader.make(RequestRegistry.make(), { signal: request.signal })
|
|
270
280
|
* const comments = loader.prefetch(commentPage, 1, 0)
|
|
271
281
|
* const issueHandle = await loader.ensure(issue, 1)
|
|
272
282
|
* ```
|
|
@@ -277,13 +287,14 @@ const result = (handle) => {
|
|
|
277
287
|
*/
|
|
278
288
|
const make = (registry, options) => {
|
|
279
289
|
const leaseTTL = options?.leaseTTL === void 0 ? void 0 : Duration.fromInputUnsafe(options.leaseTTL);
|
|
290
|
+
const signal = options?.signal;
|
|
280
291
|
const provide = (effect) => {
|
|
281
292
|
const provided = Effect.provideService(effect, AtomRegistry.AtomRegistry, registry);
|
|
282
293
|
return leaseTTL === void 0 ? provided : Effect.provideService(provided, LeaseTTL, leaseTTL);
|
|
283
294
|
};
|
|
284
295
|
return {
|
|
285
|
-
ensure: (source, ...args) =>
|
|
286
|
-
fetch: (source, ...args) =>
|
|
296
|
+
ensure: (source, ...args) => runPromise(provide(ensure(source, ...args)), signal),
|
|
297
|
+
fetch: (source, ...args) => runPromise(provide(fetch(source, ...args)), signal),
|
|
287
298
|
prefetch: (source, ...args) => {
|
|
288
299
|
const atom = atomOf(source, args);
|
|
289
300
|
afterSeed(registry, () => {
|
package/dist/Next.d.mts
CHANGED
|
@@ -67,7 +67,9 @@ export declare const makeRequestAtoms: (options?: Options) => RequestAtoms;
|
|
|
67
67
|
*
|
|
68
68
|
* Made once per request through React's `cache`, and disposed through
|
|
69
69
|
* `cacheSignal` when React finishes the render, after the last streamed
|
|
70
|
-
* result has been sent.
|
|
70
|
+
* result has been sent. The verbs take the same signal: when React aborts
|
|
71
|
+
* the render, such as for a navigation the browser left, the `ensure` and
|
|
72
|
+
* `fetch` calls still waiting reject with its reason.
|
|
71
73
|
*
|
|
72
74
|
* **Gotchas**
|
|
73
75
|
*
|
package/dist/Next.mjs
CHANGED
|
@@ -48,7 +48,9 @@ const atoms = make();
|
|
|
48
48
|
*
|
|
49
49
|
* Made once per request through React's `cache`, and disposed through
|
|
50
50
|
* `cacheSignal` when React finishes the render, after the last streamed
|
|
51
|
-
* result has been sent.
|
|
51
|
+
* result has been sent. The verbs take the same signal: when React aborts
|
|
52
|
+
* the render, such as for a navigation the browser left, the `ensure` and
|
|
53
|
+
* `fetch` calls still waiting reject with its reason.
|
|
52
54
|
*
|
|
53
55
|
* **Gotchas**
|
|
54
56
|
*
|
|
@@ -35,6 +35,12 @@ export interface HydrationBoundaryProps {
|
|
|
35
35
|
* there, so an `effect-atom-boundary` boundary sees their failures before
|
|
36
36
|
* its children read them and renders its handler on the server.
|
|
37
37
|
*
|
|
38
|
+
* **Gotchas**
|
|
39
|
+
*
|
|
40
|
+
* In development, it throws during server rendering when the registry isn't
|
|
41
|
+
* a request registry, such as `@effect/atom-react`'s default one, which
|
|
42
|
+
* every request shares: one request's state would reach the others.
|
|
43
|
+
*
|
|
38
44
|
* **Example**
|
|
39
45
|
*
|
|
40
46
|
* ```tsx
|
package/dist/ReactHydration.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { split } from "./internal/pending.mjs";
|
|
3
3
|
import { sourceOf } from "./internal/sources.mjs";
|
|
4
|
-
import { isServer } from "./internal/registry.mjs";
|
|
4
|
+
import { isServer, requireRequestRegistry } from "./internal/registry.mjs";
|
|
5
5
|
import { HydrationBoundary, RegistryContext } from "@effect/atom-react";
|
|
6
6
|
import * as React from "react";
|
|
7
7
|
import { jsx } from "react/jsx-runtime";
|
|
@@ -36,6 +36,12 @@ import { jsx } from "react/jsx-runtime";
|
|
|
36
36
|
* there, so an `effect-atom-boundary` boundary sees their failures before
|
|
37
37
|
* its children read them and renders its handler on the server.
|
|
38
38
|
*
|
|
39
|
+
* **Gotchas**
|
|
40
|
+
*
|
|
41
|
+
* In development, it throws during server rendering when the registry isn't
|
|
42
|
+
* a request registry, such as `@effect/atom-react`'s default one, which
|
|
43
|
+
* every request shares: one request's state would reach the others.
|
|
44
|
+
*
|
|
39
45
|
* **Example**
|
|
40
46
|
*
|
|
41
47
|
* ```tsx
|
|
@@ -55,6 +61,7 @@ const HydrationBoundary$1 = (props) => {
|
|
|
55
61
|
const state = props.state;
|
|
56
62
|
const source = isServer() ? sourceOf(state) : void 0;
|
|
57
63
|
const registry = source ?? context;
|
|
64
|
+
requireRequestRegistry(registry, "HydrationBoundary");
|
|
58
65
|
const settled = React.useMemo(() => state === void 0 || source !== void 0 ? void 0 : split(registry, state), [
|
|
59
66
|
registry,
|
|
60
67
|
source,
|
package/dist/ReactRouter.d.mts
CHANGED
|
@@ -3,7 +3,9 @@ import { RegistryProvider } from "./RegistryProvider.mjs";
|
|
|
3
3
|
import * as EffectHydration from "effect/reactivity/Hydration";
|
|
4
4
|
import * as AtomRegistry from "effect/reactivity/AtomRegistry";
|
|
5
5
|
/**
|
|
6
|
-
* One request's registry, with the loader verbs over it.
|
|
6
|
+
* One request's registry, with the loader verbs over it. In a client loader,
|
|
7
|
+
* the browser's registry, whose `dehydrate` returns nothing: its atoms are
|
|
8
|
+
* already where the components read them.
|
|
7
9
|
*
|
|
8
10
|
* @stability unstable
|
|
9
11
|
* @category models
|
|
@@ -68,11 +70,14 @@ export interface AtomsMiddlewareOptions {
|
|
|
68
70
|
* `seed` runs on the new registry before `next`, so every loader's atoms
|
|
69
71
|
* start with what it sets.
|
|
70
72
|
*
|
|
73
|
+
* The verbs take the request's `signal`: when the client goes before the
|
|
74
|
+
* response, such as a navigation that leaves while its loaders wait, the
|
|
75
|
+
* `ensure` and `fetch` calls still waiting reject with its reason.
|
|
76
|
+
*
|
|
71
77
|
* **Gotchas**
|
|
72
78
|
*
|
|
73
79
|
* It's server middleware: export it as the root route's `middleware`, not
|
|
74
|
-
* `clientMiddleware
|
|
75
|
-
* the registry.
|
|
80
|
+
* `clientMiddleware`, which takes `clientAtomsMiddleware`.
|
|
76
81
|
*
|
|
77
82
|
* **Example**
|
|
78
83
|
*
|
|
@@ -91,4 +96,49 @@ export interface AtomsMiddlewareOptions {
|
|
|
91
96
|
* @since 0.1.0
|
|
92
97
|
*/
|
|
93
98
|
export declare const atomsMiddleware: (context: AtomsContext, options?: AtomsMiddlewareOptions) => <Result>(args: MiddlewareArgs, next: () => Promise<Result>) => Promise<Result>;
|
|
99
|
+
/**
|
|
100
|
+
* Creates a root client middleware that gives client loaders the browser's
|
|
101
|
+
* registry, under the context `atomsMiddleware` uses on the server.
|
|
102
|
+
*
|
|
103
|
+
* **When to use**
|
|
104
|
+
*
|
|
105
|
+
* Export it as the root route's `clientMiddleware`, and each route's loader
|
|
106
|
+
* function as both its `loader` and its `clientLoader`. The first page's loaders run on
|
|
107
|
+
* the server and stream their atoms in; after that, navigations run the
|
|
108
|
+
* loaders in the browser. `ensure` then renders what the browser's registry
|
|
109
|
+
* holds without a request, and a link's prefetch doesn't fetch a route's
|
|
110
|
+
* `.data` that the click would fetch again.
|
|
111
|
+
*
|
|
112
|
+
* **Details**
|
|
113
|
+
*
|
|
114
|
+
* The registry is the one the mounted `RegistryProvider` provides. Its
|
|
115
|
+
* `dehydrate` returns nothing, so a loader's data has the same shape in
|
|
116
|
+
* the browser and on the server, and `HydrationBoundary` has nothing to do.
|
|
117
|
+
* The verbs take the navigation's `signal`: when another navigation
|
|
118
|
+
* interrupts it, the `ensure` and `fetch` calls still waiting reject with
|
|
119
|
+
* its reason, and the atoms keep loading.
|
|
120
|
+
*
|
|
121
|
+
* **Gotchas**
|
|
122
|
+
*
|
|
123
|
+
* The atoms run in the browser, so their RPC protocol must reach the server
|
|
124
|
+
* from there, as it does for anything a component starts.
|
|
125
|
+
*
|
|
126
|
+
* **Example**
|
|
127
|
+
*
|
|
128
|
+
* ```ts
|
|
129
|
+
* // app/root.tsx
|
|
130
|
+
* export const middleware = [atomsMiddleware(atomsContext)]
|
|
131
|
+
* export const clientMiddleware = [clientAtomsMiddleware(atomsContext)]
|
|
132
|
+
*
|
|
133
|
+
* // app/routes/issue.tsx: one function for both, since React Router
|
|
134
|
+
* // removes `loader` from the browser's bundle.
|
|
135
|
+
* export const loader = load
|
|
136
|
+
* export const clientLoader = load
|
|
137
|
+
* ```
|
|
138
|
+
*
|
|
139
|
+
* @stability unstable
|
|
140
|
+
* @category integrations
|
|
141
|
+
* @since 0.2.1
|
|
142
|
+
*/
|
|
143
|
+
export declare const clientAtomsMiddleware: (context: AtomsContext) => <Result>(args: MiddlewareArgs, next: () => Promise<Result>) => Promise<Result>;
|
|
94
144
|
export { RegistryProvider };
|
package/dist/ReactRouter.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { dehydrate } from "./Hydration.mjs";
|
|
2
2
|
import { make, settled } from "./RequestRegistry.mjs";
|
|
3
3
|
import { make as make$1 } from "./Loader.mjs";
|
|
4
|
+
import { current } from "./internal/registry.mjs";
|
|
4
5
|
import { RegistryProvider } from "./RegistryProvider.mjs";
|
|
5
6
|
import * as Predicate from "effect/Predicate";
|
|
6
7
|
// src/ReactRouter.ts
|
|
@@ -10,8 +11,11 @@ import * as Predicate from "effect/Predicate";
|
|
|
10
11
|
* A root `middleware` gives each server request its own registry, under a
|
|
11
12
|
* router context the app creates. Loaders start atoms with its verbs and
|
|
12
13
|
* return its `dehydrate()` with their data; the route component hydrates it
|
|
13
|
-
* with `HydrationBoundary`, inside the root's `RegistryProvider`.
|
|
14
|
-
*
|
|
14
|
+
* with `HydrationBoundary`, inside the root's `RegistryProvider`. A root
|
|
15
|
+
* `clientMiddleware` puts the browser's registry under the same context, so
|
|
16
|
+
* one function can be both the route's `loader` and its `clientLoader`: after the first
|
|
17
|
+
* page, loaders run in the browser, where `ensure` finds what the registry
|
|
18
|
+
* already holds. Nothing is imported from `react-router`.
|
|
15
19
|
*
|
|
16
20
|
* **Example**
|
|
17
21
|
*
|
|
@@ -26,14 +30,18 @@ import * as Predicate from "effect/Predicate";
|
|
|
26
30
|
* import { atomsMiddleware } from "raf-effect-atom/react-router"
|
|
27
31
|
*
|
|
28
32
|
* export const middleware = [atomsMiddleware(atomsContext)]
|
|
33
|
+
* export const clientMiddleware = [clientAtomsMiddleware(atomsContext)]
|
|
29
34
|
*
|
|
30
35
|
* // app/routes/issue.tsx
|
|
31
|
-
*
|
|
36
|
+
* async function load(args: Route.LoaderArgs) {
|
|
32
37
|
* const atoms = args.context.get(atomsContext)
|
|
33
38
|
* const handle = await atoms.ensure(issue, Number(args.params.id))
|
|
34
39
|
* return { atoms: atoms.dehydrate(), issue: handle }
|
|
35
40
|
* }
|
|
36
41
|
*
|
|
42
|
+
* export const loader = load
|
|
43
|
+
* export const clientLoader = load
|
|
44
|
+
*
|
|
37
45
|
* export default function IssueRoute(props: Route.ComponentProps) {
|
|
38
46
|
* return (
|
|
39
47
|
* <HydrationBoundary state={props.loaderData.atoms}>
|
|
@@ -61,11 +69,14 @@ import * as Predicate from "effect/Predicate";
|
|
|
61
69
|
* `seed` runs on the new registry before `next`, so every loader's atoms
|
|
62
70
|
* start with what it sets.
|
|
63
71
|
*
|
|
72
|
+
* The verbs take the request's `signal`: when the client goes before the
|
|
73
|
+
* response, such as a navigation that leaves while its loaders wait, the
|
|
74
|
+
* `ensure` and `fetch` calls still waiting reject with its reason.
|
|
75
|
+
*
|
|
64
76
|
* **Gotchas**
|
|
65
77
|
*
|
|
66
78
|
* It's server middleware: export it as the root route's `middleware`, not
|
|
67
|
-
* `clientMiddleware
|
|
68
|
-
* the registry.
|
|
79
|
+
* `clientMiddleware`, which takes `clientAtomsMiddleware`.
|
|
69
80
|
*
|
|
70
81
|
* **Example**
|
|
71
82
|
*
|
|
@@ -92,7 +103,7 @@ const atomsMiddleware = (context, options) => async (args, next) => {
|
|
|
92
103
|
registry.dispose();
|
|
93
104
|
throw error;
|
|
94
105
|
}
|
|
95
|
-
const loader = make$1(registry);
|
|
106
|
+
const loader = make$1(registry, { signal: args.request.signal });
|
|
96
107
|
args.context.set(context, {
|
|
97
108
|
...loader,
|
|
98
109
|
dehydrate: () => dehydrate(registry),
|
|
@@ -135,4 +146,58 @@ const disposeAfter = (body, registry) => {
|
|
|
135
146
|
}
|
|
136
147
|
});
|
|
137
148
|
};
|
|
138
|
-
|
|
149
|
+
/**
|
|
150
|
+
* Creates a root client middleware that gives client loaders the browser's
|
|
151
|
+
* registry, under the context `atomsMiddleware` uses on the server.
|
|
152
|
+
*
|
|
153
|
+
* **When to use**
|
|
154
|
+
*
|
|
155
|
+
* Export it as the root route's `clientMiddleware`, and each route's loader
|
|
156
|
+
* function as both its `loader` and its `clientLoader`. The first page's loaders run on
|
|
157
|
+
* the server and stream their atoms in; after that, navigations run the
|
|
158
|
+
* loaders in the browser. `ensure` then renders what the browser's registry
|
|
159
|
+
* holds without a request, and a link's prefetch doesn't fetch a route's
|
|
160
|
+
* `.data` that the click would fetch again.
|
|
161
|
+
*
|
|
162
|
+
* **Details**
|
|
163
|
+
*
|
|
164
|
+
* The registry is the one the mounted `RegistryProvider` provides. Its
|
|
165
|
+
* `dehydrate` returns nothing, so a loader's data has the same shape in
|
|
166
|
+
* the browser and on the server, and `HydrationBoundary` has nothing to do.
|
|
167
|
+
* The verbs take the navigation's `signal`: when another navigation
|
|
168
|
+
* interrupts it, the `ensure` and `fetch` calls still waiting reject with
|
|
169
|
+
* its reason, and the atoms keep loading.
|
|
170
|
+
*
|
|
171
|
+
* **Gotchas**
|
|
172
|
+
*
|
|
173
|
+
* The atoms run in the browser, so their RPC protocol must reach the server
|
|
174
|
+
* from there, as it does for anything a component starts.
|
|
175
|
+
*
|
|
176
|
+
* **Example**
|
|
177
|
+
*
|
|
178
|
+
* ```ts
|
|
179
|
+
* // app/root.tsx
|
|
180
|
+
* export const middleware = [atomsMiddleware(atomsContext)]
|
|
181
|
+
* export const clientMiddleware = [clientAtomsMiddleware(atomsContext)]
|
|
182
|
+
*
|
|
183
|
+
* // app/routes/issue.tsx: one function for both, since React Router
|
|
184
|
+
* // removes `loader` from the browser's bundle.
|
|
185
|
+
* export const loader = load
|
|
186
|
+
* export const clientLoader = load
|
|
187
|
+
* ```
|
|
188
|
+
*
|
|
189
|
+
* @stability unstable
|
|
190
|
+
* @category integrations
|
|
191
|
+
* @since 0.2.1
|
|
192
|
+
*/
|
|
193
|
+
const clientAtomsMiddleware = (context) => (args, next) => {
|
|
194
|
+
const registry = current();
|
|
195
|
+
if (registry === void 0) throw new Error("clientAtomsMiddleware found no mounted RegistryProvider: render raf-effect-atom/react-router's RegistryProvider around the app in the root layout.");
|
|
196
|
+
args.context.set(context, {
|
|
197
|
+
...make$1(registry, { signal: args.request.signal }),
|
|
198
|
+
dehydrate: () => [],
|
|
199
|
+
registry
|
|
200
|
+
});
|
|
201
|
+
return next();
|
|
202
|
+
};
|
|
203
|
+
export { RegistryProvider, atomsMiddleware, clientAtomsMiddleware };
|
|
@@ -18,6 +18,9 @@ import * as React from "react";
|
|
|
18
18
|
* It's read once, when the registry is made. The server's request registry
|
|
19
19
|
* lives for one request, so it has none.
|
|
20
20
|
*
|
|
21
|
+
* In the browser, React Router's `clientAtomsMiddleware` gives client
|
|
22
|
+
* loaders the registry of the mounted provider.
|
|
23
|
+
*
|
|
21
24
|
* **Example**
|
|
22
25
|
*
|
|
23
26
|
* ```tsx
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { make } from "./internal/registry.mjs";
|
|
2
|
+
import { make, mount } from "./internal/registry.mjs";
|
|
3
3
|
import { RegistryContext } from "@effect/atom-react";
|
|
4
4
|
import * as React from "react";
|
|
5
5
|
import { jsx } from "react/jsx-runtime";
|
|
@@ -29,6 +29,9 @@ import { jsx } from "react/jsx-runtime";
|
|
|
29
29
|
* It's read once, when the registry is made. The server's request registry
|
|
30
30
|
* lives for one request, so it has none.
|
|
31
31
|
*
|
|
32
|
+
* In the browser, React Router's `clientAtomsMiddleware` gives client
|
|
33
|
+
* loaders the registry of the mounted provider.
|
|
34
|
+
*
|
|
32
35
|
* **Example**
|
|
33
36
|
*
|
|
34
37
|
* ```tsx
|
|
@@ -47,6 +50,7 @@ import { jsx } from "react/jsx-runtime";
|
|
|
47
50
|
*/
|
|
48
51
|
const RegistryProvider = (props) => {
|
|
49
52
|
const [registry] = React.useState(() => make({ defaultIdleTTL: props.defaultIdleTTL }));
|
|
53
|
+
React.useEffect(() => mount(registry), [registry]);
|
|
50
54
|
return /* @__PURE__ */ jsx(RegistryContext.Provider, {
|
|
51
55
|
value: registry,
|
|
52
56
|
children: props.children
|
package/dist/internal/next.mjs
CHANGED
|
@@ -3,13 +3,16 @@ import { make as make$1, settled } from "../RequestRegistry.mjs";
|
|
|
3
3
|
import { make as make$2 } from "../Loader.mjs";
|
|
4
4
|
import * as React from "react";
|
|
5
5
|
// src/internal/next.ts
|
|
6
|
+
const renderSignal = () => {
|
|
7
|
+
return React.cacheSignal();
|
|
8
|
+
};
|
|
6
9
|
const make = (options) => {
|
|
7
10
|
const getRegistry = React.cache(() => {
|
|
8
11
|
const registry = make$1(options);
|
|
9
|
-
|
|
12
|
+
renderSignal()?.addEventListener("abort", () => registry.dispose(), { once: true });
|
|
10
13
|
return registry;
|
|
11
14
|
});
|
|
12
|
-
const getLoader = React.cache(() => make$2(getRegistry()));
|
|
15
|
+
const getLoader = React.cache(() => make$2(getRegistry(), { signal: renderSignal() ?? void 0 }));
|
|
13
16
|
return {
|
|
14
17
|
dehydrate: async () => {
|
|
15
18
|
const registry = getRegistry();
|
|
@@ -1,8 +1,18 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { isDevelopment } from "./development.mjs";
|
|
2
|
+
import { isRequestRegistry, make as make$1 } from "../RequestRegistry.mjs";
|
|
3
|
+
import * as Effect from "effect/Effect";
|
|
2
4
|
import * as AtomRegistry from "effect/reactivity/AtomRegistry";
|
|
3
5
|
import { scheduleTask } from "@effect/atom-react";
|
|
4
6
|
// src/internal/registry.ts
|
|
5
7
|
const isServer = () => !("window" in globalThis);
|
|
8
|
+
/**
|
|
9
|
+
* In development, throws when `rendered` renders on the server without a
|
|
10
|
+
* request registry: in one every request shares, such as
|
|
11
|
+
* `@effect/atom-react`'s default `RegistryContext`.
|
|
12
|
+
*/
|
|
13
|
+
const requireRequestRegistry = (registry, rendered) => {
|
|
14
|
+
if (isServer() && !isRequestRegistry(registry) && Effect.runSync(isDevelopment)) throw new Error(`raf-effect-atom: ${rendered} 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.`);
|
|
15
|
+
};
|
|
6
16
|
/** `@effect/atom-react`'s default registry keeps an unused atom this long. */
|
|
7
17
|
const defaultIdleTTL = 400;
|
|
8
18
|
/**
|
|
@@ -13,4 +23,18 @@ const make = (options) => isServer() ? make$1({ seed: options?.seed }) : AtomReg
|
|
|
13
23
|
defaultIdleTTL: options?.defaultIdleTTL ?? defaultIdleTTL,
|
|
14
24
|
scheduleTask
|
|
15
25
|
});
|
|
16
|
-
|
|
26
|
+
let mounted;
|
|
27
|
+
/**
|
|
28
|
+
* Records the browser registry a `RegistryProvider` provides while it's
|
|
29
|
+
* mounted, for code outside React, such as React Router's client
|
|
30
|
+
* middleware. Returns the cleanup that forgets it.
|
|
31
|
+
*/
|
|
32
|
+
const mount = (registry) => {
|
|
33
|
+
mounted = registry;
|
|
34
|
+
return () => {
|
|
35
|
+
if (mounted === registry) mounted = void 0;
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
/** The browser registry of the mounted `RegistryProvider`, if one is. */
|
|
39
|
+
const current = () => mounted;
|
|
40
|
+
export { current, isServer, make, mount, requireRequestRegistry };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "raf-effect-atom",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Render-as-you-fetch for Effect Atom: start atoms from route loaders and stream them into the browser, typed failures intact",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"effect",
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
"@types/react-dom": "19.3.0",
|
|
59
59
|
"@vitest/coverage-v8": "5.0.5",
|
|
60
60
|
"effect": "4.0.6",
|
|
61
|
-
"effect-atom-boundary": "0.1.
|
|
61
|
+
"effect-atom-boundary": "0.1.1",
|
|
62
62
|
"happy-dom": "21.0.4",
|
|
63
63
|
"react": "19.3.1",
|
|
64
64
|
"react-dom": "19.3.1",
|