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.
- package/LICENSE +21 -0
- package/README.md +89 -2
- package/dist/Hooks.svelte.d.ts +396 -0
- package/dist/Hooks.svelte.js +1139 -0
- package/dist/HydrationBoundary.svelte +127 -0
- package/dist/HydrationBoundary.svelte.d.ts +36 -0
- package/dist/RegistryContext.d.ts +117 -0
- package/dist/RegistryContext.js +85 -0
- package/dist/RegistryProvider.svelte +60 -0
- package/dist/RegistryProvider.svelte.d.ts +31 -0
- package/dist/ScopedAtom.d.ts +73 -0
- package/dist/ScopedAtom.js +55 -0
- package/dist/SvelteKit.d.ts +78 -0
- package/dist/SvelteKit.js +90 -0
- package/dist/index.d.ts +50 -0
- package/dist/index.js +50 -0
- package/dist/internal/hydration.d.ts +26 -0
- package/dist/internal/hydration.js +57 -0
- package/package.json +72 -4
|
@@ -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
|
+
}
|