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
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { dehydrate } from "./Hydration.mjs";
|
|
2
|
+
import { make } from "./RequestRegistry.mjs";
|
|
3
|
+
import { make as make$1 } from "./Loader.mjs";
|
|
4
|
+
import { RegistryProvider } from "./RegistryProvider.mjs";
|
|
5
|
+
import * as Predicate from "effect/Predicate";
|
|
6
|
+
// src/ReactRouter.ts
|
|
7
|
+
/**
|
|
8
|
+
* Connects the loader verbs to React Router's Framework Mode.
|
|
9
|
+
*
|
|
10
|
+
* A root `middleware` gives each server request its own registry, under a
|
|
11
|
+
* router context the app creates. Loaders start atoms with its verbs and
|
|
12
|
+
* return its `dehydrate()` with their data; the route component hydrates it
|
|
13
|
+
* with `HydrationBoundary`, inside the root's `RegistryProvider`. Nothing is
|
|
14
|
+
* imported from `react-router`.
|
|
15
|
+
*
|
|
16
|
+
* **Example**
|
|
17
|
+
*
|
|
18
|
+
* ```tsx
|
|
19
|
+
* // app/atoms.ts
|
|
20
|
+
* import { createContext } from "react-router"
|
|
21
|
+
* import type { RequestAtoms } from "raf-effect-atom/react-router"
|
|
22
|
+
*
|
|
23
|
+
* export const atomsContext = createContext<RequestAtoms>()
|
|
24
|
+
*
|
|
25
|
+
* // app/root.tsx
|
|
26
|
+
* import { atomsMiddleware } from "raf-effect-atom/react-router"
|
|
27
|
+
*
|
|
28
|
+
* export const middleware = [atomsMiddleware(atomsContext)]
|
|
29
|
+
*
|
|
30
|
+
* // app/routes/issue.tsx
|
|
31
|
+
* export async function loader(args: Route.LoaderArgs) {
|
|
32
|
+
* const atoms = args.context.get(atomsContext)
|
|
33
|
+
* await atoms.ensure(issue(Number(args.params.id)))
|
|
34
|
+
* return { atoms: atoms.dehydrate() }
|
|
35
|
+
* }
|
|
36
|
+
*
|
|
37
|
+
* export default function IssueRoute(props: Route.ComponentProps) {
|
|
38
|
+
* return (
|
|
39
|
+
* <HydrationBoundary state={props.loaderData.atoms}>
|
|
40
|
+
* <Issue id={Number(props.params.id)} />
|
|
41
|
+
* </HydrationBoundary>
|
|
42
|
+
* )
|
|
43
|
+
* }
|
|
44
|
+
* ```
|
|
45
|
+
*
|
|
46
|
+
* @stability unstable
|
|
47
|
+
* @since 0.1.0
|
|
48
|
+
*/
|
|
49
|
+
/**
|
|
50
|
+
* Creates a root middleware that gives each server request its own
|
|
51
|
+
* registry, under the given router context.
|
|
52
|
+
*
|
|
53
|
+
* **Details**
|
|
54
|
+
*
|
|
55
|
+
* The registry is disposed once the response's body has been sent, or the
|
|
56
|
+
* client has gone: loader data streams its pending promises after the
|
|
57
|
+
* middleware returns, so the atoms behind them must keep running until
|
|
58
|
+
* then. When `next` doesn't return a `Response`, as Data Mode's
|
|
59
|
+
* `query` can, the registry is left to be collected with the request.
|
|
60
|
+
*
|
|
61
|
+
* **Gotchas**
|
|
62
|
+
*
|
|
63
|
+
* It's server middleware: export it as the root route's `middleware`, not
|
|
64
|
+
* `clientMiddleware`. In the browser, the root's `RegistryProvider` holds
|
|
65
|
+
* the registry.
|
|
66
|
+
*
|
|
67
|
+
* @stability unstable
|
|
68
|
+
* @category integrations
|
|
69
|
+
* @since 0.1.0
|
|
70
|
+
*/
|
|
71
|
+
const atomsMiddleware = (context) => async (args, next) => {
|
|
72
|
+
const registry = make();
|
|
73
|
+
const loader = make$1(registry);
|
|
74
|
+
args.context.set(context, {
|
|
75
|
+
...loader,
|
|
76
|
+
dehydrate: () => dehydrate(registry),
|
|
77
|
+
registry
|
|
78
|
+
});
|
|
79
|
+
let result;
|
|
80
|
+
try {
|
|
81
|
+
result = await next();
|
|
82
|
+
} catch (error) {
|
|
83
|
+
registry.dispose();
|
|
84
|
+
throw error;
|
|
85
|
+
}
|
|
86
|
+
if (!isResponse(result)) return result;
|
|
87
|
+
const body = result.body;
|
|
88
|
+
if (body === null) {
|
|
89
|
+
registry.dispose();
|
|
90
|
+
return result;
|
|
91
|
+
}
|
|
92
|
+
return new Response(disposeAfter(body, registry), result);
|
|
93
|
+
};
|
|
94
|
+
const isResponse = (value) => Predicate.hasProperty(value, "body") && Predicate.hasProperty(value, "headers") && Predicate.hasProperty(value, "status");
|
|
95
|
+
const disposeAfter = (body, registry) => {
|
|
96
|
+
const reader = body.getReader();
|
|
97
|
+
return new ReadableStream({
|
|
98
|
+
cancel: async (reason) => {
|
|
99
|
+
registry.dispose();
|
|
100
|
+
await reader.cancel(reason);
|
|
101
|
+
},
|
|
102
|
+
pull: async (controller) => {
|
|
103
|
+
try {
|
|
104
|
+
const chunk = await reader.read();
|
|
105
|
+
if (chunk.done) {
|
|
106
|
+
registry.dispose();
|
|
107
|
+
controller.close();
|
|
108
|
+
} else controller.enqueue(chunk.value);
|
|
109
|
+
} catch (error) {
|
|
110
|
+
registry.dispose();
|
|
111
|
+
controller.error(error);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
};
|
|
116
|
+
export { RegistryProvider, atomsMiddleware };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
/**
|
|
3
|
+
* Provides the app's registry: a request registry while the framework
|
|
4
|
+
* renders the app on the server, and one registry for the page's lifetime
|
|
5
|
+
* in the browser.
|
|
6
|
+
*
|
|
7
|
+
* **When to use**
|
|
8
|
+
*
|
|
9
|
+
* Put it in the root layout, around `children`, in place of
|
|
10
|
+
* `@effect/atom-react`'s `RegistryProvider`, which would share one registry
|
|
11
|
+
* between requests' server renders.
|
|
12
|
+
*
|
|
13
|
+
* **Details**
|
|
14
|
+
*
|
|
15
|
+
* `defaultIdleTTL` is how many milliseconds the browser's registry keeps an
|
|
16
|
+
* atom no component reads, 400 by default, as `@effect/atom-react`'s default
|
|
17
|
+
* registry does. An atom's own `Atom.setIdleTTL` or `Atom.keepAlive` wins.
|
|
18
|
+
* It's read once, when the registry is made. The server's request registry
|
|
19
|
+
* lives for one request, so it has none.
|
|
20
|
+
*
|
|
21
|
+
* **Example**
|
|
22
|
+
*
|
|
23
|
+
* ```tsx
|
|
24
|
+
* import { RegistryProvider } from "raf-effect-atom/next" // or "raf-effect-atom/react-router"
|
|
25
|
+
*
|
|
26
|
+
* export default function RootLayout(props: { children: React.ReactNode }) {
|
|
27
|
+
* return (
|
|
28
|
+
* <RegistryProvider defaultIdleTTL={60_000}>{props.children}</RegistryProvider>
|
|
29
|
+
* )
|
|
30
|
+
* }
|
|
31
|
+
* ```
|
|
32
|
+
*
|
|
33
|
+
* @stability unstable
|
|
34
|
+
* @category components
|
|
35
|
+
* @since 0.1.0
|
|
36
|
+
*/
|
|
37
|
+
export declare const RegistryProvider: (props: {
|
|
38
|
+
readonly children?: React.ReactNode | undefined;
|
|
39
|
+
readonly defaultIdleTTL?: number | undefined;
|
|
40
|
+
}) => React.ReactNode;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { make } from "./internal/registry.mjs";
|
|
3
|
+
import { RegistryContext } from "@effect/atom-react";
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
import { jsx } from "react/jsx-runtime";
|
|
6
|
+
// src/RegistryProvider.tsx
|
|
7
|
+
/**
|
|
8
|
+
* The registry provider shared by the Next.js and React Router integrations.
|
|
9
|
+
*
|
|
10
|
+
* @stability unstable
|
|
11
|
+
* @since 0.1.0
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Provides the app's registry: a request registry while the framework
|
|
15
|
+
* renders the app on the server, and one registry for the page's lifetime
|
|
16
|
+
* in the browser.
|
|
17
|
+
*
|
|
18
|
+
* **When to use**
|
|
19
|
+
*
|
|
20
|
+
* Put it in the root layout, around `children`, in place of
|
|
21
|
+
* `@effect/atom-react`'s `RegistryProvider`, which would share one registry
|
|
22
|
+
* between requests' server renders.
|
|
23
|
+
*
|
|
24
|
+
* **Details**
|
|
25
|
+
*
|
|
26
|
+
* `defaultIdleTTL` is how many milliseconds the browser's registry keeps an
|
|
27
|
+
* atom no component reads, 400 by default, as `@effect/atom-react`'s default
|
|
28
|
+
* registry does. An atom's own `Atom.setIdleTTL` or `Atom.keepAlive` wins.
|
|
29
|
+
* It's read once, when the registry is made. The server's request registry
|
|
30
|
+
* lives for one request, so it has none.
|
|
31
|
+
*
|
|
32
|
+
* **Example**
|
|
33
|
+
*
|
|
34
|
+
* ```tsx
|
|
35
|
+
* import { RegistryProvider } from "raf-effect-atom/next" // or "raf-effect-atom/react-router"
|
|
36
|
+
*
|
|
37
|
+
* export default function RootLayout(props: { children: React.ReactNode }) {
|
|
38
|
+
* return (
|
|
39
|
+
* <RegistryProvider defaultIdleTTL={60_000}>{props.children}</RegistryProvider>
|
|
40
|
+
* )
|
|
41
|
+
* }
|
|
42
|
+
* ```
|
|
43
|
+
*
|
|
44
|
+
* @stability unstable
|
|
45
|
+
* @category components
|
|
46
|
+
* @since 0.1.0
|
|
47
|
+
*/
|
|
48
|
+
const RegistryProvider = (props) => {
|
|
49
|
+
const [registry] = React.useState(() => make({ defaultIdleTTL: props.defaultIdleTTL }));
|
|
50
|
+
return /* @__PURE__ */ jsx(RegistryContext.Provider, {
|
|
51
|
+
value: registry,
|
|
52
|
+
children: props.children
|
|
53
|
+
});
|
|
54
|
+
};
|
|
55
|
+
export { RegistryProvider };
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import * as AtomRegistry from "effect/reactivity/AtomRegistry";
|
|
2
|
+
import * as Layer from "effect/Layer";
|
|
3
|
+
declare namespace RequestRegistry_d_exports {
|
|
4
|
+
export { isRequestRegistry, layer, make };
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Creates the registry for one server request.
|
|
8
|
+
*
|
|
9
|
+
* **Details**
|
|
10
|
+
*
|
|
11
|
+
* It's an ordinary `AtomRegistry`, marked as a request registry. On a
|
|
12
|
+
* request registry, `ensure` waits like `fetch` does, nothing revalidates in
|
|
13
|
+
* the background, and a loader's lease lasts until the registry is disposed.
|
|
14
|
+
* In development, the loader verbs refuse an atom that isn't serializable,
|
|
15
|
+
* because the browser would request it again.
|
|
16
|
+
*
|
|
17
|
+
* **Example**
|
|
18
|
+
*
|
|
19
|
+
* ```ts
|
|
20
|
+
* import { RequestRegistry } from "raf-effect-atom"
|
|
21
|
+
*
|
|
22
|
+
* const registry = RequestRegistry.make()
|
|
23
|
+
* ```
|
|
24
|
+
*
|
|
25
|
+
* @stability unstable
|
|
26
|
+
* @category constructors
|
|
27
|
+
* @since 0.1.0
|
|
28
|
+
*/
|
|
29
|
+
export declare const make: () => AtomRegistry.AtomRegistry;
|
|
30
|
+
/**
|
|
31
|
+
* A layer that provides a request registry as the `AtomRegistry`, disposed
|
|
32
|
+
* when the layer's scope closes.
|
|
33
|
+
*
|
|
34
|
+
* **When to use**
|
|
35
|
+
*
|
|
36
|
+
* Use when the request is handled in Effect, around the loaders' verbs and
|
|
37
|
+
* `dehydrate`.
|
|
38
|
+
*
|
|
39
|
+
* @see {@link make} for the registry it provides
|
|
40
|
+
*
|
|
41
|
+
* @stability unstable
|
|
42
|
+
* @category layers
|
|
43
|
+
* @since 0.1.0
|
|
44
|
+
*/
|
|
45
|
+
export declare const layer: Layer.Layer<AtomRegistry.AtomRegistry>;
|
|
46
|
+
/**
|
|
47
|
+
* Returns whether a registry was made by `make`, for one server request.
|
|
48
|
+
*
|
|
49
|
+
* @stability unstable
|
|
50
|
+
* @category guards
|
|
51
|
+
* @since 0.1.0
|
|
52
|
+
*/
|
|
53
|
+
export declare const isRequestRegistry: (registry: AtomRegistry.AtomRegistry) => boolean;
|
|
54
|
+
export { RequestRegistry_d_exports };
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { __exportAll } from "./_virtual/_rolldown/runtime.mjs";
|
|
2
|
+
import * as Effect from "effect/Effect";
|
|
3
|
+
import * as AtomRegistry from "effect/reactivity/AtomRegistry";
|
|
4
|
+
import * as Layer from "effect/Layer";
|
|
5
|
+
// src/RequestRegistry.ts
|
|
6
|
+
/**
|
|
7
|
+
* A registry per server request.
|
|
8
|
+
*
|
|
9
|
+
* `@effect/atom-react`'s default `RegistryContext` is one registry for the
|
|
10
|
+
* whole module, which on a server would share one user's results with every
|
|
11
|
+
* other request. Each framework integration makes a request registry with
|
|
12
|
+
* `make`, provides it through `RegistryContext`, and dehydrates it into the
|
|
13
|
+
* response.
|
|
14
|
+
*
|
|
15
|
+
* @stability unstable
|
|
16
|
+
* @since 0.1.0
|
|
17
|
+
*/
|
|
18
|
+
var RequestRegistry_exports = /* @__PURE__ */ __exportAll({
|
|
19
|
+
isRequestRegistry: () => isRequestRegistry,
|
|
20
|
+
layer: () => layer,
|
|
21
|
+
make: () => make
|
|
22
|
+
});
|
|
23
|
+
const requestRegistries = /* @__PURE__ */ new WeakSet();
|
|
24
|
+
/**
|
|
25
|
+
* Creates the registry for one server request.
|
|
26
|
+
*
|
|
27
|
+
* **Details**
|
|
28
|
+
*
|
|
29
|
+
* It's an ordinary `AtomRegistry`, marked as a request registry. On a
|
|
30
|
+
* request registry, `ensure` waits like `fetch` does, nothing revalidates in
|
|
31
|
+
* the background, and a loader's lease lasts until the registry is disposed.
|
|
32
|
+
* In development, the loader verbs refuse an atom that isn't serializable,
|
|
33
|
+
* because the browser would request it again.
|
|
34
|
+
*
|
|
35
|
+
* **Example**
|
|
36
|
+
*
|
|
37
|
+
* ```ts
|
|
38
|
+
* import { RequestRegistry } from "raf-effect-atom"
|
|
39
|
+
*
|
|
40
|
+
* const registry = RequestRegistry.make()
|
|
41
|
+
* ```
|
|
42
|
+
*
|
|
43
|
+
* @stability unstable
|
|
44
|
+
* @category constructors
|
|
45
|
+
* @since 0.1.0
|
|
46
|
+
*/
|
|
47
|
+
const make = () => {
|
|
48
|
+
const registry = AtomRegistry.make();
|
|
49
|
+
requestRegistries.add(registry);
|
|
50
|
+
return registry;
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* A layer that provides a request registry as the `AtomRegistry`, disposed
|
|
54
|
+
* when the layer's scope closes.
|
|
55
|
+
*
|
|
56
|
+
* **When to use**
|
|
57
|
+
*
|
|
58
|
+
* Use when the request is handled in Effect, around the loaders' verbs and
|
|
59
|
+
* `dehydrate`.
|
|
60
|
+
*
|
|
61
|
+
* @see {@link make} for the registry it provides
|
|
62
|
+
*
|
|
63
|
+
* @stability unstable
|
|
64
|
+
* @category layers
|
|
65
|
+
* @since 0.1.0
|
|
66
|
+
*/
|
|
67
|
+
const layer = Layer.effect(AtomRegistry.AtomRegistry, Effect.acquireRelease(Effect.sync(() => make()), (registry) => Effect.sync(() => registry.dispose())));
|
|
68
|
+
/**
|
|
69
|
+
* Returns whether a registry was made by `make`, for one server request.
|
|
70
|
+
*
|
|
71
|
+
* @stability unstable
|
|
72
|
+
* @category guards
|
|
73
|
+
* @since 0.1.0
|
|
74
|
+
*/
|
|
75
|
+
const isRequestRegistry = (registry) => requestRegistries.has(registry);
|
|
76
|
+
export { RequestRegistry_exports, isRequestRegistry, layer, make };
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import * as EffectHydration from "effect/reactivity/Hydration";
|
|
2
|
+
import * as AtomRegistry from "effect/reactivity/AtomRegistry";
|
|
3
|
+
import { AnyRouter } from "@tanstack/react-router";
|
|
4
|
+
/**
|
|
5
|
+
* The key `setupRouterAtoms` adds to the router's dehydrated state.
|
|
6
|
+
*
|
|
7
|
+
* @stability unstable
|
|
8
|
+
* @category models
|
|
9
|
+
* @since 0.1.0
|
|
10
|
+
*/
|
|
11
|
+
export interface DehydratedRouterAtoms {
|
|
12
|
+
readonly "raf-effect-atom": ReadonlyArray<EffectHydration.DehydratedAtom>;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Options for `setupRouterAtoms`.
|
|
16
|
+
*
|
|
17
|
+
* @stability unstable
|
|
18
|
+
* @category models
|
|
19
|
+
* @since 0.1.0
|
|
20
|
+
*/
|
|
21
|
+
export interface RouterAtomsOptions<TRouter extends AnyRouter> {
|
|
22
|
+
/** The registry the router context's loader verbs run on. */
|
|
23
|
+
readonly registry: AtomRegistry.AtomRegistry;
|
|
24
|
+
readonly router: TRouter;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Creates the registry for one `getRouter` call: a request registry on the
|
|
28
|
+
* server, and in the browser a registry like `@effect/atom-react`'s
|
|
29
|
+
* `RegistryProvider` makes.
|
|
30
|
+
*
|
|
31
|
+
* **Details**
|
|
32
|
+
*
|
|
33
|
+
* - The server is anywhere without a `window`. To decide otherwise, make
|
|
34
|
+
* the registry with `RequestRegistry.make` on the server and
|
|
35
|
+
* `AtomRegistry.make` in the browser.
|
|
36
|
+
* - `defaultIdleTTL` is how many milliseconds the browser's registry keeps
|
|
37
|
+
* an atom no component reads, 400 by default, as `@effect/atom-react`'s
|
|
38
|
+
* default registry does. An atom's own `Atom.setIdleTTL` or
|
|
39
|
+
* `Atom.keepAlive` wins. The server's request registry lives for one
|
|
40
|
+
* request, so it has none.
|
|
41
|
+
*
|
|
42
|
+
* **Example**
|
|
43
|
+
*
|
|
44
|
+
* ```ts
|
|
45
|
+
* import { makeRegistry } from "raf-effect-atom/tanstack-router"
|
|
46
|
+
*
|
|
47
|
+
* // Keeps an issue the user navigated away from for a minute.
|
|
48
|
+
* const registry = makeRegistry({ defaultIdleTTL: 60_000 })
|
|
49
|
+
* ```
|
|
50
|
+
*
|
|
51
|
+
* @stability unstable
|
|
52
|
+
* @category constructors
|
|
53
|
+
* @since 0.1.0
|
|
54
|
+
*/
|
|
55
|
+
export declare const makeRegistry: (options?: {
|
|
56
|
+
readonly defaultIdleTTL?: number | undefined;
|
|
57
|
+
}) => AtomRegistry.AtomRegistry;
|
|
58
|
+
/**
|
|
59
|
+
* Provides a registry to a TanStack router's app and streams it from the
|
|
60
|
+
* server to the browser.
|
|
61
|
+
*
|
|
62
|
+
* **Details**
|
|
63
|
+
*
|
|
64
|
+
* - Wraps the app in `RegistryContext.Provider` through the router's `Wrap`
|
|
65
|
+
* option, inside any `Wrap` it has already.
|
|
66
|
+
* - On the server, adds `Hydration.dehydrate(registry)` to the router's
|
|
67
|
+
* dehydrated state once the loaders have run, so results still loading
|
|
68
|
+
* cross as promises on the router's stream, and disposes the registry when
|
|
69
|
+
* the response finishes.
|
|
70
|
+
* - In the browser, hydrates the registry from that state before the app
|
|
71
|
+
* renders.
|
|
72
|
+
*
|
|
73
|
+
* **Gotchas**
|
|
74
|
+
*
|
|
75
|
+
* The server's registry must be a request registry and the browser's must
|
|
76
|
+
* not, so it throws otherwise: a registry shared between requests would send
|
|
77
|
+
* one user's results to another. Only atoms in the registry when the loaders
|
|
78
|
+
* finish are sent; one first read while rendering runs again in the browser.
|
|
79
|
+
*
|
|
80
|
+
* @stability unstable
|
|
81
|
+
* @category integrations
|
|
82
|
+
* @since 0.1.0
|
|
83
|
+
*/
|
|
84
|
+
export declare const setupRouterAtoms: <TRouter extends AnyRouter>(options: RouterAtomsOptions<TRouter>) => void;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { dehydrate, hydrate } from "./Hydration.mjs";
|
|
2
|
+
import { isRequestRegistry } from "./RequestRegistry.mjs";
|
|
3
|
+
import { make } from "./internal/registry.mjs";
|
|
4
|
+
import * as Predicate from "effect/Predicate";
|
|
5
|
+
import { RegistryContext } from "@effect/atom-react";
|
|
6
|
+
import * as React from "react";
|
|
7
|
+
import { jsx } from "react/jsx-runtime";
|
|
8
|
+
// src/TanStackRouter.tsx
|
|
9
|
+
/**
|
|
10
|
+
* Connects the loader verbs to TanStack Router and TanStack Start.
|
|
11
|
+
*
|
|
12
|
+
* `getRouter` runs once per request on the server and once in the browser.
|
|
13
|
+
* It makes the registry with `makeRegistry`, puts `Loader.make(registry)` on
|
|
14
|
+
* the router context for the loaders, and calls `setupRouterAtoms`, which
|
|
15
|
+
* provides the registry to the app and sends it from the server to the
|
|
16
|
+
* browser through the router's own dehydration.
|
|
17
|
+
*
|
|
18
|
+
* **Example**
|
|
19
|
+
*
|
|
20
|
+
* ```tsx
|
|
21
|
+
* import { createRouter } from "@tanstack/react-router"
|
|
22
|
+
* import { Loader } from "raf-effect-atom"
|
|
23
|
+
* import { makeRegistry, setupRouterAtoms } from "raf-effect-atom/tanstack-router"
|
|
24
|
+
*
|
|
25
|
+
* export function getRouter() {
|
|
26
|
+
* const registry = makeRegistry()
|
|
27
|
+
* const router = createRouter({
|
|
28
|
+
* routeTree,
|
|
29
|
+
* context: { atoms: Loader.make(registry) }
|
|
30
|
+
* })
|
|
31
|
+
* setupRouterAtoms({ router, registry })
|
|
32
|
+
* return router
|
|
33
|
+
* }
|
|
34
|
+
* ```
|
|
35
|
+
*
|
|
36
|
+
* @stability unstable
|
|
37
|
+
* @since 0.1.0
|
|
38
|
+
*/
|
|
39
|
+
/**
|
|
40
|
+
* Creates the registry for one `getRouter` call: a request registry on the
|
|
41
|
+
* server, and in the browser a registry like `@effect/atom-react`'s
|
|
42
|
+
* `RegistryProvider` makes.
|
|
43
|
+
*
|
|
44
|
+
* **Details**
|
|
45
|
+
*
|
|
46
|
+
* - The server is anywhere without a `window`. To decide otherwise, make
|
|
47
|
+
* the registry with `RequestRegistry.make` on the server and
|
|
48
|
+
* `AtomRegistry.make` in the browser.
|
|
49
|
+
* - `defaultIdleTTL` is how many milliseconds the browser's registry keeps
|
|
50
|
+
* an atom no component reads, 400 by default, as `@effect/atom-react`'s
|
|
51
|
+
* default registry does. An atom's own `Atom.setIdleTTL` or
|
|
52
|
+
* `Atom.keepAlive` wins. The server's request registry lives for one
|
|
53
|
+
* request, so it has none.
|
|
54
|
+
*
|
|
55
|
+
* **Example**
|
|
56
|
+
*
|
|
57
|
+
* ```ts
|
|
58
|
+
* import { makeRegistry } from "raf-effect-atom/tanstack-router"
|
|
59
|
+
*
|
|
60
|
+
* // Keeps an issue the user navigated away from for a minute.
|
|
61
|
+
* const registry = makeRegistry({ defaultIdleTTL: 60_000 })
|
|
62
|
+
* ```
|
|
63
|
+
*
|
|
64
|
+
* @stability unstable
|
|
65
|
+
* @category constructors
|
|
66
|
+
* @since 0.1.0
|
|
67
|
+
*/
|
|
68
|
+
const makeRegistry = (options) => make(options);
|
|
69
|
+
const hasRouterAtoms = (value) => Predicate.hasProperty(value, "raf-effect-atom") && Array.isArray(value["raf-effect-atom"]);
|
|
70
|
+
/**
|
|
71
|
+
* Provides a registry to a TanStack router's app and streams it from the
|
|
72
|
+
* server to the browser.
|
|
73
|
+
*
|
|
74
|
+
* **Details**
|
|
75
|
+
*
|
|
76
|
+
* - Wraps the app in `RegistryContext.Provider` through the router's `Wrap`
|
|
77
|
+
* option, inside any `Wrap` it has already.
|
|
78
|
+
* - On the server, adds `Hydration.dehydrate(registry)` to the router's
|
|
79
|
+
* dehydrated state once the loaders have run, so results still loading
|
|
80
|
+
* cross as promises on the router's stream, and disposes the registry when
|
|
81
|
+
* the response finishes.
|
|
82
|
+
* - In the browser, hydrates the registry from that state before the app
|
|
83
|
+
* renders.
|
|
84
|
+
*
|
|
85
|
+
* **Gotchas**
|
|
86
|
+
*
|
|
87
|
+
* The server's registry must be a request registry and the browser's must
|
|
88
|
+
* not, so it throws otherwise: a registry shared between requests would send
|
|
89
|
+
* one user's results to another. Only atoms in the registry when the loaders
|
|
90
|
+
* finish are sent; one first read while rendering runs again in the browser.
|
|
91
|
+
*
|
|
92
|
+
* @stability unstable
|
|
93
|
+
* @category integrations
|
|
94
|
+
* @since 0.1.0
|
|
95
|
+
*/
|
|
96
|
+
const setupRouterAtoms = (options) => {
|
|
97
|
+
const router = options.router;
|
|
98
|
+
const registry = options.registry;
|
|
99
|
+
if (router.isServer !== isRequestRegistry(registry)) throw new Error(router.isServer ? "raf-effect-atom: setupRouterAtoms got a server router without a request registry, which would share results between requests. Make it with makeRegistry or RequestRegistry.make." : "raf-effect-atom: setupRouterAtoms got a browser router with a request registry. Make it with makeRegistry or AtomRegistry.make.");
|
|
100
|
+
const Wrap = router.options.Wrap ?? React.Fragment;
|
|
101
|
+
router.options.Wrap = (props) => /* @__PURE__ */ jsx(RegistryContext.Provider, {
|
|
102
|
+
value: registry,
|
|
103
|
+
children: /* @__PURE__ */ jsx(Wrap, { children: props.children })
|
|
104
|
+
});
|
|
105
|
+
if (router.isServer) {
|
|
106
|
+
let disposed = false;
|
|
107
|
+
router.serverSsrLifecycle = {
|
|
108
|
+
...router.serverSsrLifecycle,
|
|
109
|
+
onServerSsrAttach: [...router.serverSsrLifecycle?.onServerSsrAttach ?? [], (serverSsr) => {
|
|
110
|
+
serverSsr.onCleanup(() => {
|
|
111
|
+
disposed = true;
|
|
112
|
+
registry.dispose();
|
|
113
|
+
});
|
|
114
|
+
}]
|
|
115
|
+
};
|
|
116
|
+
const dehydrate$1 = router.options.dehydrate;
|
|
117
|
+
router.options.dehydrate = async () => {
|
|
118
|
+
const dehydrated = await dehydrate$1?.();
|
|
119
|
+
if (disposed) return dehydrated;
|
|
120
|
+
const atoms = { "raf-effect-atom": dehydrate(registry) };
|
|
121
|
+
return Predicate.isObject(dehydrated) ? {
|
|
122
|
+
...dehydrated,
|
|
123
|
+
...atoms
|
|
124
|
+
} : atoms;
|
|
125
|
+
};
|
|
126
|
+
} else {
|
|
127
|
+
const hydrate$1 = router.options.hydrate;
|
|
128
|
+
router.options.hydrate = async (dehydrated) => {
|
|
129
|
+
await hydrate$1?.(dehydrated);
|
|
130
|
+
if (hasRouterAtoms(dehydrated)) hydrate(registry, dehydrated["raf-effect-atom"]);
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
export { makeRegistry, setupRouterAtoms };
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { Mode } from "./internal/waterfalls.mjs";
|
|
2
|
+
import * as Atom from "effect/reactivity/Atom";
|
|
3
|
+
import * as AsyncResult from "effect/reactivity/AsyncResult";
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
/**
|
|
6
|
+
* What `useAtomResult` does in development when a read starts an atom that
|
|
7
|
+
* no loader, server component, event handler or `useWaterfall` started.
|
|
8
|
+
*
|
|
9
|
+
* - `"warn"`: `console.warn` once per atom, naming it. The default.
|
|
10
|
+
* - `"throw"`: throw to the nearest error boundary, so the page can't be
|
|
11
|
+
* used until the atom starts before render.
|
|
12
|
+
* - `"off"`: don't check.
|
|
13
|
+
*
|
|
14
|
+
* @stability unstable
|
|
15
|
+
* @category models
|
|
16
|
+
* @since 0.1.0
|
|
17
|
+
*/
|
|
18
|
+
export type WaterfallMode = Mode;
|
|
19
|
+
/**
|
|
20
|
+
* Sets how `useAtomResult` reports request waterfalls in the tree below.
|
|
21
|
+
*
|
|
22
|
+
* **When to use**
|
|
23
|
+
*
|
|
24
|
+
* Put it in the root layout, around the app. `"throw"` turns every atom a
|
|
25
|
+
* component starts on its own into an error in development, which a person
|
|
26
|
+
* or a coding agent can't miss.
|
|
27
|
+
*
|
|
28
|
+
* **Details**
|
|
29
|
+
*
|
|
30
|
+
* Production builds don't check: the check sits behind
|
|
31
|
+
* `process.env.NODE_ENV !== "production"`, which bundlers replace at build
|
|
32
|
+
* time, so it's left out of the bundle, and this component renders only its
|
|
33
|
+
* children.
|
|
34
|
+
*
|
|
35
|
+
* **Example**
|
|
36
|
+
*
|
|
37
|
+
* ```tsx
|
|
38
|
+
* import { WaterfallDetection } from "raf-effect-atom"
|
|
39
|
+
*
|
|
40
|
+
* function App(props: { readonly children: React.ReactNode }) {
|
|
41
|
+
* return <WaterfallDetection mode="throw">{props.children}</WaterfallDetection>
|
|
42
|
+
* }
|
|
43
|
+
* ```
|
|
44
|
+
*
|
|
45
|
+
* @stability unstable
|
|
46
|
+
* @category components
|
|
47
|
+
* @since 0.1.0
|
|
48
|
+
*/
|
|
49
|
+
export declare const WaterfallDetection: (props: {
|
|
50
|
+
readonly children?: React.ReactNode | undefined;
|
|
51
|
+
readonly mode: WaterfallMode;
|
|
52
|
+
}) => React.ReactNode;
|
|
53
|
+
/**
|
|
54
|
+
* Starts an atom during render and returns it, for an input only a render
|
|
55
|
+
* knows, such as a debounced search query.
|
|
56
|
+
*
|
|
57
|
+
* **When to use**
|
|
58
|
+
*
|
|
59
|
+
* As an escape hatch, when no loader, server component or event handler can
|
|
60
|
+
* start the atom: each call is a request waterfall, a round trip after the
|
|
61
|
+
* render that found the input. Prefer putting the state that picks the data
|
|
62
|
+
* in the URL, where a loader reads it, or a procedure that resolves the input
|
|
63
|
+
* on the server. Searching a codebase for `useWaterfall` lists its
|
|
64
|
+
* waterfalls.
|
|
65
|
+
*
|
|
66
|
+
* **Details**
|
|
67
|
+
*
|
|
68
|
+
* - Starts the atom like `Loader.prefetch`, with a lease that
|
|
69
|
+
* `useAtomResult` takes over when the component mounts, and only if
|
|
70
|
+
* nothing started it yet: re-renders start nothing.
|
|
71
|
+
* - A result the server is still streaming is awaited by `useAtomResult`,
|
|
72
|
+
* not started again.
|
|
73
|
+
* - `useAtomResult` doesn't report an atom started this way.
|
|
74
|
+
*
|
|
75
|
+
* **Gotchas**
|
|
76
|
+
*
|
|
77
|
+
* Pass it the atom a loader would start, inside an error boundary's `wrap`:
|
|
78
|
+
* `useAtomResult(boundary.wrap(useWaterfall(user(id))))`. The wrapper isn't
|
|
79
|
+
* serializable, so the server couldn't send it to the browser.
|
|
80
|
+
*
|
|
81
|
+
* **Example**
|
|
82
|
+
*
|
|
83
|
+
* ```tsx
|
|
84
|
+
* import { useAtomResult, useWaterfall } from "raf-effect-atom"
|
|
85
|
+
*
|
|
86
|
+
* function Author(props: { readonly userId: UserId }) {
|
|
87
|
+
* const result = useAtomResult(useWaterfall(user(props.userId)))
|
|
88
|
+
* // ...
|
|
89
|
+
* }
|
|
90
|
+
* ```
|
|
91
|
+
*
|
|
92
|
+
* @see {@link WaterfallDetection} for the development check it opts out of
|
|
93
|
+
*
|
|
94
|
+
* @stability unstable
|
|
95
|
+
* @category hooks
|
|
96
|
+
* @since 0.1.0
|
|
97
|
+
*/
|
|
98
|
+
export declare const useWaterfall: <Self extends Atom.Atom<AsyncResult.AsyncResult<unknown, unknown>>>(atom: Self) => Self;
|