effect-atom-svelte 0.0.0-stage → 0.1.1

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.
@@ -0,0 +1,1149 @@
1
+ /**
2
+ * Hooks that read, write and await atoms from Svelte 5 components.
3
+ *
4
+ * @since 0.1.0
5
+ */
6
+ import { Cause, Effect, Exit } from "effect";
7
+ import { AsyncResult, Atom, AtomRegistry } from "effect/reactivity";
8
+ import { BROWSER, DEV } from "esm-env";
9
+ import { getAbortSignal, hydratable, onDestroy, untrack } from "svelte";
10
+ import { createSubscriber } from "svelte/reactivity";
11
+ import { decodeSeed, encodeSeed, isWaiting, revalidatesOnHydrate, } from "./internal/hydration.js";
12
+ import { getRegistry } from "./RegistryContext.js";
13
+ const toGetter = (input) => typeof input === "function" && !Atom.isAtom(input)
14
+ ? input
15
+ : () => input;
16
+ // The registry notifies subscribers synchronously while it computes an atom, and Svelte throws
17
+ // state_unsafe_mutation if state changes while a template or $derived is evaluating. Reads are
18
+ // counted, and a notification raised during one is delivered on a microtask instead. Outside a
19
+ // read, notifications stay synchronous so writes from event handlers update in the same tick.
20
+ // useAtomSubscribe keeps its own order on top of this (see orderedDelivery).
21
+ let activeReads = 0;
22
+ const afterReads = (update) => {
23
+ if (activeReads > 0) {
24
+ queueMicrotask(update);
25
+ }
26
+ else {
27
+ update();
28
+ }
29
+ };
30
+ const notifyAfterReads = (update) => () => afterReads(update);
31
+ const duringRead = (f) => {
32
+ activeReads += 1;
33
+ try {
34
+ return f();
35
+ }
36
+ finally {
37
+ activeReads -= 1;
38
+ }
39
+ };
40
+ /**
41
+ * Runs `f` when the component is destroyed, even while its script is still awaiting. In the browser
42
+ * `onDestroy` only takes effect once the component mounts, so a component removed while pending
43
+ * would never call it. A pre effect runs during init and is torn down with the component (JND-16).
44
+ * On the server, `onDestroy` runs when the render ends.
45
+ */
46
+ const onTeardown = (f) => {
47
+ if (BROWSER) {
48
+ $effect.pre(() => f);
49
+ }
50
+ else {
51
+ onDestroy(f);
52
+ }
53
+ };
54
+ /** Holds read and write closures; a class so `.current` is a real accessor `bind:` can use. */
55
+ class AtomCell {
56
+ #read;
57
+ #write;
58
+ constructor(read, write) {
59
+ this.#read = read;
60
+ this.#write = write;
61
+ }
62
+ get current() {
63
+ return this.#read();
64
+ }
65
+ set current(value) {
66
+ this.#write(value);
67
+ }
68
+ }
69
+ const readOnly = () => {
70
+ throw new Error("This atom value is read-only");
71
+ };
72
+ const valueOrThrow = (exit) => {
73
+ if (Exit.isSuccess(exit)) {
74
+ return exit.value;
75
+ }
76
+ throw Cause.squash(exit.cause);
77
+ };
78
+ /**
79
+ * Whether the atom has a `withServerValue` override. The server reads that instead and never
80
+ * computes the atom, as its real read is often browser-only.
81
+ */
82
+ const hasServerValue = (atom) => Atom.ServerValueTypeId in atom;
83
+ // SAFETY: ensureNode and scheduleNodeRemoval are on the registry implementation, not the interface
84
+ // (Effect 4.0.0); @effect/atom-react uses ensureNode the same way.
85
+ const internals = (registry) => registry;
86
+ /** The atom that holds a value given to `atom`: a wrapper such as `withRefresh` passes it on. */
87
+ const initialValueTarget = (atom) => {
88
+ let target = atom;
89
+ while (target.initialValueTarget) {
90
+ target = target.initialValueTarget;
91
+ }
92
+ return target;
93
+ };
94
+ const noop = () => undefined;
95
+ /**
96
+ * Keeps an atom's node in the registry without computing it, as a mount would but without the read.
97
+ * The registry sweeps a node without listeners, so a listener holds it; letting go schedules the
98
+ * sweep, as an unsubscribe does.
99
+ */
100
+ const holdNode = (registry, node) => {
101
+ const unsubscribe = node.subscribe(noop);
102
+ return () => {
103
+ unsubscribe();
104
+ if (node.canBeRemoved) {
105
+ registry.scheduleNodeRemoval(node);
106
+ }
107
+ };
108
+ };
109
+ const awaitResult = (registry, atom, options, signal) => Effect.runPromiseExit(AtomRegistry.getResult(registry, atom, options), {
110
+ signal,
111
+ });
112
+ const subscribedReader = (registry, getAtom) => {
113
+ if (!BROWSER) {
114
+ // Nothing subscribes during SSR, so without a mount the registry would sweep the node while the
115
+ // render awaits, losing initial values and refetching async atoms. Mounting at setup, before any
116
+ // await, keeps it for the request. The mounts are released in onDestroy, which runs when the
117
+ // server render ends, because a registry passed in by the caller outlives the request (JND-17).
118
+ // An atom with a withServerValue override is never computed on the server, so it is not mounted:
119
+ // mounting would run its real read, which is often browser-only. Nor is one that holds a value
120
+ // from useAtomInitialValues: the server renders that value, which is what the atom's first build
121
+ // would keep, without running a read that may be browser-only or start a request.
122
+ const releases = [];
123
+ onDestroy(() => {
124
+ for (const release of releases) {
125
+ release();
126
+ }
127
+ });
128
+ const nodes = internals(registry);
129
+ const mount = (atom) => {
130
+ if (hasServerValue(atom)) {
131
+ return atom;
132
+ }
133
+ const node = nodes.ensureNode(atom);
134
+ releases.push(node.preserveInitialValueOnBuild
135
+ ? holdNode(nodes, node)
136
+ : registry.mount(atom));
137
+ return atom;
138
+ };
139
+ let mounted = mount(getAtom());
140
+ return () => {
141
+ const atom = getAtom();
142
+ if (atom !== mounted) {
143
+ mounted = mount(atom);
144
+ }
145
+ if (!hasServerValue(atom)) {
146
+ const node = nodes.ensureNode(atom);
147
+ if (node.preserveInitialValueOnBuild) {
148
+ return node._value;
149
+ }
150
+ }
151
+ return Atom.getServerValue(atom, registry);
152
+ };
153
+ }
154
+ // The atom is picked on every read, not held in a $derived. In async mode Svelte renders a batch
155
+ // with other pending batches' changes rolled back, deriveds included, but registry reads always
156
+ // see the latest state; a derived atom could then pair an old atom with new state, and that
157
+ // render could commit last and stick (JND-23). One subscription follows whichever atom was read.
158
+ let atom;
159
+ let notify;
160
+ let cancel;
161
+ // The atom the last commit picked. When a read switches away from it, its subscription is kept
162
+ // until a commit picks another: a render with the switch rolled back reads it again, and a
163
+ // released atom would compute again (JND-35). Atoms no commit picked are released at once, so an
164
+ // abandoned pending atom is still interrupted (JND-16).
165
+ let committed;
166
+ let kept;
167
+ const releaseKept = () => {
168
+ kept?.cancel();
169
+ kept = undefined;
170
+ };
171
+ // Whether the component has mounted: its effects have run once.
172
+ let mounted = false;
173
+ // The atom this reader's own registry.get is reading. A subscribed node is rebuilt as soon as it
174
+ // goes stale, so the read builds it only the first time, and that build announces the very value
175
+ // the read returns. Before the component mounts, that announcement can only re-render its first
176
+ // render, which already has the value, so it is dropped. Delivered on a microtask it started a
177
+ // Svelte batch that, when the first render ran after an await in markup (a HydrationBoundary
178
+ // awaiting its state), committed before hydration's own and made Svelte's dev build throw "Batch
179
+ // has scheduled effects" (JND-95). After mount it is still delivered, as before. Nothing depends
180
+ // on the update it starts: a getter switched in onMount once did, by accident, as that update
181
+ // rendered before the registry swept the old atom (JND-98, fixed in the effect below).
182
+ let reading;
183
+ const listen = (current, update) => registry.subscribe(current, () => {
184
+ if (mounted || reading !== current) {
185
+ update();
186
+ }
187
+ });
188
+ const follow = (current) => {
189
+ if (kept?.atom === current) {
190
+ const { cancel: keptCancel } = kept;
191
+ kept = undefined;
192
+ return keptCancel;
193
+ }
194
+ return notify ? listen(current, notify) : undefined;
195
+ };
196
+ $effect(() => {
197
+ mounted = true;
198
+ committed = getAtom();
199
+ if (kept && kept.atom !== committed) {
200
+ releaseKept();
201
+ }
202
+ // A render with the switch rolled back can read the old atom after the render that commits, so
203
+ // the subscription may follow an atom the commit did not pick. It moves to the committed atom,
204
+ // or that atom's later changes, a refresh's result or failure included, would not reach the
205
+ // page (JND-93).
206
+ // The atom it leaves is kept, not released: this effect can run before the switch it reads has
207
+ // committed. A getter switched in onMount is read here in the same flush, while Svelte's batch
208
+ // for the switch is still pending, and the renders with that batch rolled back read the old
209
+ // atom. Released here, the registry could sweep it before they do, and they would compute it
210
+ // again (JND-98). A later run that commits another atom releases it.
211
+ if (atom !== undefined && atom !== committed) {
212
+ const previous = atom;
213
+ const previousCancel = cancel;
214
+ atom = committed;
215
+ cancel = follow(committed);
216
+ if (previousCancel) {
217
+ releaseKept();
218
+ kept = { atom: previous, cancel: previousCancel };
219
+ }
220
+ }
221
+ });
222
+ const subscribe = createSubscriber((update) => {
223
+ notify = notifyAfterReads(update);
224
+ cancel = atom ? follow(atom) : undefined;
225
+ return () => {
226
+ cancel?.();
227
+ cancel = undefined;
228
+ releaseKept();
229
+ notify = undefined;
230
+ };
231
+ });
232
+ return () => duringRead(() => {
233
+ const current = getAtom();
234
+ if (current !== atom) {
235
+ const previous = atom;
236
+ const previousCancel = cancel;
237
+ atom = current;
238
+ cancel = follow(current);
239
+ if (previous === committed &&
240
+ previous !== undefined &&
241
+ previousCancel) {
242
+ releaseKept();
243
+ kept = { atom: previous, cancel: previousCancel };
244
+ }
245
+ else {
246
+ previousCancel?.();
247
+ }
248
+ }
249
+ subscribe();
250
+ const outer = reading;
251
+ reading = current;
252
+ try {
253
+ return registry.get(current);
254
+ }
255
+ finally {
256
+ reading = outer;
257
+ }
258
+ });
259
+ };
260
+ export function useAtomValue(input, f) {
261
+ const read = subscribedReader(getRegistry(), toGetter(input));
262
+ if (!f) {
263
+ return new AtomCell(read, readOnly);
264
+ }
265
+ // A derived keeps the transform's result, and its identity, until the atom or state the transform
266
+ // reads changes. Outside a reactive context nothing subscribes, so the derived would not hear the
267
+ // atom change: such a read runs the transform again (JND-25).
268
+ const mapped = $derived(f(read()));
269
+ return new AtomCell(() => ($effect.tracking() ? mapped : f(read())), readOnly);
270
+ }
271
+ /**
272
+ * Reads and writes a writable atom through `.current`, so `bind:value={state.current}` works.
273
+ *
274
+ * **Example** (Binding an input to an atom)
275
+ *
276
+ * ```ts
277
+ * import { useAtom } from "effect-atom-svelte";
278
+ * import { nameAtom } from "./atoms.ts";
279
+ *
280
+ * const name = useAtom(nameAtom);
281
+ * // In the markup: <input bind:value={name.current} />
282
+ * ```
283
+ *
284
+ * @stability unstable
285
+ * @since 0.1.0
286
+ * @category hooks
287
+ */
288
+ export const useAtom = (input) => {
289
+ const registry = getRegistry();
290
+ const getAtom = toGetter(input);
291
+ return new AtomCell(subscribedReader(registry, getAtom), (value) => registry.set(getAtom(), value));
292
+ };
293
+ /**
294
+ * Keeps an atom alive for as long as this component is mounted, even when no component reads
295
+ * it. Use it in a component that outlives the readers, such as a layout, so the atom keeps its
296
+ * value while they come and go. Mounting an atom that hasn't been computed also computes it.
297
+ *
298
+ * **Example** (Keeping a connection open while a component is mounted)
299
+ *
300
+ * ```ts
301
+ * import { useAtomMount } from "effect-atom-svelte";
302
+ * import { socketAtom } from "./atoms.ts";
303
+ *
304
+ * // Not disposed while this component lives, even though nothing reads it.
305
+ * useAtomMount(socketAtom);
306
+ * ```
307
+ *
308
+ * @stability unstable
309
+ * @since 0.1.0
310
+ * @category hooks
311
+ */
312
+ export const useAtomMount = (input) => {
313
+ const registry = getRegistry();
314
+ const getAtom = toGetter(input);
315
+ $effect(() => registry.mount(getAtom()));
316
+ };
317
+ export function useAtomSet(input, options) {
318
+ const registry = getRegistry();
319
+ const getAtom = toGetter(input);
320
+ useAtomMount(getAtom);
321
+ const mode = options?.mode ?? "value";
322
+ if (mode === "value") {
323
+ return (value) => {
324
+ const atom = getAtom();
325
+ registry.set(atom, typeof value === "function"
326
+ ? value(registry.get(atom))
327
+ : value);
328
+ };
329
+ }
330
+ return async (value, writeOptions) => {
331
+ if (value === Atom.Reset) {
332
+ // Excluded by the types: a reset result is Initial, which never settles, so the wait would
333
+ // never end.
334
+ throw new TypeError(`useAtomSet's ${mode} mode cannot wait for Atom.Reset; reset with a value mode setter`);
335
+ }
336
+ const atom = getAtom();
337
+ // An already aborted signal rejects without writing, as fetch does (JND-25).
338
+ if (!writeOptions?.signal?.aborted) {
339
+ registry.set(atom, value);
340
+ }
341
+ const exit = await awaitResult(registry, atom, { suspendOnWaiting: true }, writeOptions?.signal);
342
+ return mode === "promiseExit" ? exit : valueOrThrow(exit);
343
+ };
344
+ }
345
+ /**
346
+ * Returns a function that runs the atom again. The atom is mounted so the refresh is not lost.
347
+ *
348
+ * **Example** (Running an atom again on click)
349
+ *
350
+ * ```ts
351
+ * import { useAtomRefresh } from "effect-atom-svelte";
352
+ * import { dieAtom } from "./atoms.ts";
353
+ *
354
+ * const roll = useAtomRefresh(dieAtom);
355
+ * // In the markup: <button onclick={roll}>Roll</button>
356
+ * ```
357
+ *
358
+ * @stability unstable
359
+ * @since 0.1.0
360
+ * @category hooks
361
+ */
362
+ export const useAtomRefresh = (input) => {
363
+ const registry = getRegistry();
364
+ const getAtom = toGetter(input);
365
+ useAtomMount(getAtom);
366
+ return () => registry.refresh(getAtom());
367
+ };
368
+ /**
369
+ * Delivers each value to `f` as afterReads would, but in order and only while subscribed. Once a
370
+ * change raised during a read is deferred, later changes queue behind it, so `f` never hears an
371
+ * older value after a newer one; whatever is still queued when the subscription ends is dropped.
372
+ */
373
+ const orderedDelivery = (f) => {
374
+ let live = true;
375
+ let flushing = false;
376
+ const queue = [];
377
+ // Stopping empties the queue, which ends a flush in progress.
378
+ const flush = () => {
379
+ try {
380
+ while (queue.length > 0) {
381
+ f(queue.shift());
382
+ }
383
+ }
384
+ finally {
385
+ // Still queued only if `f` threw: the rest goes out on the next microtask.
386
+ flushing = queue.length > 0;
387
+ if (flushing) {
388
+ queueMicrotask(flush);
389
+ }
390
+ }
391
+ };
392
+ const deliver = (value) => {
393
+ if (!live) {
394
+ return;
395
+ }
396
+ if (flushing || activeReads > 0) {
397
+ queue.push(value);
398
+ if (!flushing) {
399
+ flushing = true;
400
+ queueMicrotask(flush);
401
+ }
402
+ }
403
+ else {
404
+ f(value);
405
+ }
406
+ };
407
+ return Object.assign(deliver, {
408
+ stop: () => {
409
+ live = false;
410
+ queue.length = 0;
411
+ },
412
+ });
413
+ };
414
+ /**
415
+ * Calls `f` on every change while the component lives, and with the current value first when
416
+ * `immediate` is set. The atom is computed once the component mounts, and never on the server, so a
417
+ * derived or effect atom that nothing else reads still runs and reports its changes. A change raised while another component
418
+ * is reading an atom reaches `f` on a microtask, so `f` can write `$state` (Svelte forbids that
419
+ * during a read). Changes after it wait their turn, so `f` sees every change in order; any other
420
+ * change reaches it synchronously. Nothing reaches `f` once the component is destroyed, not even a
421
+ * change still waiting for its microtask.
422
+ *
423
+ * **Example** (Saving every change)
424
+ *
425
+ * ```ts
426
+ * import { useAtomSubscribe } from "effect-atom-svelte";
427
+ * import { draftAtom } from "./atoms.ts";
428
+ *
429
+ * useAtomSubscribe(draftAtom, (draft) => localStorage.setItem("draft", draft));
430
+ * ```
431
+ *
432
+ * @stability unstable
433
+ * @since 0.1.0
434
+ * @category hooks
435
+ */
436
+ export const useAtomSubscribe = (input, f, options) => {
437
+ const registry = getRegistry();
438
+ const getAtom = toGetter(input);
439
+ $effect(() => {
440
+ const atom = getAtom();
441
+ // `immediate` calls `f` now, inside this effect; what `f` reads must not re-run it.
442
+ return untrack(() => {
443
+ // registry.subscribe does not compute a node that has never been read, so an atom nothing
444
+ // else reads would never run, and a derived atom would never hear its sources. Reading it
445
+ // first builds it.
446
+ const value = duringRead(() => registry.get(atom));
447
+ if (options?.immediate === true) {
448
+ f(value);
449
+ }
450
+ const deliver = orderedDelivery(f);
451
+ const cancel = registry.subscribe(atom, deliver);
452
+ return () => {
453
+ deliver.stop();
454
+ cancel();
455
+ };
456
+ });
457
+ });
458
+ };
459
+ /**
460
+ * Gives an atom a value without computing it. The public registry can only queue a value for an
461
+ * atom's next read; this sets it on the node now, which is valid, so nothing reads it again.
462
+ */
463
+ const setNodeValue = (registry, atom, value) => {
464
+ internals(registry).ensureNode(atom).setValue(value);
465
+ };
466
+ // The nodes given a value by useAtomInitialValues. A node belongs to one registry, and a node the
467
+ // registry sweeps is gone from here too, so the next component to start it gives it the value again.
468
+ const initialValuesApplied = new WeakSet();
469
+ /**
470
+ * Sets starting values as `AtomRegistry.make({ initialValues })` would: on the atom that receives it
471
+ * (a wrapper such as `withRefresh` passes it to its source), kept as the value of the atom's first
472
+ * build, which still reads and follows the atom's sources. The atoms are not computed here: each
473
+ * keeps its value, uncomputed, while this component lives, and the first component to read it
474
+ * computes it from there, so a value set in a layout is still there when a page reads it later, and
475
+ * an atom that can only compute in the browser can start from a value on the server.
476
+ *
477
+ * A value applies once while the atom is held, so a component mounted later, or a second
478
+ * component with its own value, does not overwrite a value the atom has moved on from. Once nothing
479
+ * holds the atom and the registry disposes it, the next component to start it applies its value
480
+ * again. On the server a value lasts one render, so each request against a shared registry applies
481
+ * its own; requests rendering at the same time share the registry's atoms, and with them the value
482
+ * the first one applied.
483
+ *
484
+ * **Example** (Starting an atom from a prop)
485
+ *
486
+ * ```ts
487
+ * import { untrack } from "svelte";
488
+ * import { useAtomInitialValues, useAtomValue } from "effect-atom-svelte";
489
+ * import { countAtom } from "./atoms.ts";
490
+ *
491
+ * const { start } = $props();
492
+ * // Only the first value counts, so untrack says a later change to the prop is not followed.
493
+ * useAtomInitialValues([[countAtom, untrack(() => start)]]);
494
+ * const count = useAtomValue(countAtom);
495
+ * ```
496
+ *
497
+ * @stability unstable
498
+ * @since 0.1.0
499
+ * @category hooks
500
+ */
501
+ export const useAtomInitialValues = (initialValues) => {
502
+ const registry = internals(getRegistry());
503
+ const releases = [];
504
+ for (const [atom, value] of initialValues) {
505
+ const node = registry.ensureNode(initialValueTarget(atom));
506
+ if (!initialValuesApplied.has(node)) {
507
+ initialValuesApplied.add(node);
508
+ node.setInitialValue(value);
509
+ if (!BROWSER) {
510
+ // A registry passed in by the caller outlives the request, and its node may not be swept
511
+ // before the next request starts, whose value must apply too (JND-17).
512
+ releases.push(() => initialValuesApplied.delete(node));
513
+ }
514
+ }
515
+ releases.push(holdNode(registry, node));
516
+ }
517
+ onTeardown(() => {
518
+ for (const release of releases) {
519
+ release();
520
+ }
521
+ });
522
+ };
523
+ /**
524
+ * Reads an `AtomRef`, following it when the getter returns a different ref. For one property of a
525
+ * ref, use `useAtomRefPropValue`.
526
+ *
527
+ * A ref has no registry. On the server, one created at module level is shared by every request,
528
+ * so don't write a visitor's data to it there: see
529
+ * https://atom.jarrednorris.dev/server-rendering#module-state-is-shared-between-visitors
530
+ *
531
+ * **Example** (Reading a ref)
532
+ *
533
+ * ```ts
534
+ * import { useAtomRef } from "effect-atom-svelte";
535
+ * import { profile } from "./refs.ts";
536
+ *
537
+ * const current = useAtomRef(profile);
538
+ * // In the markup: {current.current.name}
539
+ * ```
540
+ *
541
+ * @stability unstable
542
+ * @since 0.1.0
543
+ * @category hooks
544
+ */
545
+ export const useAtomRef = (input) => {
546
+ const getRef = toGetter(input);
547
+ if (!BROWSER) {
548
+ return new AtomCell(() => getRef().value, readOnly);
549
+ }
550
+ const ref = $derived(getRef());
551
+ const subscribe = $derived.by(() => {
552
+ const current = ref;
553
+ return createSubscriber((update) => current.subscribe(notifyAfterReads(update)));
554
+ });
555
+ return new AtomCell(() => duringRead(() => {
556
+ subscribe();
557
+ return ref.value;
558
+ }), readOnly);
559
+ };
560
+ /**
561
+ * Returns `ref.prop(prop)`, the `AtomRef` for one property of an `AtomRef`. It takes the ref
562
+ * itself, not a getter, and does not follow later changes to `ref` or `prop`; to read a property
563
+ * of a ref picked by a getter, use `useAtomRefPropValue`.
564
+ *
565
+ * **Example** (Writing one property of a ref)
566
+ *
567
+ * ```ts
568
+ * import { useAtomRefProp } from "effect-atom-svelte";
569
+ * import { profile } from "./refs.ts";
570
+ *
571
+ * const name = useAtomRefProp(profile, "name");
572
+ * const rename = (next: string) => name.set(next); // profile.value.name changes too
573
+ * ```
574
+ *
575
+ * @stability unstable
576
+ * @since 0.1.0
577
+ * @category hooks
578
+ */
579
+ export const useAtomRefProp = (ref, prop) => ref.prop(prop);
580
+ /**
581
+ * Reads one property of an `AtomRef`, following the getter to a different ref. `prop` is taken
582
+ * once, when the component starts, and is not followed.
583
+ *
584
+ * **Example** (Reading one property of a ref)
585
+ *
586
+ * ```ts
587
+ * import { useAtomRefPropValue } from "effect-atom-svelte";
588
+ * import { profile } from "./refs.ts";
589
+ *
590
+ * const name = useAtomRefPropValue(profile, "name");
591
+ * // In the markup: {name.current}
592
+ * ```
593
+ *
594
+ * @stability unstable
595
+ * @since 0.1.0
596
+ * @category hooks
597
+ */
598
+ export const useAtomRefPropValue = (input, prop) => {
599
+ const getRef = toGetter(input);
600
+ const propRef = $derived(getRef().prop(prop));
601
+ return useAtomRef(() => propRef);
602
+ };
603
+ /**
604
+ * Whether a result is `Initial` with nothing running to settle it, as an `Atom.fn` nothing has
605
+ * called. On the server nothing will start it during the render, so waiting would hang it.
606
+ */
607
+ const notStarted = (result) => result._tag === "Initial" && !result.waiting;
608
+ const notStartedError = (hook) => new Error(`${hook} read an atom that has not started on the server: its result is Initial and nothing is running it, as for an Atom.fn that has not been called, so the render would wait for it forever. Read it with useAtomValue, which renders Initial, or inside a <svelte:boundary> with a pending snippet, which the server renders instead.`);
609
+ /**
610
+ * Puts the server's value in the registry as current, so nothing fetches it again (JND-19). Seeding
611
+ * through `setSerializable` would not: for an atom that wraps another, such as a query with
612
+ * reactivity keys, the registry seeds the inner atom but marks it stale, and building it to wire up
613
+ * reactivity starts the request again. The value goes on the innermost atom, where the registry
614
+ * would put it, so the wrapper still builds and wires up on first read.
615
+ */
616
+ const applySeed = (registry, atom, value, revalidate) => {
617
+ const target = initialValueTarget(atom);
618
+ setNodeValue(registry, target, value);
619
+ // Released at once, so the registry sweeps the node if the atom is not mounted soon.
620
+ registry.mount(target)();
621
+ if (revalidate) {
622
+ registry.refresh(target);
623
+ }
624
+ };
625
+ const seeds = new WeakMap();
626
+ // hydratable returns one promise per key per render, so it identifies the atom that claimed a key.
627
+ const serverSeeds = new WeakMap();
628
+ /**
629
+ * On the server each request seeds for itself: `hydratable` already shares one result per key
630
+ * within a render, and a per-registry record would outlive the request when the caller owns the
631
+ * registry, so later requests would skip embedding their seed (JND-17).
632
+ */
633
+ const seedOnServer = (registry, atom, key, encode) => {
634
+ // Mounted until the render ends, so the settled node is what the render reads.
635
+ const release = registry.mount(atom);
636
+ onDestroy(release);
637
+ // hydratable hands every later reader of a key the first reader's value, but Svelte's dev build
638
+ // also runs each later reader's callback and throws hydratable_clobbering if what it encodes
639
+ // differs. Reading the atom again there would encode whatever it holds by then, so a later
640
+ // reader's callback encodes the first reader's seed instead: the callback runs inside hydratable,
641
+ // and once it has returned, `claim.first` says whose call it was.
642
+ const claim = {};
643
+ const encoded = hydratable(key, () => {
644
+ claim.own = (async () => {
645
+ await undefined;
646
+ if (claim.first !== claim.own) {
647
+ return await claim.first;
648
+ }
649
+ // Not waited for if nothing has started it: the hook rejects once seeded.
650
+ if (!notStarted(registry.get(atom))) {
651
+ await awaitResult(registry, atom);
652
+ }
653
+ return encodeSeed(key, encode, registry.get(atom));
654
+ })();
655
+ return claim.own;
656
+ });
657
+ claim.first = encoded;
658
+ const claimed = serverSeeds.get(encoded);
659
+ if (claimed && claimed !== atom) {
660
+ throw new Error(`Two different atoms share the serialization key "${key}"`);
661
+ }
662
+ serverSeeds.set(encoded, atom);
663
+ return (async () => {
664
+ await encoded;
665
+ })();
666
+ };
667
+ const noSeedMount = () => undefined;
668
+ /**
669
+ * The keys of the server's values on this page that a reader has had its chance at, per page: for
670
+ * each, `hydratable` either returned the server's value or the development warning below was shown.
671
+ * Svelte keeps the values on `window.__svelte.h` and never removes them, so a key a reader has had
672
+ * is not missed when a later render, after client-side navigation, reads it again.
673
+ */
674
+ const claimedKeys = new WeakMap();
675
+ // The store is not public API, so a Svelte that changes its shape or drops it turns the warning off
676
+ // rather than throwing.
677
+ const serverValues = () => {
678
+ const store = globalThis.__svelte?.h;
679
+ return store instanceof Map ? store : undefined;
680
+ };
681
+ const warnIfSent = async (key, sent) => {
682
+ try {
683
+ if ((await sent) === undefined) {
684
+ return;
685
+ }
686
+ }
687
+ catch {
688
+ return;
689
+ }
690
+ console.warn(`effect-atom-svelte: the atom with serialization key "${key}" got no value from the server, so it runs again in the browser. Svelte stops hydrating at a component script's first top-level \`await\`, so call useAtomResult and useAtomSuspense before it, for example in one Promise.all. See https://atom.jarrednorris.dev/hydration#call-hooks-before-the-first-await`);
691
+ };
692
+ /**
693
+ * Records that a reader had its chance at the server's value for `key`. Development builds warn
694
+ * when it missed one the server sent for it: Svelte reads them only while it is hydrating, which
695
+ * stops at a component script's first top-level `await`, so a hook called after one gets nothing
696
+ * and its atom runs again in the browser (JND-96, fix proposed in sveltejs/svelte#18927). This reads
697
+ * Svelte's internal store only to warn.
698
+ */
699
+ const claimServerValue = (key, missed) => {
700
+ const store = DEV ? serverValues() : undefined;
701
+ if (store === undefined) {
702
+ return;
703
+ }
704
+ let claimed = claimedKeys.get(store);
705
+ if (!claimed) {
706
+ claimed = new Set();
707
+ claimedKeys.set(store, claimed);
708
+ }
709
+ if (claimed.has(key)) {
710
+ return;
711
+ }
712
+ claimed.add(key);
713
+ if (!missed || !store.has(key)) {
714
+ return;
715
+ }
716
+ // A result the server couldn't pass on, such as a defect, is sent as nothing: missing it changes
717
+ // nothing. The value may still be on its way, as a promise.
718
+ void warnIfSent(key, store.get(key));
719
+ };
720
+ /**
721
+ * For the getter's first atom, if serializable, resolves it and passes the encoded result from
722
+ * server to client with `hydratable`, so hydration seeds the registry instead of fetching again.
723
+ * Must run synchronously during component init. Returns undefined for atoms without a
724
+ * serialization key. The atom is fetched again once seeded if any component using the key then
725
+ * asked to revalidate.
726
+ */
727
+ const seedFromServer = (registry, getAtom, revalidateOption) => {
728
+ const atom = getAtom();
729
+ // The server never computes an atom with a server value, so it has no value to pass on, and the
730
+ // browser must not ask for one: hydratable throws for a key the server didn't write (JND-58).
731
+ if (!Atom.isSerializable(atom) || hasServerValue(atom)) {
732
+ return undefined;
733
+ }
734
+ const { decode, encode, key } = atom[Atom.SerializableTypeId];
735
+ if (!BROWSER) {
736
+ return {
737
+ done: seedOnServer(registry, atom, key, encode),
738
+ letGo: noSeedMount,
739
+ };
740
+ }
741
+ const revalidate = revalidatesOnHydrate(revalidateOption);
742
+ let registrySeeds = seeds.get(registry);
743
+ if (!registrySeeds) {
744
+ registrySeeds = { held: new Map(), spent: new Set() };
745
+ seeds.set(registry, registrySeeds);
746
+ }
747
+ const { held, spent } = registrySeeds;
748
+ let entry = held.get(key);
749
+ if (entry && entry.atom !== atom) {
750
+ throw new Error(`Two different atoms share the serialization key "${key}"`);
751
+ }
752
+ if (!entry && spent.has(key)) {
753
+ return undefined;
754
+ }
755
+ if (!entry) {
756
+ // hydratable runs this only when it has no value from the server, as after client-side
757
+ // navigation. It fetches nothing: the hook fetches the atom itself, with a wait that is
758
+ // interrupted when the component goes away (JND-57). A wait here would hold the atom until
759
+ // its request finished.
760
+ let computedHere = false;
761
+ const encoded = hydratable(key, () => {
762
+ computedHere = true;
763
+ return undefined;
764
+ });
765
+ claimServerValue(key, computedHere);
766
+ let holders = 0;
767
+ let revalidating = 0;
768
+ entry = {
769
+ atom,
770
+ done: (async () => {
771
+ const value = await encoded;
772
+ // Only the server's value is a seed, and only while someone is there to read it now: a seed
773
+ // set later would show data however old by then (JND-37).
774
+ // The server sends no seed for a result it can't or mustn't pass on, such as a defect: the
775
+ // atom computes here instead. A seed still waiting, as a stream's between its values, runs
776
+ // again too: the server's run ended with the render, so it would wait forever.
777
+ const seed = !computedHere && holders > 0 ? decodeSeed(value, decode) : undefined;
778
+ if (seed !== undefined) {
779
+ applySeed(registry, atom, seed, revalidating > 0 || isWaiting(seed));
780
+ }
781
+ })(),
782
+ hold: (wantsRevalidate) => {
783
+ const counted = wantsRevalidate ? 1 : 0;
784
+ holders += 1;
785
+ revalidating += counted;
786
+ return () => {
787
+ holders -= 1;
788
+ revalidating -= counted;
789
+ if (holders === 0) {
790
+ held.delete(key);
791
+ spent.add(key);
792
+ }
793
+ };
794
+ },
795
+ };
796
+ held.set(key, entry);
797
+ }
798
+ // Every caller holds the atom until it is destroyed, not only the first, which may go first and
799
+ // leave the others' seed to be swept (JND-36). It is mounted only once the seed is in: the
800
+ // seed must be in before the atom first computes, or mounting would fetch what hydration is about
801
+ // to provide.
802
+ const { done } = entry;
803
+ const unhold = entry.hold(revalidate);
804
+ let release;
805
+ let handedOver = false;
806
+ const letGo = () => {
807
+ handedOver = true;
808
+ release?.();
809
+ release = undefined;
810
+ };
811
+ let left = false;
812
+ const leave = () => {
813
+ if (!left) {
814
+ left = true;
815
+ unhold();
816
+ letGo();
817
+ }
818
+ };
819
+ onTeardown(leave);
820
+ // A committed switch to another atom lets go too: the hook's own subscription follows the getter,
821
+ // and this mount would keep the old atom running for nobody (JND-59).
822
+ $effect(() => {
823
+ if (getAtom() !== atom) {
824
+ leave();
825
+ }
826
+ });
827
+ return {
828
+ done: (async () => {
829
+ await done;
830
+ if (!left && !handedOver) {
831
+ release = registry.mount(atom);
832
+ }
833
+ })(),
834
+ letGo,
835
+ };
836
+ };
837
+ /**
838
+ * Awaits an async atom's first result, then returns a live handle to its `AsyncResult`. Use it as
839
+ * a top-level `await` in a component script; server rendering waits for it, and with a
840
+ * serialization key the browser starts from the server's result instead of running the atom
841
+ * again. With a getter, only the first atom is awaited: when the getter picks another atom the
842
+ * handle follows it, starting from that atom's current result (often `Initial`), and the
843
+ * component's await does not run again. On the server, an atom with a `withServerValue` override
844
+ * reads as that value and is never computed. An atom that has not started, whose result is
845
+ * `Initial` with nothing running, as an `Atom.fn` nothing has called, keeps the await pending in the
846
+ * browser until something writes it; on the server, where nothing will during the render, the
847
+ * await rejects.
848
+ *
849
+ * **Example** (Awaiting an atom picked by a prop)
850
+ *
851
+ * ```ts
852
+ * import { useAtomResult } from "effect-atom-svelte";
853
+ * import { todoAtom } from "./atoms.ts";
854
+ *
855
+ * const { id } = $props();
856
+ * const todo = await useAtomResult(() => todoAtom(id));
857
+ * // todo.current is the AsyncResult, a Success or a Failure by now
858
+ * ```
859
+ *
860
+ * @stability unstable
861
+ * @since 0.1.0
862
+ * @category async
863
+ */
864
+ export const useAtomResult = async (input, options) => {
865
+ const registry = getRegistry();
866
+ const getAtom = toGetter(input);
867
+ const value = useAtomValue(getAtom);
868
+ const atom = getAtom();
869
+ let release;
870
+ // Aborted when the component is destroyed, which interrupts the wait below so the atom is not held.
871
+ const lifetime = new AbortController();
872
+ onTeardown(() => {
873
+ lifetime.abort();
874
+ release?.();
875
+ });
876
+ // Later atoms are mounted by this effect, which runs once the component mounts, so usually after
877
+ // the await below. The first atom's mount is released then, as the effect holds it from there.
878
+ let effectHolds = false;
879
+ $effect(() => {
880
+ effectHolds = true;
881
+ const unmount = registry.mount(getAtom());
882
+ release?.();
883
+ release = undefined;
884
+ return unmount;
885
+ });
886
+ const seed = seedFromServer(registry, getAtom, options?.revalidateOnHydrate);
887
+ if (seed) {
888
+ await seed.done;
889
+ }
890
+ // Destroyed while seeding: reading the atom now would only compute it for nobody. On the server,
891
+ // an atom with a server value is read as that value, with nothing to wait for.
892
+ if (lifetime.signal.aborted || (!BROWSER && hasServerValue(atom))) {
893
+ return value;
894
+ }
895
+ if (effectHolds) {
896
+ // Mounted while seeding, as when the promise is not awaited straight away: the effect holds the
897
+ // getter's atom, and a mount here would never be released. Once the getter has switched away,
898
+ // waiting would only compute the first atom for nobody.
899
+ seed?.letGo();
900
+ if (untrack(getAtom) !== atom) {
901
+ return value;
902
+ }
903
+ }
904
+ else {
905
+ // Mounted only after seeding, so hydration's value is in place before the atom first computes.
906
+ release = registry.mount(atom);
907
+ seed?.letGo();
908
+ }
909
+ if (!BROWSER && notStarted(registry.get(atom))) {
910
+ throw notStartedError("useAtomResult");
911
+ }
912
+ await awaitResult(registry, atom, options, lifetime.signal);
913
+ return value;
914
+ };
915
+ /** Whether the hook waits for a result: it is `Initial`, or refreshing with `suspendOnWaiting`. */
916
+ const isPending = (current, options) => current._tag === "Initial" ||
917
+ (options.suspendOnWaiting === true && current.waiting);
918
+ /** What the promise resolves with, or the error it rejects with, for a result that is not pending. */
919
+ const fromSettled = (current, options) => {
920
+ if (current._tag === "Success") {
921
+ return options.includeFailure ? current : current.value;
922
+ }
923
+ if (current._tag === "Failure" && !options.includeFailure) {
924
+ throw Cause.squash(current.cause);
925
+ }
926
+ return current;
927
+ };
928
+ /**
929
+ * On the server, an atom with a server value resolves from that value and is never computed. An
930
+ * `Initial` server value, as from `withServerValueInitial`, has nothing to resolve with, and waiting
931
+ * would hang the render, so it rejects with what to do instead (JND-58).
932
+ */
933
+ const fromServerValue = (current, options) => {
934
+ if (isPending(current, options)) {
935
+ return Promise.reject(new Error("useAtomSuspense read an atom whose server value is pending, so the server has nothing to render. Read it inside a <svelte:boundary> with a pending snippet, which the server renders instead, or use useAtomResult."));
936
+ }
937
+ try {
938
+ return Promise.resolve(fromSettled(current, options));
939
+ }
940
+ catch (error) {
941
+ return Promise.reject(error);
942
+ }
943
+ };
944
+ const suspend = async (registry, atom, current, options, signal) => {
945
+ if (isPending(current, options)) {
946
+ if (!BROWSER && notStarted(current)) {
947
+ throw notStartedError("useAtomSuspense");
948
+ }
949
+ const exit = await awaitResult(registry, atom, { suspendOnWaiting: options.suspendOnWaiting }, signal);
950
+ // Abandoned: rejects with the reason its last reader's signal was aborted with. For a re-run
951
+ // that is Svelte's STALE_REACTION, which it ignores, so a pending render that still awaits this
952
+ // wait keeps waiting for its next run instead of showing the interruption (JND-22).
953
+ signal.throwIfAborted();
954
+ if (options.includeFailure) {
955
+ return Exit.isSuccess(exit)
956
+ ? AsyncResult.success(exit.value)
957
+ : AsyncResult.failure(exit.cause);
958
+ }
959
+ return valueOrThrow(exit);
960
+ }
961
+ return fromSettled(current, options);
962
+ };
963
+ /** The derived or effect reading now's abort signal, which Svelte aborts when it re-runs or is destroyed. */
964
+ const readerSignal = () => {
965
+ if (!BROWSER) {
966
+ return undefined;
967
+ }
968
+ try {
969
+ return getAbortSignal();
970
+ }
971
+ catch {
972
+ return undefined;
973
+ }
974
+ };
975
+ /**
976
+ * Mounts an atom until every signal has aborted. The release waits a microtask, as a reaction aborts
977
+ * its signal before it re-runs, and the re-run's own subscription takes over.
978
+ */
979
+ const holdWhileRead = (registry, atom, signals) => {
980
+ const release = registry.mount(atom);
981
+ let holders = signals.length;
982
+ const letGo = () => {
983
+ holders -= 1;
984
+ if (holders === 0) {
985
+ queueMicrotask(release);
986
+ }
987
+ };
988
+ for (const signal of signals) {
989
+ signal.addEventListener("abort", letGo, { once: true });
990
+ }
991
+ };
992
+ /**
993
+ * Once no reader holds a pending wait, it is interrupted so it stops holding the atom, which lets
994
+ * the registry dispose it and interrupt its request (JND-16), and `onAbandoned` drops it from the
995
+ * cache. A settled wait is kept, so its promise stays the same for the next reader.
996
+ */
997
+ const sharedWait = (start, onAbandoned) => {
998
+ const controller = new AbortController();
999
+ const promise = start(controller.signal);
1000
+ let settled = false;
1001
+ // Each holder's abort listener, removed once the wait settles: the signal may outlive it by far, as
1002
+ // the component's lifetime signal does, and would otherwise keep one listener per result it read.
1003
+ const listeners = [];
1004
+ void (async () => {
1005
+ try {
1006
+ await promise;
1007
+ }
1008
+ catch {
1009
+ // Rejections belong to the awaiting template; this stops an unread one being reported as unhandled.
1010
+ }
1011
+ settled = true;
1012
+ for (const [signal, listener] of listeners) {
1013
+ signal.removeEventListener("abort", listener);
1014
+ }
1015
+ listeners.length = 0;
1016
+ })();
1017
+ let holders = 0;
1018
+ // Each signal holds once: reads outside a reaction all share the component's lifetime signal, and
1019
+ // would otherwise add a holder and a listener per read until it is destroyed (JND-59).
1020
+ const held = new WeakSet();
1021
+ const release = (reason) => {
1022
+ holders -= 1;
1023
+ // A reaction aborts its signal before it re-runs, so wait a microtask: the re-run may read the
1024
+ // same result again and hold the wait instead of starting a new one.
1025
+ queueMicrotask(() => {
1026
+ if (holders === 0 && !settled && !controller.signal.aborted) {
1027
+ controller.abort(reason);
1028
+ onAbandoned();
1029
+ }
1030
+ });
1031
+ };
1032
+ return {
1033
+ hold: (signal) => {
1034
+ // A settled wait is never interrupted, so it needs no holders.
1035
+ if (settled || signal.aborted || held.has(signal)) {
1036
+ return;
1037
+ }
1038
+ held.add(signal);
1039
+ holders += 1;
1040
+ const listener = () => release(signal.reason);
1041
+ signal.addEventListener("abort", listener, { once: true });
1042
+ listeners.push([signal, listener]);
1043
+ },
1044
+ promise,
1045
+ };
1046
+ };
1047
+ export function useAtomSuspense(input, options = {}) {
1048
+ const registry = getRegistry();
1049
+ const getAtom = toGetter(input);
1050
+ const result = useAtomValue(getAtom);
1051
+ const seed = seedFromServer(registry, getAtom, options.revalidateOnHydrate);
1052
+ // A plain variable, not $state: a render in a batch with this write rolled back would take the
1053
+ // pre-seed path and track nothing but the flag. Reading `trackSeed` asks for a re-read instead.
1054
+ let seeded = seed === undefined;
1055
+ if (seed) {
1056
+ void (async () => {
1057
+ await seed.done;
1058
+ seeded = true;
1059
+ })();
1060
+ }
1061
+ const trackSeed = seed && BROWSER
1062
+ ? createSubscriber((update) => {
1063
+ let live = true;
1064
+ void (async () => {
1065
+ await seed.done;
1066
+ if (live) {
1067
+ update();
1068
+ }
1069
+ })();
1070
+ return () => {
1071
+ live = false;
1072
+ };
1073
+ })
1074
+ : undefined;
1075
+ // Per atom, the promise every reader before the seed shares, and those readers' signals.
1076
+ const afterSeed = new WeakMap();
1077
+ // Holds waits read outside any derived or effect (a top-level await in the script, an event
1078
+ // handler), which cannot say when they are done with them, until the component is destroyed.
1079
+ const lifetime = new AbortController();
1080
+ onTeardown(() => lifetime.abort());
1081
+ // Keyed by atom, then result: atoms can share a result object, as a derived atom returning its
1082
+ // source's result does, and a wait holds and waits for one atom.
1083
+ const waits = new WeakMap();
1084
+ const settle = (atom, current, signal) => {
1085
+ let atomWaits = waits.get(atom);
1086
+ if (!atomWaits) {
1087
+ atomWaits = new WeakMap();
1088
+ waits.set(atom, atomWaits);
1089
+ }
1090
+ let wait = atomWaits.get(current);
1091
+ if (!wait) {
1092
+ const forAtom = atomWaits;
1093
+ wait = sharedWait((abort) => suspend(registry, atom, current, options, abort), () => forAtom.delete(current));
1094
+ atomWaits.set(current, wait);
1095
+ }
1096
+ wait.hold(signal);
1097
+ return wait.promise;
1098
+ };
1099
+ const fromServer = new WeakMap();
1100
+ return new AtomCell(() => {
1101
+ if (!BROWSER && hasServerValue(getAtom())) {
1102
+ const { current } = result;
1103
+ let promise = fromServer.get(current);
1104
+ if (!promise) {
1105
+ promise = fromServerValue(current, options);
1106
+ fromServer.set(current, promise);
1107
+ }
1108
+ return promise;
1109
+ }
1110
+ const signal = readerSignal();
1111
+ if (!seeded && seed) {
1112
+ // Reading the atom before the seed lands would fetch what hydration is about to provide.
1113
+ const atom = getAtom();
1114
+ trackSeed?.();
1115
+ let pending = afterSeed.get(atom);
1116
+ if (!pending) {
1117
+ const readers = new Set();
1118
+ const promise = (async () => {
1119
+ await seed.done;
1120
+ // Held by the readers still there, not the component, so a getter switch while the first
1121
+ // load is pending interrupts it. None left, as when destroyed: nobody awaits this, and
1122
+ // reading would compute the atom for nobody.
1123
+ const live = [...readers].filter((reader) => !reader.aborted);
1124
+ if (live.length === 0) {
1125
+ return undefined;
1126
+ }
1127
+ holdWhileRead(registry, atom, live);
1128
+ seed.letGo();
1129
+ const current = registry.get(atom);
1130
+ let waited;
1131
+ for (const reader of live) {
1132
+ waited = settle(atom, current, reader);
1133
+ }
1134
+ return waited;
1135
+ })();
1136
+ pending = { promise, readers };
1137
+ afterSeed.set(atom, pending);
1138
+ }
1139
+ pending.readers.add(signal ?? lifetime.signal);
1140
+ return pending.promise;
1141
+ }
1142
+ const promise = settle(getAtom(), result.current, signal ?? lifetime.signal);
1143
+ if (signal) {
1144
+ // A reaction read it, so the reader's subscription holds the atom from here.
1145
+ seed?.letGo();
1146
+ }
1147
+ return promise;
1148
+ }, readOnly);
1149
+ }