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