raf-effect-atom 0.2.5 → 0.2.6

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 CHANGED
@@ -1,10 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.2.6](https://github.com/KATT/raf-effect-atom/compare/raf-effect-atom%400.2.5...raf-effect-atom%400.2.6) (2026-10-23)
4
+
5
+ ### Features
6
+
7
+ * **raf-effect-atom:** start browser-only reads with prefetchInBrowser ([65f783a](https://github.com/KATT/raf-effect-atom/commit/65f783a0014e44a8aa11ce5e8fa34ab863063403))
8
+
9
+ ### Documentation
10
+
11
+ * **www:** keep a search box's text in the URL, and drop the debounce sample ([a3949bf](https://github.com/KATT/raf-effect-atom/commit/a3949bfca4b64e478132f983033b2776e50ba949))
12
+
13
+ ### Tests
14
+
15
+ * **raf-effect-atom:** start a browser-only read from a client-only TanStack route ([69a8bd8](https://github.com/KATT/raf-effect-atom/commit/69a8bd887c34a7aeef51fdd20057ac30aa9e1fbc))
16
+ * **raf-effect-atom:** type a derived atom's handle through it and its wrappers ([a16b587](https://github.com/KATT/raf-effect-atom/commit/a16b587a59dc4827dd18ecb3a3d5bb8f6fdedf0b))
17
+
3
18
  ## [0.2.5](https://github.com/KATT/raf-effect-atom/compare/raf-effect-atom%400.2.4...raf-effect-atom%400.2.5) (2026-10-23)
4
19
 
5
20
  ### Features
6
21
 
7
- * **raf-effect-atom:** start a derived atom's source from the loader verbs, following initialValueTarget ([00a9760](https://github.com/KATT/raf-effect-atom/commit/00a97602068cabf085fa9cfe41e88bf853b6af32))
22
+ - **raf-effect-atom:** start a derived atom's source from the loader verbs, following initialValueTarget ([00a9760](https://github.com/KATT/raf-effect-atom/commit/00a97602068cabf085fa9cfe41e88bf853b6af32))
8
23
 
9
24
  ## [0.2.4](https://github.com/KATT/raf-effect-atom/compare/raf-effect-atom%400.2.3...raf-effect-atom%400.2.4) (2026-10-23)
10
25
 
package/dist/Handle.d.mts CHANGED
@@ -67,6 +67,11 @@ export type HandleKey<S> = string & {
67
67
  */
68
68
  export interface Handle<S> {
69
69
  readonly args: ArgsOf<S>;
70
+ /**
71
+ * `true` when `Loader.prefetchInBrowser` ran on the server and left the
72
+ * atom to the browser: the browser's read starts it.
73
+ */
74
+ readonly inBrowser?: true;
70
75
  /** The atom's serialization key; `undefined` for an atom without one. */
71
76
  readonly key: HandleKey<S> | undefined;
72
77
  }
package/dist/Hooks.mjs CHANGED
@@ -3,6 +3,7 @@ import { stream } from "./internal/pending.mjs";
3
3
  import { hydrated } from "./Hydration.mjs";
4
4
  import { keyOf, release } from "./internal/leases.mjs";
5
5
  import { atomOf, sourceOf } from "./internal/handles.mjs";
6
+ import { isRequestRegistry } from "./RequestRegistry.mjs";
6
7
  import { requireRequestRegistry } from "./internal/registry.mjs";
7
8
  import { ModeContext, check } from "./internal/waterfalls.mjs";
8
9
  import * as Predicate from "effect/Predicate";
@@ -99,6 +100,7 @@ const useAtomResult = (source, handle) => {
99
100
  const registry = React.useContext(RegistryContext);
100
101
  const atom = atomOf(source, handle.args);
101
102
  requireRequestRegistry(registry, "useAtomResult");
103
+ if (handle.inBrowser === true && isRequestRegistry(registry)) throw new Error(`raf-effect-atom: ${String(atom)} starts in the browser (Loader.prefetchInBrowser), so the server renders the nearest Suspense boundary's fallback, and the browser renders the boundary.`);
102
104
  const target = sourceOf(atom);
103
105
  const started = hydrated(target);
104
106
  const read = target === atom ? hydrated(atom) : atom;
@@ -106,7 +108,7 @@ const useAtomResult = (source, handle) => {
106
108
  if (process.env.NODE_ENV !== "production" && handle.key !== void 0 && Predicate.isString(key) && handle.key !== key) throw new Error(`raf-effect-atom: useAtomResult read the handle of ${handle.key} through a source that makes ${key}. Read a handle through the source that made it.`);
107
109
  const streamed = Predicate.isString(key) ? stream(registry, key) : void 0;
108
110
  if (streamed !== void 0) React.use(streamed);
109
- if (process.env.NODE_ENV !== "production") check(registry, started, React.use(ModeContext));
111
+ if (process.env.NODE_ENV !== "production" && handle.inBrowser !== true) check(registry, started, React.use(ModeContext));
110
112
  if (read !== started) registry.get(started);
111
113
  const current = registry.get(read);
112
114
  const result = useAtomSuspense(read, {
package/dist/Loader.d.mts CHANGED
@@ -5,7 +5,7 @@ import * as Effect from "effect/Effect";
5
5
  import * as AsyncResult from "effect/reactivity/AsyncResult";
6
6
  import * as AtomRegistry from "effect/reactivity/AtomRegistry";
7
7
  declare namespace Loader_d_exports {
8
- export { LeaseTTL, Loader, MakeOptions, SettledResult, ensure, fetch, make, prefetch, result };
8
+ export { LeaseTTL, Loader, MakeOptions, SettledResult, ensure, fetch, make, prefetch, prefetchInBrowser, result };
9
9
  }
10
10
  /**
11
11
  * A settled `AsyncResult`: what `ensure` and `fetch` return.
@@ -73,6 +73,49 @@ export declare const LeaseTTL: Context.Reference<Duration.Duration>;
73
73
  * @since 0.1.0
74
74
  */
75
75
  export declare const prefetch: <S extends Source>(source: S, ...args: ArgsOf<S>) => Effect.Effect<Handle<S>, never, AtomRegistry.AtomRegistry>;
76
+ /**
77
+ * Starts a source's atom in the browser, and returns its handle: on the
78
+ * server, it starts nothing, and the browser's read starts the atom.
79
+ *
80
+ * **When to use**
81
+ *
82
+ * Use for data only the browser can load: an input the server never sees,
83
+ * such as a token in the URL's fragment or a permission, or a request the
84
+ * server can't make. The rest of the page still renders on the server.
85
+ *
86
+ * **Details**
87
+ *
88
+ * - In the browser, it's `prefetch`: a loader that runs there, such as on a
89
+ * client navigation, starts the atom and takes a lease.
90
+ * - On a request registry, it returns a handle marked `inBrowser` without
91
+ * starting the atom, so the atom needs no serialization key. On the
92
+ * server, `useAtomResult` throws for that handle, so React renders the
93
+ * nearest Suspense boundary's fallback there and renders the boundary in
94
+ * the browser. There, the read starts the atom, and the waterfall check
95
+ * skips it, since a loader asked for it.
96
+ *
97
+ * **Gotchas**
98
+ *
99
+ * - Read the handle under a Suspense boundary: on the server, a read
100
+ * outside one fails the whole render. React reports the boundary it
101
+ * renders in the browser to `onRecoverableError`.
102
+ * - On a page the server rendered, the request starts when the browser
103
+ * renders the read, after hydration's JavaScript has loaded: start the
104
+ * data the server can load with `prefetch` or `ensure`.
105
+ *
106
+ * **Example**
107
+ *
108
+ * ```ts
109
+ * import { Loader } from "raf-effect-atom"
110
+ *
111
+ * const program = Loader.prefetchInBrowser(pushSubscription)
112
+ * ```
113
+ *
114
+ * @stability unstable
115
+ * @category verbs
116
+ * @since 0.2.6
117
+ */
118
+ export declare const prefetchInBrowser: <S extends Source>(source: S, ...args: ArgsOf<S>) => Effect.Effect<Handle<S>, never, AtomRegistry.AtomRegistry>;
76
119
  /**
77
120
  * Starts a source's atom, waits only while it has no result, and returns a
78
121
  * handle whose reader renders without suspending.
@@ -179,6 +222,8 @@ export interface Loader {
179
222
  * the seed resolves.
180
223
  */
181
224
  readonly prefetch: <S extends Source>(source: S, ...args: ArgsOf<S>) => Handle<S>;
225
+ /** {@link prefetchInBrowser}, run at once. */
226
+ readonly prefetchInBrowser: <S extends Source>(source: S, ...args: ArgsOf<S>) => Handle<S>;
182
227
  /**
183
228
  * Runs an Effect that needs the registry, such as the Effect verbs in an
184
229
  * `Effect.gen`, as a Promise that rejects with its squashed cause.
@@ -213,8 +258,8 @@ export interface MakeOptions {
213
258
  *
214
259
  * Use in a framework integration, in loaders that are async functions, or
215
260
  * in an event handler that starts an atom before the state change that
216
- * renders its reader, for state that can't be in the URL, such as a
217
- * search-as-you-type query. In Effect code, use the verbs directly, and run
261
+ * renders its reader, for a choice a click or a submit makes that doesn't
262
+ * belong in the URL. In Effect code, use the verbs directly, and run
218
263
  * the Effect on the registry with `run`.
219
264
  *
220
265
  * **Details**
package/dist/Loader.mjs CHANGED
@@ -2,7 +2,7 @@ import { __exportAll } from "./_virtual/_rolldown/runtime.mjs";
2
2
  import { hydrated } from "./Hydration.mjs";
3
3
  import { isDevelopment } from "./internal/development.mjs";
4
4
  import { keyOf, take } from "./internal/leases.mjs";
5
- import { atomOf, loaded as loaded$1, make as make$1, sourceOf } from "./internal/handles.mjs";
5
+ import { atomOf, inBrowser, loaded as loaded$1, make as make$1, sourceOf } from "./internal/handles.mjs";
6
6
  import { afterSeed, pending } from "./internal/seeds.mjs";
7
7
  import { isRequestRegistry } from "./RequestRegistry.mjs";
8
8
  import * as Atom from "effect/reactivity/Atom";
@@ -31,6 +31,9 @@ import * as AtomRegistry from "effect/reactivity/AtomRegistry";
31
31
  * | `ensure` | if it isn't in the registry | only while it's `Initial` | returns it at once |
32
32
  * | `fetch` | if it isn't, or refreshes it | until the new result settles | refreshes, then waits |
33
33
  *
34
+ * `prefetchInBrowser` is `prefetch` in the browser. On the server, it
35
+ * starts nothing, and the browser's read starts the atom.
36
+ *
34
37
  * Typed failures don't reject: `ensure` and `fetch` settle on them, and
35
38
  * `result` returns the settled `AsyncResult`, so a loader can redirect on a
36
39
  * tag and otherwise let a boundary render the failure. Only defects and
@@ -48,6 +51,7 @@ var Loader_exports = /* @__PURE__ */ __exportAll({
48
51
  fetch: () => fetch,
49
52
  make: () => make,
50
53
  prefetch: () => prefetch,
54
+ prefetchInBrowser: () => prefetchInBrowser,
51
55
  result: () => result
52
56
  });
53
57
  /**
@@ -154,6 +158,54 @@ const prefetch = (source, ...args) => Effect.gen(function* () {
154
158
  yield* start(atom);
155
159
  return make$1(args, atom);
156
160
  });
161
+ /**
162
+ * Starts a source's atom in the browser, and returns its handle: on the
163
+ * server, it starts nothing, and the browser's read starts the atom.
164
+ *
165
+ * **When to use**
166
+ *
167
+ * Use for data only the browser can load: an input the server never sees,
168
+ * such as a token in the URL's fragment or a permission, or a request the
169
+ * server can't make. The rest of the page still renders on the server.
170
+ *
171
+ * **Details**
172
+ *
173
+ * - In the browser, it's `prefetch`: a loader that runs there, such as on a
174
+ * client navigation, starts the atom and takes a lease.
175
+ * - On a request registry, it returns a handle marked `inBrowser` without
176
+ * starting the atom, so the atom needs no serialization key. On the
177
+ * server, `useAtomResult` throws for that handle, so React renders the
178
+ * nearest Suspense boundary's fallback there and renders the boundary in
179
+ * the browser. There, the read starts the atom, and the waterfall check
180
+ * skips it, since a loader asked for it.
181
+ *
182
+ * **Gotchas**
183
+ *
184
+ * - Read the handle under a Suspense boundary: on the server, a read
185
+ * outside one fails the whole render. React reports the boundary it
186
+ * renders in the browser to `onRecoverableError`.
187
+ * - On a page the server rendered, the request starts when the browser
188
+ * renders the read, after hydration's JavaScript has loaded: start the
189
+ * data the server can load with `prefetch` or `ensure`.
190
+ *
191
+ * **Example**
192
+ *
193
+ * ```ts
194
+ * import { Loader } from "raf-effect-atom"
195
+ *
196
+ * const program = Loader.prefetchInBrowser(pushSubscription)
197
+ * ```
198
+ *
199
+ * @stability unstable
200
+ * @category verbs
201
+ * @since 0.2.6
202
+ */
203
+ const prefetchInBrowser = (source, ...args) => Effect.gen(function* () {
204
+ const atom = atomOf(source, args);
205
+ if (isRequestRegistry(yield* AtomRegistry.AtomRegistry)) return inBrowser(args, atom);
206
+ yield* start(atom);
207
+ return make$1(args, atom);
208
+ });
157
209
  const loaded = (args, atom, result) => {
158
210
  const handle = loaded$1(args, atom);
159
211
  results.set(handle, result);
@@ -277,8 +329,8 @@ const either = (first, second) => first === void 0 || second === void 0 ? first
277
329
  *
278
330
  * Use in a framework integration, in loaders that are async functions, or
279
331
  * in an event handler that starts an atom before the state change that
280
- * renders its reader, for state that can't be in the URL, such as a
281
- * search-as-you-type query. In Effect code, use the verbs directly, and run
332
+ * renders its reader, for a choice a click or a submit makes that doesn't
333
+ * belong in the URL. In Effect code, use the verbs directly, and run
282
334
  * the Effect on the registry with `run`.
283
335
  *
284
336
  * **Details**
@@ -319,7 +371,15 @@ const make = (registry, options) => {
319
371
  });
320
372
  return make$1(args, atom);
321
373
  },
374
+ prefetchInBrowser: (source, ...args) => {
375
+ const atom = atomOf(source, args);
376
+ if (isRequestRegistry(registry)) return inBrowser(args, atom);
377
+ afterSeed(registry, () => {
378
+ Effect.runSync(provide(start(atom)));
379
+ });
380
+ return make$1(args, atom);
381
+ },
322
382
  run: (effect, runOptions) => runPromise(provide(effect), either(signal, runOptions?.signal))
323
383
  };
324
384
  };
325
- export { LeaseTTL, Loader_exports, ensure, fetch, make, prefetch, result };
385
+ export { LeaseTTL, Loader_exports, ensure, fetch, make, prefetch, prefetchInBrowser, result };
package/dist/Next.d.mts CHANGED
@@ -90,6 +90,15 @@ export declare const getRegistry: () => AtomRegistry.AtomRegistry;
90
90
  * @since 0.1.0
91
91
  */
92
92
  export declare const prefetch: <S extends Source>(source: S, ...args: ArgsOf<S>) => Handle<S>;
93
+ /**
94
+ * `Loader.prefetchInBrowser` on the request's registry: returns a handle
95
+ * whose atom the browser's read starts, without starting it on the server.
96
+ *
97
+ * @stability unstable
98
+ * @category verbs
99
+ * @since 0.2.6
100
+ */
101
+ export declare const prefetchInBrowser: <S extends Source>(source: S, ...args: ArgsOf<S>) => Handle<S>;
93
102
  /**
94
103
  * `Loader.ensure` on the request's registry: starts a source's atom, waits
95
104
  * for its result, and returns its handle.
package/dist/Next.mjs CHANGED
@@ -72,6 +72,15 @@ const getRegistry = atoms.getRegistry;
72
72
  */
73
73
  const prefetch = atoms.prefetch;
74
74
  /**
75
+ * `Loader.prefetchInBrowser` on the request's registry: returns a handle
76
+ * whose atom the browser's read starts, without starting it on the server.
77
+ *
78
+ * @stability unstable
79
+ * @category verbs
80
+ * @since 0.2.6
81
+ */
82
+ const prefetchInBrowser = atoms.prefetchInBrowser;
83
+ /**
75
84
  * `Loader.ensure` on the request's registry: starts a source's atom, waits
76
85
  * for its result, and returns its handle.
77
86
  *
@@ -130,4 +139,4 @@ const run = atoms.run;
130
139
  * @since 0.1.0
131
140
  */
132
141
  const dehydrate = atoms.dehydrate;
133
- export { HydrateAtoms, RegistryProvider, dehydrate, ensure, fetch, getRegistry, makeRequestAtoms, prefetch, run };
142
+ export { HydrateAtoms, RegistryProvider, dehydrate, ensure, fetch, getRegistry, makeRequestAtoms, prefetch, prefetchInBrowser, run };
@@ -51,7 +51,7 @@ export declare const WaterfallDetection: (props: {
51
51
  }) => React.ReactNode;
52
52
  /**
53
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.
54
+ * only a render knows, such as an id read from another atom's result.
55
55
  *
56
56
  * **When to use**
57
57
  *
@@ -55,7 +55,7 @@ const WaterfallDetection = (props) => process.env.NODE_ENV === "production" ? pr
55
55
  });
56
56
  /**
57
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.
58
+ * only a render knows, such as an id read from another atom's result.
59
59
  *
60
60
  * **When to use**
61
61
  *
@@ -23,9 +23,14 @@ const make = (args, atom) => ({
23
23
  args,
24
24
  key: keyOf(atom)
25
25
  });
26
+ const inBrowser = (args, atom) => ({
27
+ args,
28
+ inBrowser: true,
29
+ key: keyOf(atom)
30
+ });
26
31
  const loaded = (args, atom) => ({
27
32
  args,
28
33
  key: keyOf(atom),
29
34
  loaded: true
30
35
  });
31
- export { atomOf, loaded, make, sourceOf };
36
+ export { atomOf, inBrowser, loaded, make, sourceOf };
@@ -23,6 +23,7 @@ const make = (options) => {
23
23
  fetch: (source, ...args) => getLoader().fetch(source, ...args),
24
24
  getRegistry,
25
25
  prefetch: (source, ...args) => getLoader().prefetch(source, ...args),
26
+ prefetchInBrowser: (source, ...args) => getLoader().prefetchInBrowser(source, ...args),
26
27
  run: (effect, runOptions) => getLoader().run(effect, runOptions)
27
28
  };
28
29
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "raf-effect-atom",
3
- "version": "0.2.5",
3
+ "version": "0.2.6",
4
4
  "description": "Render-as-you-fetch for Effect Atom: start atoms from route loaders and stream them into the browser, typed failures intact",
5
5
  "keywords": [
6
6
  "effect",
@@ -58,7 +58,7 @@
58
58
  "@types/react-dom": "19.3.0",
59
59
  "@vitest/coverage-v8": "5.0.5",
60
60
  "effect": "4.0.6",
61
- "effect-atom-boundary": "0.1.1",
61
+ "effect-atom-boundary": "0.1.2",
62
62
  "happy-dom": "21.0.4",
63
63
  "react": "19.3.1",
64
64
  "react-dom": "19.3.1",