raf-effect-atom 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +111 -0
- package/LICENSE +21 -0
- package/README.md +65 -2
- package/dist/Handle.d.mts +90 -0
- package/dist/Hooks.d.mts +89 -0
- package/dist/Hooks.mjs +135 -0
- package/dist/Hydration.d.mts +93 -0
- package/dist/Hydration.mjs +134 -0
- package/dist/Loader.d.mts +192 -0
- package/dist/Loader.mjs +286 -0
- package/dist/Next.d.mts +65 -0
- package/dist/Next.mjs +64 -0
- package/dist/NextClient.d.mts +32 -0
- package/dist/NextClient.mjs +44 -0
- package/dist/ReactHydration.d.mts +52 -0
- package/dist/ReactHydration.mjs +71 -0
- package/dist/ReactRouter.d.mts +63 -0
- package/dist/ReactRouter.mjs +116 -0
- package/dist/RegistryProvider.d.mts +40 -0
- package/dist/RegistryProvider.mjs +55 -0
- package/dist/RequestRegistry.d.mts +54 -0
- package/dist/RequestRegistry.mjs +76 -0
- package/dist/TanStackRouter.d.mts +84 -0
- package/dist/TanStackRouter.mjs +134 -0
- package/dist/Waterfalls.d.mts +99 -0
- package/dist/Waterfalls.mjs +111 -0
- package/dist/_virtual/_rolldown/runtime.mjs +12 -0
- package/dist/index.d.mts +8 -0
- package/dist/index.mjs +7 -0
- package/dist/internal/development.mjs +6 -0
- package/dist/internal/handles.mjs +22 -0
- package/dist/internal/leases.mjs +39 -0
- package/dist/internal/next.mjs +16 -0
- package/dist/internal/pending.mjs +75 -0
- package/dist/internal/registry.mjs +16 -0
- package/dist/internal/sources.mjs +14 -0
- package/dist/internal/waterfalls.d.mts +5 -0
- package/dist/internal/waterfalls.mjs +83 -0
- package/package.json +80 -3
|
@@ -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,99 @@
|
|
|
1
|
+
import { ArgsOf, Handle, Source } from "./Handle.mjs";
|
|
2
|
+
import { Mode } from "./internal/waterfalls.mjs";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
/**
|
|
5
|
+
* What `useAtomResult` does in development when a read starts an atom that
|
|
6
|
+
* no loader, server component, event handler or `useWaterfall` started.
|
|
7
|
+
*
|
|
8
|
+
* - `"warn"`: `console.warn` once per atom, naming it. The default.
|
|
9
|
+
* - `"throw"`: throw to the nearest error boundary, so the page can't be
|
|
10
|
+
* used until the atom starts before render.
|
|
11
|
+
* - `"off"`: don't check.
|
|
12
|
+
*
|
|
13
|
+
* @stability unstable
|
|
14
|
+
* @category models
|
|
15
|
+
* @since 0.1.0
|
|
16
|
+
*/
|
|
17
|
+
export type WaterfallMode = Mode;
|
|
18
|
+
/**
|
|
19
|
+
* Sets how `useAtomResult` reports request waterfalls in the tree below.
|
|
20
|
+
*
|
|
21
|
+
* **When to use**
|
|
22
|
+
*
|
|
23
|
+
* Put it in the root layout, around the app. `"throw"` turns every atom a
|
|
24
|
+
* component starts on its own into an error in development, which a person
|
|
25
|
+
* or a coding agent can't miss.
|
|
26
|
+
*
|
|
27
|
+
* **Details**
|
|
28
|
+
*
|
|
29
|
+
* Production builds don't check: the check sits behind
|
|
30
|
+
* `process.env.NODE_ENV !== "production"`, which bundlers replace at build
|
|
31
|
+
* time, so it's left out of the bundle, and this component renders only its
|
|
32
|
+
* children.
|
|
33
|
+
*
|
|
34
|
+
* **Example**
|
|
35
|
+
*
|
|
36
|
+
* ```tsx
|
|
37
|
+
* import { WaterfallDetection } from "raf-effect-atom"
|
|
38
|
+
*
|
|
39
|
+
* function App(props: { readonly children: React.ReactNode }) {
|
|
40
|
+
* return <WaterfallDetection mode="throw">{props.children}</WaterfallDetection>
|
|
41
|
+
* }
|
|
42
|
+
* ```
|
|
43
|
+
*
|
|
44
|
+
* @stability unstable
|
|
45
|
+
* @category components
|
|
46
|
+
* @since 0.1.0
|
|
47
|
+
*/
|
|
48
|
+
export declare const WaterfallDetection: (props: {
|
|
49
|
+
readonly children?: React.ReactNode | undefined;
|
|
50
|
+
readonly mode: WaterfallMode;
|
|
51
|
+
}) => React.ReactNode;
|
|
52
|
+
/**
|
|
53
|
+
* Starts a source's atom during render and returns its handle, for an input
|
|
54
|
+
* only a render knows, such as a debounced search query.
|
|
55
|
+
*
|
|
56
|
+
* **When to use**
|
|
57
|
+
*
|
|
58
|
+
* As an escape hatch, when no loader, server component or event handler can
|
|
59
|
+
* start the atom: each call is a request waterfall, a round trip after the
|
|
60
|
+
* render that found the input. Prefer putting the state that picks the data
|
|
61
|
+
* in the URL, where a loader reads it, or a procedure that resolves the input
|
|
62
|
+
* on the server. Searching a codebase for `useWaterfall` lists its
|
|
63
|
+
* waterfalls.
|
|
64
|
+
*
|
|
65
|
+
* **Details**
|
|
66
|
+
*
|
|
67
|
+
* - Starts the atom like `Loader.prefetch`, with a lease that
|
|
68
|
+
* `useAtomResult` takes over when the component mounts, and only if
|
|
69
|
+
* nothing started it yet: re-renders start nothing.
|
|
70
|
+
* - A result the server is still streaming is awaited by `useAtomResult`,
|
|
71
|
+
* not started again.
|
|
72
|
+
* - `useAtomResult` doesn't report an atom started this way.
|
|
73
|
+
* - The handle is a prefetched one: its reader suspends until the result
|
|
74
|
+
* settles, as for a handle from a loader's `prefetch`.
|
|
75
|
+
*
|
|
76
|
+
* **Gotchas**
|
|
77
|
+
*
|
|
78
|
+
* Pass it the source a loader would start, not an error boundary's
|
|
79
|
+
* `wrap(source)`: the wrapper's atoms aren't serializable, so the server
|
|
80
|
+
* couldn't send them to the browser. Read the handle through the wrapper.
|
|
81
|
+
*
|
|
82
|
+
* **Example**
|
|
83
|
+
*
|
|
84
|
+
* ```tsx
|
|
85
|
+
* import { useAtomResult, useWaterfall } from "raf-effect-atom"
|
|
86
|
+
*
|
|
87
|
+
* function Author(props: { readonly userId: UserId }) {
|
|
88
|
+
* const result = useAtomResult(user, useWaterfall(user, props.userId))
|
|
89
|
+
* // ...
|
|
90
|
+
* }
|
|
91
|
+
* ```
|
|
92
|
+
*
|
|
93
|
+
* @see {@link WaterfallDetection} for the development check it opts out of
|
|
94
|
+
*
|
|
95
|
+
* @stability unstable
|
|
96
|
+
* @category hooks
|
|
97
|
+
* @since 0.1.0
|
|
98
|
+
*/
|
|
99
|
+
export declare const useWaterfall: <S extends Source>(source: S, ...args: ArgsOf<S>) => Handle<S>;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { get } from "./internal/pending.mjs";
|
|
3
|
+
import { hydrated } from "./Hydration.mjs";
|
|
4
|
+
import { keyOf } from "./internal/leases.mjs";
|
|
5
|
+
import { atomOf, make } from "./internal/handles.mjs";
|
|
6
|
+
import { prefetch } from "./Loader.mjs";
|
|
7
|
+
import { ModeContext, isStarted } from "./internal/waterfalls.mjs";
|
|
8
|
+
import * as Predicate from "effect/Predicate";
|
|
9
|
+
import * as Effect from "effect/Effect";
|
|
10
|
+
import * as AtomRegistry from "effect/reactivity/AtomRegistry";
|
|
11
|
+
import { RegistryContext } from "@effect/atom-react";
|
|
12
|
+
import * as React from "react";
|
|
13
|
+
import { jsx } from "react/jsx-runtime";
|
|
14
|
+
// src/Waterfalls.tsx
|
|
15
|
+
/**
|
|
16
|
+
* Request waterfalls: the escape hatch for atoms only a render can start,
|
|
17
|
+
* and the development check that finds the others.
|
|
18
|
+
*
|
|
19
|
+
* @stability unstable
|
|
20
|
+
* @since 0.1.0
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* Sets how `useAtomResult` reports request waterfalls in the tree below.
|
|
24
|
+
*
|
|
25
|
+
* **When to use**
|
|
26
|
+
*
|
|
27
|
+
* Put it in the root layout, around the app. `"throw"` turns every atom a
|
|
28
|
+
* component starts on its own into an error in development, which a person
|
|
29
|
+
* or a coding agent can't miss.
|
|
30
|
+
*
|
|
31
|
+
* **Details**
|
|
32
|
+
*
|
|
33
|
+
* Production builds don't check: the check sits behind
|
|
34
|
+
* `process.env.NODE_ENV !== "production"`, which bundlers replace at build
|
|
35
|
+
* time, so it's left out of the bundle, and this component renders only its
|
|
36
|
+
* children.
|
|
37
|
+
*
|
|
38
|
+
* **Example**
|
|
39
|
+
*
|
|
40
|
+
* ```tsx
|
|
41
|
+
* import { WaterfallDetection } from "raf-effect-atom"
|
|
42
|
+
*
|
|
43
|
+
* function App(props: { readonly children: React.ReactNode }) {
|
|
44
|
+
* return <WaterfallDetection mode="throw">{props.children}</WaterfallDetection>
|
|
45
|
+
* }
|
|
46
|
+
* ```
|
|
47
|
+
*
|
|
48
|
+
* @stability unstable
|
|
49
|
+
* @category components
|
|
50
|
+
* @since 0.1.0
|
|
51
|
+
*/
|
|
52
|
+
const WaterfallDetection = (props) => process.env.NODE_ENV === "production" ? props.children : /* @__PURE__ */ jsx(ModeContext, {
|
|
53
|
+
value: props.mode,
|
|
54
|
+
children: props.children
|
|
55
|
+
});
|
|
56
|
+
/**
|
|
57
|
+
* Starts a source's atom during render and returns its handle, for an input
|
|
58
|
+
* only a render knows, such as a debounced search query.
|
|
59
|
+
*
|
|
60
|
+
* **When to use**
|
|
61
|
+
*
|
|
62
|
+
* As an escape hatch, when no loader, server component or event handler can
|
|
63
|
+
* start the atom: each call is a request waterfall, a round trip after the
|
|
64
|
+
* render that found the input. Prefer putting the state that picks the data
|
|
65
|
+
* in the URL, where a loader reads it, or a procedure that resolves the input
|
|
66
|
+
* on the server. Searching a codebase for `useWaterfall` lists its
|
|
67
|
+
* waterfalls.
|
|
68
|
+
*
|
|
69
|
+
* **Details**
|
|
70
|
+
*
|
|
71
|
+
* - Starts the atom like `Loader.prefetch`, with a lease that
|
|
72
|
+
* `useAtomResult` takes over when the component mounts, and only if
|
|
73
|
+
* nothing started it yet: re-renders start nothing.
|
|
74
|
+
* - A result the server is still streaming is awaited by `useAtomResult`,
|
|
75
|
+
* not started again.
|
|
76
|
+
* - `useAtomResult` doesn't report an atom started this way.
|
|
77
|
+
* - The handle is a prefetched one: its reader suspends until the result
|
|
78
|
+
* settles, as for a handle from a loader's `prefetch`.
|
|
79
|
+
*
|
|
80
|
+
* **Gotchas**
|
|
81
|
+
*
|
|
82
|
+
* Pass it the source a loader would start, not an error boundary's
|
|
83
|
+
* `wrap(source)`: the wrapper's atoms aren't serializable, so the server
|
|
84
|
+
* couldn't send them to the browser. Read the handle through the wrapper.
|
|
85
|
+
*
|
|
86
|
+
* **Example**
|
|
87
|
+
*
|
|
88
|
+
* ```tsx
|
|
89
|
+
* import { useAtomResult, useWaterfall } from "raf-effect-atom"
|
|
90
|
+
*
|
|
91
|
+
* function Author(props: { readonly userId: UserId }) {
|
|
92
|
+
* const result = useAtomResult(user, useWaterfall(user, props.userId))
|
|
93
|
+
* // ...
|
|
94
|
+
* }
|
|
95
|
+
* ```
|
|
96
|
+
*
|
|
97
|
+
* @see {@link WaterfallDetection} for the development check it opts out of
|
|
98
|
+
*
|
|
99
|
+
* @stability unstable
|
|
100
|
+
* @category hooks
|
|
101
|
+
* @since 0.1.0
|
|
102
|
+
*/
|
|
103
|
+
const useWaterfall = (source, ...args) => {
|
|
104
|
+
const registry = React.useContext(RegistryContext);
|
|
105
|
+
const atom = atomOf(source, args);
|
|
106
|
+
const target = hydrated(atom);
|
|
107
|
+
const key = keyOf(target);
|
|
108
|
+
if (!(Predicate.isString(key) && get(registry, key) !== void 0) && !isStarted(registry, target)) return Effect.runSync(Effect.provideService(prefetch(source, ...args), AtomRegistry.AtomRegistry, registry));
|
|
109
|
+
return make(args, atom);
|
|
110
|
+
};
|
|
111
|
+
export { WaterfallDetection, useWaterfall };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// \0rolldown/runtime.js
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __exportAll = (all, no_symbols) => {
|
|
4
|
+
let target = {};
|
|
5
|
+
for (var name in all) __defProp(target, name, {
|
|
6
|
+
get: all[name],
|
|
7
|
+
enumerable: true
|
|
8
|
+
});
|
|
9
|
+
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
10
|
+
return target;
|
|
11
|
+
};
|
|
12
|
+
export { __exportAll };
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { ArgsOf, ErrorOf, Handle, HandleKey, LoadedHandle, Source, SuccessOf } from "./Handle.mjs";
|
|
2
|
+
import { HydrationBoundary, HydrationBoundaryProps } from "./ReactHydration.mjs";
|
|
3
|
+
import { Loader_d_exports } from "./Loader.mjs";
|
|
4
|
+
import { Hydration_d_exports } from "./Hydration.mjs";
|
|
5
|
+
import { RequestRegistry_d_exports } from "./RequestRegistry.mjs";
|
|
6
|
+
import { useAtomResult } from "./Hooks.mjs";
|
|
7
|
+
import { WaterfallDetection, WaterfallMode, useWaterfall } from "./Waterfalls.mjs";
|
|
8
|
+
export { ArgsOf, ErrorOf, Handle, HandleKey, Hydration_d_exports as Hydration, HydrationBoundary, HydrationBoundaryProps, LoadedHandle, Loader_d_exports as Loader, RequestRegistry_d_exports as RequestRegistry, Source, SuccessOf, WaterfallDetection, WaterfallMode, useAtomResult, useWaterfall };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Hydration_exports } from "./Hydration.mjs";
|
|
2
|
+
import { RequestRegistry_exports } from "./RequestRegistry.mjs";
|
|
3
|
+
import { Loader_exports } from "./Loader.mjs";
|
|
4
|
+
import { useAtomResult } from "./Hooks.mjs";
|
|
5
|
+
import { HydrationBoundary } from "./ReactHydration.mjs";
|
|
6
|
+
import { WaterfallDetection, useWaterfall } from "./Waterfalls.mjs";
|
|
7
|
+
export { Hydration_exports as Hydration, HydrationBoundary, Loader_exports as Loader, RequestRegistry_exports as RequestRegistry, WaterfallDetection, useAtomResult, useWaterfall };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import * as Config from "effect/Config";
|
|
3
|
+
// src/internal/development.ts
|
|
4
|
+
/** Whether `NODE_ENV` isn't `production`, read through `Config`. */
|
|
5
|
+
const isDevelopment = Config.String("NODE_ENV").pipe(Config.withDefault("development"), Effect.map((env) => env !== "production"), Effect.orElseSucceed(() => true));
|
|
6
|
+
export { isDevelopment };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { keyOf as keyOf$1 } from "./leases.mjs";
|
|
2
|
+
import * as Atom from "effect/reactivity/Atom";
|
|
3
|
+
import * as Predicate from "effect/Predicate";
|
|
4
|
+
// src/internal/handles.ts
|
|
5
|
+
/** The atom a source makes for its arguments. */
|
|
6
|
+
const atomOf = (source, args) => Atom.isAtom(source) ? source : source(...args);
|
|
7
|
+
/** The registry key a handle records: the atom's serialization key. */
|
|
8
|
+
const keyOf = (atom) => {
|
|
9
|
+
const key = keyOf$1(atom);
|
|
10
|
+
if (!Predicate.isString(key)) return;
|
|
11
|
+
return key;
|
|
12
|
+
};
|
|
13
|
+
const make = (args, atom) => ({
|
|
14
|
+
args,
|
|
15
|
+
key: keyOf(atom)
|
|
16
|
+
});
|
|
17
|
+
const loaded = (args, atom) => ({
|
|
18
|
+
args,
|
|
19
|
+
key: keyOf(atom),
|
|
20
|
+
loaded: true
|
|
21
|
+
});
|
|
22
|
+
export { atomOf, loaded, make };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import * as Atom from "effect/reactivity/Atom";
|
|
2
|
+
// src/internal/leases.ts
|
|
3
|
+
const leases = /* @__PURE__ */ new WeakMap();
|
|
4
|
+
/** The registry's key for an atom: its serialization key, or the atom. */
|
|
5
|
+
const keyOf = (atom) => Atom.isSerializable(atom) ? atom[Atom.SerializableTypeId].key : atom;
|
|
6
|
+
/**
|
|
7
|
+
* Mounts `atom` until `release`, or until `ttl` milliseconds pass. Taking a
|
|
8
|
+
* lease the atom already has restarts its timer.
|
|
9
|
+
*/
|
|
10
|
+
const take = (registry, atom, ttl) => {
|
|
11
|
+
let byKey = leases.get(registry);
|
|
12
|
+
if (byKey === void 0) {
|
|
13
|
+
byKey = /* @__PURE__ */ new Map();
|
|
14
|
+
leases.set(registry, byKey);
|
|
15
|
+
}
|
|
16
|
+
const key = keyOf(atom);
|
|
17
|
+
let lease = byKey.get(key);
|
|
18
|
+
if (lease === void 0) {
|
|
19
|
+
const unmount = registry.mount(atom);
|
|
20
|
+
const owner = byKey;
|
|
21
|
+
const created = {
|
|
22
|
+
release: () => {
|
|
23
|
+
clearTimeout(created.timer);
|
|
24
|
+
owner.delete(key);
|
|
25
|
+
unmount();
|
|
26
|
+
},
|
|
27
|
+
timer: void 0
|
|
28
|
+
};
|
|
29
|
+
byKey.set(key, created);
|
|
30
|
+
lease = created;
|
|
31
|
+
}
|
|
32
|
+
clearTimeout(lease.timer);
|
|
33
|
+
lease.timer = ttl === void 0 ? void 0 : setTimeout(lease.release, ttl);
|
|
34
|
+
};
|
|
35
|
+
/** Releases `atom`'s lease, if it has one. */
|
|
36
|
+
const release = (registry, atom) => {
|
|
37
|
+
leases.get(registry)?.get(keyOf(atom))?.release();
|
|
38
|
+
};
|
|
39
|
+
export { keyOf, release, take };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { dehydrate as dehydrate$1 } from "../Hydration.mjs";
|
|
2
|
+
import { make } from "../RequestRegistry.mjs";
|
|
3
|
+
import { make as make$1 } from "../Loader.mjs";
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
// src/internal/next.ts
|
|
6
|
+
const getRegistry = React.cache(() => {
|
|
7
|
+
const registry = make();
|
|
8
|
+
React.cacheSignal()?.addEventListener("abort", () => registry.dispose(), { once: true });
|
|
9
|
+
return registry;
|
|
10
|
+
});
|
|
11
|
+
const getLoader = React.cache(() => make$1(getRegistry()));
|
|
12
|
+
const prefetch = (source, ...args) => getLoader().prefetch(source, ...args);
|
|
13
|
+
const ensure = (source, ...args) => getLoader().ensure(source, ...args);
|
|
14
|
+
const fetch = (source, ...args) => getLoader().fetch(source, ...args);
|
|
15
|
+
const dehydrate = () => dehydrate$1(getRegistry());
|
|
16
|
+
export { dehydrate, ensure, fetch, getRegistry, prefetch };
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import * as Hydration from "effect/reactivity/Hydration";
|
|
2
|
+
import * as Predicate from "effect/Predicate";
|
|
3
|
+
// src/internal/pending.ts
|
|
4
|
+
const held = /* @__PURE__ */ new WeakMap();
|
|
5
|
+
const pending = /* @__PURE__ */ new WeakMap();
|
|
6
|
+
/** Each key's latest stream, kept once it settles. */
|
|
7
|
+
const streams = /* @__PURE__ */ new WeakMap();
|
|
8
|
+
/** The promise of a streamed result still on its way, if there is one. */
|
|
9
|
+
const get = (registry, key) => pending.get(registry)?.get(key);
|
|
10
|
+
/**
|
|
11
|
+
* The promise of a key's streamed result, settled or not. A component that
|
|
12
|
+
* suspended on it with `React.use` must call `React.use` again when React
|
|
13
|
+
* retries it, or React reports a conditional `use`. Once settled, it's
|
|
14
|
+
* marked fulfilled the way React marks the thenables it tracks, so `use`
|
|
15
|
+
* returns at once instead of suspending again.
|
|
16
|
+
*/
|
|
17
|
+
const stream = (registry, key) => streams.get(registry)?.get(key);
|
|
18
|
+
const hold = (registry, entry, result) => {
|
|
19
|
+
let entries = held.get(registry);
|
|
20
|
+
if (entries === void 0) {
|
|
21
|
+
entries = /* @__PURE__ */ new WeakSet();
|
|
22
|
+
held.set(registry, entries);
|
|
23
|
+
}
|
|
24
|
+
if (entries.has(entry)) return;
|
|
25
|
+
entries.add(entry);
|
|
26
|
+
let byKey = pending.get(registry);
|
|
27
|
+
if (byKey === void 0) {
|
|
28
|
+
byKey = /* @__PURE__ */ new Map();
|
|
29
|
+
pending.set(registry, byKey);
|
|
30
|
+
}
|
|
31
|
+
if (byKey.has(entry.key)) return;
|
|
32
|
+
let latest = streams.get(registry);
|
|
33
|
+
if (latest === void 0) {
|
|
34
|
+
latest = /* @__PURE__ */ new Map();
|
|
35
|
+
streams.set(registry, latest);
|
|
36
|
+
}
|
|
37
|
+
const owner = byKey;
|
|
38
|
+
const done = () => {
|
|
39
|
+
owner.delete(entry.key);
|
|
40
|
+
};
|
|
41
|
+
const fulfilled = () => {
|
|
42
|
+
Object.assign(promise, {
|
|
43
|
+
status: "fulfilled",
|
|
44
|
+
value: void 0
|
|
45
|
+
});
|
|
46
|
+
};
|
|
47
|
+
const promise = Promise.resolve(result).then((value) => {
|
|
48
|
+
done();
|
|
49
|
+
if (!Predicate.isSymbol(value)) {
|
|
50
|
+
const resolved = {
|
|
51
|
+
...entry,
|
|
52
|
+
resultPromise: void 0,
|
|
53
|
+
value
|
|
54
|
+
};
|
|
55
|
+
Hydration.hydrate(registry, [resolved]);
|
|
56
|
+
}
|
|
57
|
+
fulfilled();
|
|
58
|
+
}, () => {
|
|
59
|
+
done();
|
|
60
|
+
fulfilled();
|
|
61
|
+
});
|
|
62
|
+
owner.set(entry.key, promise);
|
|
63
|
+
latest.set(entry.key, promise);
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* Holds back each streamed entry of `state` until its promise resolves, and
|
|
67
|
+
* returns the settled ones.
|
|
68
|
+
*/
|
|
69
|
+
const split = (registry, state) => {
|
|
70
|
+
const settled = [];
|
|
71
|
+
for (const entry of Hydration.toValues(Array.from(state))) if (entry.resultPromise === void 0) settled.push(entry);
|
|
72
|
+
else hold(registry, entry, entry.resultPromise);
|
|
73
|
+
return settled;
|
|
74
|
+
};
|
|
75
|
+
export { get, split, stream };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { make as make$1 } from "../RequestRegistry.mjs";
|
|
2
|
+
import * as AtomRegistry from "effect/reactivity/AtomRegistry";
|
|
3
|
+
import { scheduleTask } from "@effect/atom-react";
|
|
4
|
+
// src/internal/registry.ts
|
|
5
|
+
const isServer = () => !("window" in globalThis);
|
|
6
|
+
/** `@effect/atom-react`'s default registry keeps an unused atom this long. */
|
|
7
|
+
const defaultIdleTTL = 400;
|
|
8
|
+
/**
|
|
9
|
+
* A request registry on the server, and in the browser a registry like
|
|
10
|
+
* `@effect/atom-react`'s `RegistryProvider` makes.
|
|
11
|
+
*/
|
|
12
|
+
const make = (options) => isServer() ? make$1() : AtomRegistry.make({
|
|
13
|
+
defaultIdleTTL: options?.defaultIdleTTL ?? defaultIdleTTL,
|
|
14
|
+
scheduleTask
|
|
15
|
+
});
|
|
16
|
+
export { isServer, make };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// src/internal/sources.ts
|
|
2
|
+
/** The registry each `dehydrate` result came from, while it's referenced. */
|
|
3
|
+
const sources = /* @__PURE__ */ new WeakMap();
|
|
4
|
+
const remember = (state, registry) => {
|
|
5
|
+
sources.set(state, registry);
|
|
6
|
+
return state;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* The registry `state` was dehydrated from, when it's the same object
|
|
10
|
+
* `dehydrate` returned: during server rendering by a framework that passes
|
|
11
|
+
* loader data to components without serializing it, such as React Router.
|
|
12
|
+
*/
|
|
13
|
+
const sourceOf = (state) => state === void 0 ? void 0 : sources.get(state);
|
|
14
|
+
export { remember, sourceOf };
|