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,134 @@
|
|
|
1
|
+
import { __exportAll } from "./_virtual/_rolldown/runtime.mjs";
|
|
2
|
+
import { split } from "./internal/pending.mjs";
|
|
3
|
+
import { remember } from "./internal/sources.mjs";
|
|
4
|
+
import * as Atom from "effect/reactivity/Atom";
|
|
5
|
+
import * as EffectHydration from "effect/reactivity/Hydration";
|
|
6
|
+
// src/Hydration.ts
|
|
7
|
+
/**
|
|
8
|
+
* Streams a server registry into the browser's, typed failures intact, and
|
|
9
|
+
* hydrates it without requesting anything again.
|
|
10
|
+
*
|
|
11
|
+
* `dehydrate` encodes the request registry's serializable atoms. Settled
|
|
12
|
+
* results cross as values; results still loading cross as promises that
|
|
13
|
+
* resolve when they settle, carried by the framework's own streaming.
|
|
14
|
+
* `hydrate` applies settled values at once and holds each streamed result
|
|
15
|
+
* until its promise resolves, so `useAtomResult` waits for it instead of
|
|
16
|
+
* starting the atom. `hydrated` keeps a hydrated value on the atom's first
|
|
17
|
+
* read instead of running its effect again.
|
|
18
|
+
*
|
|
19
|
+
* Typed failures survive because a serializable `AsyncResult` atom, such as
|
|
20
|
+
* an `AtomRpc` query with a `serializationKey`, encodes its `Cause` with the
|
|
21
|
+
* procedure's error Schemas: a server-side `IssueNotFound` decodes as an
|
|
22
|
+
* `IssueNotFound` in the browser.
|
|
23
|
+
*
|
|
24
|
+
* @stability unstable
|
|
25
|
+
* @since 0.1.0
|
|
26
|
+
*/
|
|
27
|
+
var Hydration_exports = /* @__PURE__ */ __exportAll({
|
|
28
|
+
dehydrate: () => dehydrate,
|
|
29
|
+
hydrate: () => hydrate,
|
|
30
|
+
hydrated: () => hydrated
|
|
31
|
+
});
|
|
32
|
+
const copies = /* @__PURE__ */ new WeakMap();
|
|
33
|
+
/**
|
|
34
|
+
* Returns a serializable atom that keeps its hydrated value on its first
|
|
35
|
+
* read, instead of running its effect again.
|
|
36
|
+
*
|
|
37
|
+
* **When to use**
|
|
38
|
+
*
|
|
39
|
+
* The loader verbs and `useAtomResult` already read through it. Use it when
|
|
40
|
+
* reading a hydrated atom with `@effect/atom-react`'s own hooks.
|
|
41
|
+
*
|
|
42
|
+
* **Details**
|
|
43
|
+
*
|
|
44
|
+
* The copy shares the atom's serialization key, so both read the same
|
|
45
|
+
* registry node. It sets `skipInitialRun` on the atom's `Serializable`
|
|
46
|
+
* metadata, and is memoized per atom, so every call returns the same copy.
|
|
47
|
+
* An atom that isn't serializable, or already skips its initial run, is
|
|
48
|
+
* returned as it is.
|
|
49
|
+
*
|
|
50
|
+
* **Gotchas**
|
|
51
|
+
*
|
|
52
|
+
* The copy keeps the hydrated value only for the first read: a refresh, or a
|
|
53
|
+
* change to a dependency, runs the effect as usual. An `Initial` value never
|
|
54
|
+
* skips the effect, which is why a result the server was still loading
|
|
55
|
+
* crosses as a promise that `useAtomResult` waits for.
|
|
56
|
+
*
|
|
57
|
+
* **Example**
|
|
58
|
+
*
|
|
59
|
+
* ```ts
|
|
60
|
+
* import { useAtomValue } from "@effect/atom-react"
|
|
61
|
+
* import { Hydration } from "raf-effect-atom"
|
|
62
|
+
*
|
|
63
|
+
* const result = useAtomValue(Hydration.hydrated(issue(1)))
|
|
64
|
+
* ```
|
|
65
|
+
*
|
|
66
|
+
* @see {@link Atom.serializable} for the metadata it copies
|
|
67
|
+
*
|
|
68
|
+
* @stability unstable
|
|
69
|
+
* @category combinators
|
|
70
|
+
* @since 0.1.0
|
|
71
|
+
*/
|
|
72
|
+
const hydrated = (atom) => {
|
|
73
|
+
if (!Atom.isSerializable(atom) || atom[Atom.SerializableTypeId].skipInitialRun === true) return atom;
|
|
74
|
+
const cached = copies.get(atom);
|
|
75
|
+
if (cached !== void 0) return cached;
|
|
76
|
+
const copy = Object.assign(Object.create(Object.getPrototypeOf(atom)), {
|
|
77
|
+
...atom,
|
|
78
|
+
[Atom.SerializableTypeId]: {
|
|
79
|
+
...atom[Atom.SerializableTypeId],
|
|
80
|
+
skipInitialRun: true
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
copies.set(atom, copy);
|
|
84
|
+
return copy;
|
|
85
|
+
};
|
|
86
|
+
/**
|
|
87
|
+
* Encodes a request registry's serializable atoms for the browser.
|
|
88
|
+
*
|
|
89
|
+
* **Details**
|
|
90
|
+
*
|
|
91
|
+
* `Hydration.dehydrate` with `encodeInitialAs: "promise"`: a settled result
|
|
92
|
+
* crosses as its encoded value, and a result still loading crosses as a
|
|
93
|
+
* promise of its encoded value, which resolves when it settles. The
|
|
94
|
+
* framework's serializer carries the promises (TanStack Router's dehydrated
|
|
95
|
+
* state, React's server component props, React Router's Single Fetch).
|
|
96
|
+
*
|
|
97
|
+
* **Gotchas**
|
|
98
|
+
*
|
|
99
|
+
* Only atoms in the registry when it's called are included, so call it once
|
|
100
|
+
* the loaders have started theirs.
|
|
101
|
+
*
|
|
102
|
+
* @see {@link hydrate} for the browser side
|
|
103
|
+
*
|
|
104
|
+
* @stability unstable
|
|
105
|
+
* @category dehydration
|
|
106
|
+
* @since 0.1.0
|
|
107
|
+
*/
|
|
108
|
+
const dehydrate = (registry) => remember(EffectHydration.dehydrate(registry, { encodeInitialAs: "promise" }), registry);
|
|
109
|
+
/**
|
|
110
|
+
* Applies dehydrated state to the browser's registry.
|
|
111
|
+
*
|
|
112
|
+
* **When to use**
|
|
113
|
+
*
|
|
114
|
+
* Use outside React, such as in a router's `hydrate` hook, before the app
|
|
115
|
+
* renders. Inside React, use `HydrationBoundary`.
|
|
116
|
+
*
|
|
117
|
+
* **Details**
|
|
118
|
+
*
|
|
119
|
+
* Settled values go through `Hydration.hydrate`. A streamed result is held
|
|
120
|
+
* until its promise resolves, then hydrated; until then `useAtomResult`
|
|
121
|
+
* suspends on it instead of starting the atom, which would request it again.
|
|
122
|
+
* A streamed result that can't be decoded, or whose promise rejects, is
|
|
123
|
+
* dropped, and the atom runs in the browser.
|
|
124
|
+
*
|
|
125
|
+
* @see {@link dehydrate} for the server side
|
|
126
|
+
*
|
|
127
|
+
* @stability unstable
|
|
128
|
+
* @category hydration
|
|
129
|
+
* @since 0.1.0
|
|
130
|
+
*/
|
|
131
|
+
const hydrate = (registry, state) => {
|
|
132
|
+
EffectHydration.hydrate(registry, split(registry, state));
|
|
133
|
+
};
|
|
134
|
+
export { Hydration_exports, dehydrate, hydrate, hydrated };
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { ArgsOf, ErrorOf, Handle, LoadedHandle, Source, SuccessOf } from "./Handle.mjs";
|
|
2
|
+
import * as Context from "effect/Context";
|
|
3
|
+
import * as Duration from "effect/Duration";
|
|
4
|
+
import * as Effect from "effect/Effect";
|
|
5
|
+
import * as AsyncResult from "effect/reactivity/AsyncResult";
|
|
6
|
+
import * as AtomRegistry from "effect/reactivity/AtomRegistry";
|
|
7
|
+
declare namespace Loader_d_exports {
|
|
8
|
+
export { LeaseTTL, Loader, SettledResult, ensure, fetch, make, prefetch, result };
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* A settled `AsyncResult`: what `ensure` and `fetch` return.
|
|
12
|
+
*
|
|
13
|
+
* @stability unstable
|
|
14
|
+
* @category models
|
|
15
|
+
* @since 0.1.0
|
|
16
|
+
*/
|
|
17
|
+
export type SettledResult<A, E> = AsyncResult.Failure<A, E> | AsyncResult.Success<A, E>;
|
|
18
|
+
/**
|
|
19
|
+
* How long a loader's lease keeps an atom mounted when no component takes it
|
|
20
|
+
* over. Default 30 seconds.
|
|
21
|
+
*
|
|
22
|
+
* **Details**
|
|
23
|
+
*
|
|
24
|
+
* A lease ends when `useAtomResult` mounts the atom, or when this time has
|
|
25
|
+
* passed, after which the registry's idle TTL applies as usual. On a request
|
|
26
|
+
* registry, leases last until the registry is disposed.
|
|
27
|
+
*
|
|
28
|
+
* @stability unstable
|
|
29
|
+
* @category references
|
|
30
|
+
* @since 0.1.0
|
|
31
|
+
*/
|
|
32
|
+
export declare const LeaseTTL: Context.Reference<Duration.Duration>;
|
|
33
|
+
/**
|
|
34
|
+
* Starts a source's atom without waiting for it, and returns its handle.
|
|
35
|
+
*
|
|
36
|
+
* **When to use**
|
|
37
|
+
*
|
|
38
|
+
* Use for data the route needs but its loader doesn't: it streams in while
|
|
39
|
+
* the route renders.
|
|
40
|
+
*
|
|
41
|
+
* **Details**
|
|
42
|
+
*
|
|
43
|
+
* Starts the atom if it isn't in the registry, and leaves it alone if it is.
|
|
44
|
+
* Takes a lease either way. The component that reads the handle suspends
|
|
45
|
+
* until the result settles.
|
|
46
|
+
*
|
|
47
|
+
* **Example**
|
|
48
|
+
*
|
|
49
|
+
* ```ts
|
|
50
|
+
* import { Effect } from "effect"
|
|
51
|
+
* import { Loader } from "raf-effect-atom"
|
|
52
|
+
*
|
|
53
|
+
* const program = Loader.prefetch(commentPage, issueId, 0)
|
|
54
|
+
* ```
|
|
55
|
+
*
|
|
56
|
+
* @stability unstable
|
|
57
|
+
* @category verbs
|
|
58
|
+
* @since 0.1.0
|
|
59
|
+
*/
|
|
60
|
+
export declare const prefetch: <S extends Source>(source: S, ...args: ArgsOf<S>) => Effect.Effect<Handle<S>, never, AtomRegistry.AtomRegistry>;
|
|
61
|
+
/**
|
|
62
|
+
* Starts a source's atom, waits only while it has no result, and returns a
|
|
63
|
+
* handle whose reader renders without suspending.
|
|
64
|
+
*
|
|
65
|
+
* **When to use**
|
|
66
|
+
*
|
|
67
|
+
* Use when the loader needs the result, such as to redirect on a tag, or
|
|
68
|
+
* when the route shouldn't render without it.
|
|
69
|
+
*
|
|
70
|
+
* **Details**
|
|
71
|
+
*
|
|
72
|
+
* An atom with a result returns at once, without refetching. On a request
|
|
73
|
+
* registry, it also waits out a refresh in flight, so the server never
|
|
74
|
+
* renders a result that's about to change. A typed failure settles it like
|
|
75
|
+
* a success: `result` returns it. A defect or an interruption fails the
|
|
76
|
+
* Effect.
|
|
77
|
+
*
|
|
78
|
+
* **Example**
|
|
79
|
+
*
|
|
80
|
+
* ```ts
|
|
81
|
+
* import { Effect } from "effect"
|
|
82
|
+
* import { AsyncResult } from "effect/reactivity"
|
|
83
|
+
* import { Loader } from "raf-effect-atom"
|
|
84
|
+
*
|
|
85
|
+
* const program = Effect.gen(function* () {
|
|
86
|
+
* const handle = yield* Loader.ensure(issue, 1)
|
|
87
|
+
* const result = Loader.result(handle)
|
|
88
|
+
* return AsyncResult.isSuccess(result) ? result.value.title : "missing"
|
|
89
|
+
* })
|
|
90
|
+
* ```
|
|
91
|
+
*
|
|
92
|
+
* @stability unstable
|
|
93
|
+
* @category verbs
|
|
94
|
+
* @since 0.1.0
|
|
95
|
+
*/
|
|
96
|
+
export declare const ensure: <S extends Source>(source: S, ...args: ArgsOf<S>) => Effect.Effect<LoadedHandle<S>, never, AtomRegistry.AtomRegistry>;
|
|
97
|
+
/**
|
|
98
|
+
* Starts a source's atom, or refreshes it if it has a result, waits for the
|
|
99
|
+
* new result to settle, and returns a handle whose reader renders without
|
|
100
|
+
* suspending.
|
|
101
|
+
*
|
|
102
|
+
* **When to use**
|
|
103
|
+
*
|
|
104
|
+
* Use when the loader must not see a result from before it ran, such as
|
|
105
|
+
* after a mutation.
|
|
106
|
+
*
|
|
107
|
+
* **Details**
|
|
108
|
+
*
|
|
109
|
+
* Waits until the result settles and isn't refreshing. A typed failure
|
|
110
|
+
* settles it like a success: `result` returns it. A defect or an
|
|
111
|
+
* interruption fails the Effect.
|
|
112
|
+
*
|
|
113
|
+
* @stability unstable
|
|
114
|
+
* @category verbs
|
|
115
|
+
* @since 0.1.0
|
|
116
|
+
*/
|
|
117
|
+
export declare const fetch: <S extends Source>(source: S, ...args: ArgsOf<S>) => Effect.Effect<LoadedHandle<S>, never, AtomRegistry.AtomRegistry>;
|
|
118
|
+
/**
|
|
119
|
+
* Returns the settled result `ensure` or `fetch` waited for.
|
|
120
|
+
*
|
|
121
|
+
* **When to use**
|
|
122
|
+
*
|
|
123
|
+
* Use in the loader that ran the verb, to branch on the result: redirect on
|
|
124
|
+
* a tag, or build another atom's arguments from it. Components read the
|
|
125
|
+
* handle with `useAtomResult`, which follows the registry.
|
|
126
|
+
*
|
|
127
|
+
* **Gotchas**
|
|
128
|
+
*
|
|
129
|
+
* Only the handle object the verb returned has it: a handle that crossed
|
|
130
|
+
* the network, as loader data or a server component's prop, is a copy, and
|
|
131
|
+
* this throws for it.
|
|
132
|
+
*
|
|
133
|
+
* **Example**
|
|
134
|
+
*
|
|
135
|
+
* ```ts
|
|
136
|
+
* import { AsyncResult } from "effect/reactivity"
|
|
137
|
+
* import { Loader } from "raf-effect-atom"
|
|
138
|
+
*
|
|
139
|
+
* const handle = await atoms.ensure(issue, id)
|
|
140
|
+
* const error = AsyncResult.error(Loader.result(handle))
|
|
141
|
+
* ```
|
|
142
|
+
*
|
|
143
|
+
* @stability unstable
|
|
144
|
+
* @category getters
|
|
145
|
+
* @since 0.1.0
|
|
146
|
+
*/
|
|
147
|
+
export declare const result: <S extends Source>(handle: LoadedHandle<S>) => SettledResult<SuccessOf<S>, ErrorOf<S>>;
|
|
148
|
+
/**
|
|
149
|
+
* The loader verbs as Promises over one registry, as the framework
|
|
150
|
+
* integrations put them on the router context.
|
|
151
|
+
*
|
|
152
|
+
* @stability unstable
|
|
153
|
+
* @category models
|
|
154
|
+
* @since 0.1.0
|
|
155
|
+
*/
|
|
156
|
+
export interface Loader {
|
|
157
|
+
/** {@link ensure}, as a Promise that rejects only on a defect. */
|
|
158
|
+
readonly ensure: <S extends Source>(source: S, ...args: ArgsOf<S>) => Promise<LoadedHandle<S>>;
|
|
159
|
+
/** {@link fetch}, as a Promise that rejects only on a defect. */
|
|
160
|
+
readonly fetch: <S extends Source>(source: S, ...args: ArgsOf<S>) => Promise<LoadedHandle<S>>;
|
|
161
|
+
/** {@link prefetch}, run at once. */
|
|
162
|
+
readonly prefetch: <S extends Source>(source: S, ...args: ArgsOf<S>) => Handle<S>;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Creates the Promise verbs over one registry.
|
|
166
|
+
*
|
|
167
|
+
* **When to use**
|
|
168
|
+
*
|
|
169
|
+
* Use in a framework integration, in loaders that are async functions, or
|
|
170
|
+
* in an event handler that starts an atom before the state change that
|
|
171
|
+
* renders its reader, for state that can't be in the URL, such as a
|
|
172
|
+
* search-as-you-type query. In Effect code, use
|
|
173
|
+
* the verbs directly with the `AtomRegistry` provided.
|
|
174
|
+
*
|
|
175
|
+
* **Example**
|
|
176
|
+
*
|
|
177
|
+
* ```ts
|
|
178
|
+
* import { Loader, RequestRegistry } from "raf-effect-atom"
|
|
179
|
+
*
|
|
180
|
+
* const loader = Loader.make(RequestRegistry.make())
|
|
181
|
+
* const comments = loader.prefetch(commentPage, 1, 0)
|
|
182
|
+
* const issueHandle = await loader.ensure(issue, 1)
|
|
183
|
+
* ```
|
|
184
|
+
*
|
|
185
|
+
* @stability unstable
|
|
186
|
+
* @category constructors
|
|
187
|
+
* @since 0.1.0
|
|
188
|
+
*/
|
|
189
|
+
export declare const make: (registry: AtomRegistry.AtomRegistry, options?: {
|
|
190
|
+
readonly leaseTTL?: Duration.Input | undefined;
|
|
191
|
+
}) => Loader;
|
|
192
|
+
export { Loader_d_exports };
|
package/dist/Loader.mjs
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import { __exportAll } from "./_virtual/_rolldown/runtime.mjs";
|
|
2
|
+
import { hydrated } from "./Hydration.mjs";
|
|
3
|
+
import { isDevelopment } from "./internal/development.mjs";
|
|
4
|
+
import { keyOf, take } from "./internal/leases.mjs";
|
|
5
|
+
import { atomOf, loaded as loaded$1, make as make$1 } from "./internal/handles.mjs";
|
|
6
|
+
import { isRequestRegistry } from "./RequestRegistry.mjs";
|
|
7
|
+
import * as Atom from "effect/reactivity/Atom";
|
|
8
|
+
import * as Predicate from "effect/Predicate";
|
|
9
|
+
import * as Cause from "effect/Cause";
|
|
10
|
+
import * as Context from "effect/Context";
|
|
11
|
+
import * as Duration from "effect/Duration";
|
|
12
|
+
import * as Effect from "effect/Effect";
|
|
13
|
+
import * as Option from "effect/Option";
|
|
14
|
+
import * as AsyncResult from "effect/reactivity/AsyncResult";
|
|
15
|
+
import * as AtomRegistry from "effect/reactivity/AtomRegistry";
|
|
16
|
+
// src/Loader.ts
|
|
17
|
+
/**
|
|
18
|
+
* Starts atoms from route loaders, before anything renders, so nothing
|
|
19
|
+
* waterfalls.
|
|
20
|
+
*
|
|
21
|
+
* Each verb takes a source, an atom family or an `AsyncResult` atom, with its
|
|
22
|
+
* arguments. It starts the atom in the registry, takes a lease on it, so the
|
|
23
|
+
* registry's idle TTL can't evict it before the route renders and
|
|
24
|
+
* `useAtomResult` takes it over, and returns its handle for the component
|
|
25
|
+
* that reads it:
|
|
26
|
+
*
|
|
27
|
+
* | Verb | Starts the atom | Waits | Has a value already |
|
|
28
|
+
* | ---------- | ------------------------------- | ----------------------------- | --------------------- |
|
|
29
|
+
* | `prefetch` | if it isn't in the registry | never | leaves it alone |
|
|
30
|
+
* | `ensure` | if it isn't in the registry | only while it's `Initial` | returns it at once |
|
|
31
|
+
* | `fetch` | if it isn't, or refreshes it | until the new result settles | refreshes, then waits |
|
|
32
|
+
*
|
|
33
|
+
* Typed failures don't reject: `ensure` and `fetch` settle on them, and
|
|
34
|
+
* `result` returns the settled `AsyncResult`, so a loader can redirect on a
|
|
35
|
+
* tag and otherwise let a boundary render the failure. Only defects and
|
|
36
|
+
* interruptions reject.
|
|
37
|
+
*
|
|
38
|
+
* The verbs are Effects that need the `AtomRegistry`; `make` gives the same
|
|
39
|
+
* verbs as Promises over one registry, for loaders that are async functions.
|
|
40
|
+
*
|
|
41
|
+
* @stability unstable
|
|
42
|
+
* @since 0.1.0
|
|
43
|
+
*/
|
|
44
|
+
var Loader_exports = /* @__PURE__ */ __exportAll({
|
|
45
|
+
LeaseTTL: () => LeaseTTL,
|
|
46
|
+
ensure: () => ensure,
|
|
47
|
+
fetch: () => fetch,
|
|
48
|
+
make: () => make,
|
|
49
|
+
prefetch: () => prefetch,
|
|
50
|
+
result: () => result
|
|
51
|
+
});
|
|
52
|
+
/**
|
|
53
|
+
* How long a loader's lease keeps an atom mounted when no component takes it
|
|
54
|
+
* over. Default 30 seconds.
|
|
55
|
+
*
|
|
56
|
+
* **Details**
|
|
57
|
+
*
|
|
58
|
+
* A lease ends when `useAtomResult` mounts the atom, or when this time has
|
|
59
|
+
* passed, after which the registry's idle TTL applies as usual. On a request
|
|
60
|
+
* registry, leases last until the registry is disposed.
|
|
61
|
+
*
|
|
62
|
+
* @stability unstable
|
|
63
|
+
* @category references
|
|
64
|
+
* @since 0.1.0
|
|
65
|
+
*/
|
|
66
|
+
const LeaseTTL = Context.Reference("raf-effect-atom/Loader/LeaseTTL", { defaultValue: () => Duration.seconds(30) });
|
|
67
|
+
const start = Effect.fnUntraced(function* (atom) {
|
|
68
|
+
const registry = yield* AtomRegistry.AtomRegistry;
|
|
69
|
+
const request = isRequestRegistry(registry);
|
|
70
|
+
if (request && !Atom.isSerializable(atom) && (yield* isDevelopment)) return yield* Effect.die(/* @__PURE__ */ new Error(`raf-effect-atom: a loader started ${String(atom)}, which isn't serializable, so the browser would request it again. Give it a serialization key, such as AtomRpc's serializationKey option.`));
|
|
71
|
+
const ttl = request ? void 0 : Duration.toMillis(yield* LeaseTTL);
|
|
72
|
+
const target = hydrated(atom);
|
|
73
|
+
const key = keyOf(target);
|
|
74
|
+
const existed = registry.getNodes().has(key) || Predicate.isString(key) && registry.preloadedSerializable.has(key);
|
|
75
|
+
take(registry, target, ttl);
|
|
76
|
+
return {
|
|
77
|
+
existed,
|
|
78
|
+
registry,
|
|
79
|
+
request,
|
|
80
|
+
target
|
|
81
|
+
};
|
|
82
|
+
});
|
|
83
|
+
const isSettled = (result) => AsyncResult.isNotInitial(result) && !result.waiting;
|
|
84
|
+
const settled = (registry, atom, isDone) => Effect.callback((resume) => {
|
|
85
|
+
const current = registry.get(atom);
|
|
86
|
+
if (isDone(current)) {
|
|
87
|
+
resume(Effect.succeed(current));
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const cancel = registry.subscribe(atom, (value) => {
|
|
91
|
+
if (isDone(value)) {
|
|
92
|
+
cancel();
|
|
93
|
+
resume(Effect.succeed(value));
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
return Effect.sync(cancel);
|
|
97
|
+
}).pipe(Effect.flatMap(rejectDefects));
|
|
98
|
+
const rejectDefects = (result) => {
|
|
99
|
+
if (AsyncResult.isFailure(result) && Option.isNone(Cause.findErrorOption(result.cause))) {
|
|
100
|
+
const defects = result.cause;
|
|
101
|
+
return Effect.failCause(defects);
|
|
102
|
+
}
|
|
103
|
+
return Effect.succeed(result);
|
|
104
|
+
};
|
|
105
|
+
/**
|
|
106
|
+
* Starts a source's atom without waiting for it, and returns its handle.
|
|
107
|
+
*
|
|
108
|
+
* **When to use**
|
|
109
|
+
*
|
|
110
|
+
* Use for data the route needs but its loader doesn't: it streams in while
|
|
111
|
+
* the route renders.
|
|
112
|
+
*
|
|
113
|
+
* **Details**
|
|
114
|
+
*
|
|
115
|
+
* Starts the atom if it isn't in the registry, and leaves it alone if it is.
|
|
116
|
+
* Takes a lease either way. The component that reads the handle suspends
|
|
117
|
+
* until the result settles.
|
|
118
|
+
*
|
|
119
|
+
* **Example**
|
|
120
|
+
*
|
|
121
|
+
* ```ts
|
|
122
|
+
* import { Effect } from "effect"
|
|
123
|
+
* import { Loader } from "raf-effect-atom"
|
|
124
|
+
*
|
|
125
|
+
* const program = Loader.prefetch(commentPage, issueId, 0)
|
|
126
|
+
* ```
|
|
127
|
+
*
|
|
128
|
+
* @stability unstable
|
|
129
|
+
* @category verbs
|
|
130
|
+
* @since 0.1.0
|
|
131
|
+
*/
|
|
132
|
+
const prefetch = (source, ...args) => Effect.gen(function* () {
|
|
133
|
+
const atom = atomOf(source, args);
|
|
134
|
+
yield* start(atom);
|
|
135
|
+
return make$1(args, atom);
|
|
136
|
+
});
|
|
137
|
+
const loaded = (args, atom, result) => {
|
|
138
|
+
const handle = loaded$1(args, atom);
|
|
139
|
+
results.set(handle, result);
|
|
140
|
+
return handle;
|
|
141
|
+
};
|
|
142
|
+
/**
|
|
143
|
+
* Starts a source's atom, waits only while it has no result, and returns a
|
|
144
|
+
* handle whose reader renders without suspending.
|
|
145
|
+
*
|
|
146
|
+
* **When to use**
|
|
147
|
+
*
|
|
148
|
+
* Use when the loader needs the result, such as to redirect on a tag, or
|
|
149
|
+
* when the route shouldn't render without it.
|
|
150
|
+
*
|
|
151
|
+
* **Details**
|
|
152
|
+
*
|
|
153
|
+
* An atom with a result returns at once, without refetching. On a request
|
|
154
|
+
* registry, it also waits out a refresh in flight, so the server never
|
|
155
|
+
* renders a result that's about to change. A typed failure settles it like
|
|
156
|
+
* a success: `result` returns it. A defect or an interruption fails the
|
|
157
|
+
* Effect.
|
|
158
|
+
*
|
|
159
|
+
* **Example**
|
|
160
|
+
*
|
|
161
|
+
* ```ts
|
|
162
|
+
* import { Effect } from "effect"
|
|
163
|
+
* import { AsyncResult } from "effect/reactivity"
|
|
164
|
+
* import { Loader } from "raf-effect-atom"
|
|
165
|
+
*
|
|
166
|
+
* const program = Effect.gen(function* () {
|
|
167
|
+
* const handle = yield* Loader.ensure(issue, 1)
|
|
168
|
+
* const result = Loader.result(handle)
|
|
169
|
+
* return AsyncResult.isSuccess(result) ? result.value.title : "missing"
|
|
170
|
+
* })
|
|
171
|
+
* ```
|
|
172
|
+
*
|
|
173
|
+
* @stability unstable
|
|
174
|
+
* @category verbs
|
|
175
|
+
* @since 0.1.0
|
|
176
|
+
*/
|
|
177
|
+
const ensure = (source, ...args) => Effect.gen(function* () {
|
|
178
|
+
const atom = atomOf(source, args);
|
|
179
|
+
const started = yield* start(atom);
|
|
180
|
+
const result = yield* settled(started.registry, started.target, started.request ? isSettled : AsyncResult.isNotInitial);
|
|
181
|
+
return loaded(args, atom, result);
|
|
182
|
+
});
|
|
183
|
+
/**
|
|
184
|
+
* Starts a source's atom, or refreshes it if it has a result, waits for the
|
|
185
|
+
* new result to settle, and returns a handle whose reader renders without
|
|
186
|
+
* suspending.
|
|
187
|
+
*
|
|
188
|
+
* **When to use**
|
|
189
|
+
*
|
|
190
|
+
* Use when the loader must not see a result from before it ran, such as
|
|
191
|
+
* after a mutation.
|
|
192
|
+
*
|
|
193
|
+
* **Details**
|
|
194
|
+
*
|
|
195
|
+
* Waits until the result settles and isn't refreshing. A typed failure
|
|
196
|
+
* settles it like a success: `result` returns it. A defect or an
|
|
197
|
+
* interruption fails the Effect.
|
|
198
|
+
*
|
|
199
|
+
* @stability unstable
|
|
200
|
+
* @category verbs
|
|
201
|
+
* @since 0.1.0
|
|
202
|
+
*/
|
|
203
|
+
const fetch = (source, ...args) => Effect.gen(function* () {
|
|
204
|
+
const atom = atomOf(source, args);
|
|
205
|
+
const started = yield* start(atom);
|
|
206
|
+
const { registry, target } = started;
|
|
207
|
+
if (started.existed && isSettled(registry.get(target))) {
|
|
208
|
+
const node = registry.getNodes().get(keyOf(target));
|
|
209
|
+
registry.refresh(node?.atom ?? target);
|
|
210
|
+
}
|
|
211
|
+
const result = yield* settled(registry, target, isSettled);
|
|
212
|
+
return loaded(args, atom, result);
|
|
213
|
+
});
|
|
214
|
+
const results = /* @__PURE__ */ new WeakMap();
|
|
215
|
+
/**
|
|
216
|
+
* Returns the settled result `ensure` or `fetch` waited for.
|
|
217
|
+
*
|
|
218
|
+
* **When to use**
|
|
219
|
+
*
|
|
220
|
+
* Use in the loader that ran the verb, to branch on the result: redirect on
|
|
221
|
+
* a tag, or build another atom's arguments from it. Components read the
|
|
222
|
+
* handle with `useAtomResult`, which follows the registry.
|
|
223
|
+
*
|
|
224
|
+
* **Gotchas**
|
|
225
|
+
*
|
|
226
|
+
* Only the handle object the verb returned has it: a handle that crossed
|
|
227
|
+
* the network, as loader data or a server component's prop, is a copy, and
|
|
228
|
+
* this throws for it.
|
|
229
|
+
*
|
|
230
|
+
* **Example**
|
|
231
|
+
*
|
|
232
|
+
* ```ts
|
|
233
|
+
* import { AsyncResult } from "effect/reactivity"
|
|
234
|
+
* import { Loader } from "raf-effect-atom"
|
|
235
|
+
*
|
|
236
|
+
* const handle = await atoms.ensure(issue, id)
|
|
237
|
+
* const error = AsyncResult.error(Loader.result(handle))
|
|
238
|
+
* ```
|
|
239
|
+
*
|
|
240
|
+
* @stability unstable
|
|
241
|
+
* @category getters
|
|
242
|
+
* @since 0.1.0
|
|
243
|
+
*/
|
|
244
|
+
const result = (handle) => {
|
|
245
|
+
const settledResult = results.get(handle);
|
|
246
|
+
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.");
|
|
247
|
+
return settledResult;
|
|
248
|
+
};
|
|
249
|
+
/**
|
|
250
|
+
* Creates the Promise verbs over one registry.
|
|
251
|
+
*
|
|
252
|
+
* **When to use**
|
|
253
|
+
*
|
|
254
|
+
* Use in a framework integration, in loaders that are async functions, or
|
|
255
|
+
* in an event handler that starts an atom before the state change that
|
|
256
|
+
* renders its reader, for state that can't be in the URL, such as a
|
|
257
|
+
* search-as-you-type query. In Effect code, use
|
|
258
|
+
* the verbs directly with the `AtomRegistry` provided.
|
|
259
|
+
*
|
|
260
|
+
* **Example**
|
|
261
|
+
*
|
|
262
|
+
* ```ts
|
|
263
|
+
* import { Loader, RequestRegistry } from "raf-effect-atom"
|
|
264
|
+
*
|
|
265
|
+
* const loader = Loader.make(RequestRegistry.make())
|
|
266
|
+
* const comments = loader.prefetch(commentPage, 1, 0)
|
|
267
|
+
* const issueHandle = await loader.ensure(issue, 1)
|
|
268
|
+
* ```
|
|
269
|
+
*
|
|
270
|
+
* @stability unstable
|
|
271
|
+
* @category constructors
|
|
272
|
+
* @since 0.1.0
|
|
273
|
+
*/
|
|
274
|
+
const make = (registry, options) => {
|
|
275
|
+
const leaseTTL = options?.leaseTTL === void 0 ? void 0 : Duration.fromInputUnsafe(options.leaseTTL);
|
|
276
|
+
const provide = (effect) => {
|
|
277
|
+
const provided = Effect.provideService(effect, AtomRegistry.AtomRegistry, registry);
|
|
278
|
+
return leaseTTL === void 0 ? provided : Effect.provideService(provided, LeaseTTL, leaseTTL);
|
|
279
|
+
};
|
|
280
|
+
return {
|
|
281
|
+
ensure: (source, ...args) => Effect.runPromise(provide(ensure(source, ...args))),
|
|
282
|
+
fetch: (source, ...args) => Effect.runPromise(provide(fetch(source, ...args))),
|
|
283
|
+
prefetch: (source, ...args) => Effect.runSync(provide(prefetch(source, ...args)))
|
|
284
|
+
};
|
|
285
|
+
};
|
|
286
|
+
export { LeaseTTL, Loader_exports, ensure, fetch, make, prefetch, result };
|
package/dist/Next.d.mts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { ArgsOf, Handle, LoadedHandle, Source } from "./Handle.mjs";
|
|
2
|
+
import { HydrateAtoms } from "./NextClient.mjs";
|
|
3
|
+
import { RegistryProvider } from "./RegistryProvider.mjs";
|
|
4
|
+
import * as EffectHydration from "effect/reactivity/Hydration";
|
|
5
|
+
import * as AtomRegistry from "effect/reactivity/AtomRegistry";
|
|
6
|
+
/**
|
|
7
|
+
* Returns the request registry of the server render in progress.
|
|
8
|
+
*
|
|
9
|
+
* **Details**
|
|
10
|
+
*
|
|
11
|
+
* Made once per request through React's `cache`, and disposed through
|
|
12
|
+
* `cacheSignal` when React finishes the render, after the last streamed
|
|
13
|
+
* result has been sent.
|
|
14
|
+
*
|
|
15
|
+
* **Gotchas**
|
|
16
|
+
*
|
|
17
|
+
* Only server components share it: outside a server component render,
|
|
18
|
+
* `cache` doesn't cache, so each call makes a new registry.
|
|
19
|
+
*
|
|
20
|
+
* @stability unstable
|
|
21
|
+
* @category registry
|
|
22
|
+
* @since 0.1.0
|
|
23
|
+
*/
|
|
24
|
+
export declare const getRegistry: () => AtomRegistry.AtomRegistry;
|
|
25
|
+
/**
|
|
26
|
+
* `Loader.prefetch` on the request's registry: starts a source's atom, to
|
|
27
|
+
* stream it, and returns its handle.
|
|
28
|
+
*
|
|
29
|
+
* @stability unstable
|
|
30
|
+
* @category verbs
|
|
31
|
+
* @since 0.1.0
|
|
32
|
+
*/
|
|
33
|
+
export declare const prefetch: <S extends Source>(source: S, ...args: ArgsOf<S>) => Handle<S>;
|
|
34
|
+
/**
|
|
35
|
+
* `Loader.ensure` on the request's registry: starts a source's atom, waits
|
|
36
|
+
* for its result, and returns its handle.
|
|
37
|
+
*
|
|
38
|
+
* @stability unstable
|
|
39
|
+
* @category verbs
|
|
40
|
+
* @since 0.1.0
|
|
41
|
+
*/
|
|
42
|
+
export declare const ensure: <S extends Source>(source: S, ...args: ArgsOf<S>) => Promise<LoadedHandle<S>>;
|
|
43
|
+
/**
|
|
44
|
+
* `Loader.fetch` on the request's registry: starts or refreshes a source's
|
|
45
|
+
* atom, waits for the new result, and returns its handle.
|
|
46
|
+
*
|
|
47
|
+
* @stability unstable
|
|
48
|
+
* @category verbs
|
|
49
|
+
* @since 0.1.0
|
|
50
|
+
*/
|
|
51
|
+
export declare const fetch: <S extends Source>(source: S, ...args: ArgsOf<S>) => Promise<LoadedHandle<S>>;
|
|
52
|
+
/**
|
|
53
|
+
* `Hydration.dehydrate` on the request's registry, for `HydrateAtoms`.
|
|
54
|
+
*
|
|
55
|
+
* **Gotchas**
|
|
56
|
+
*
|
|
57
|
+
* Includes the atoms started so far, so call it after the server component's
|
|
58
|
+
* verbs. Each `HydrateAtoms` hydrates what its own `dehydrate()` saw.
|
|
59
|
+
*
|
|
60
|
+
* @stability unstable
|
|
61
|
+
* @category dehydration
|
|
62
|
+
* @since 0.1.0
|
|
63
|
+
*/
|
|
64
|
+
export declare const dehydrate: () => Array<EffectHydration.DehydratedAtom>;
|
|
65
|
+
export { HydrateAtoms, RegistryProvider };
|