effect-atom-svelte 0.1.2 → 0.3.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/README.md +1 -1
- package/dist/Hooks.svelte.d.ts +16 -10
- package/dist/Hooks.svelte.js +443 -143
- package/dist/HydrationBoundary.svelte +130 -12
- package/dist/HydrationBoundary.svelte.d.ts +13 -4
- package/dist/Inspector.d.ts +333 -0
- package/dist/Inspector.js +558 -0
- package/dist/RegistryContext.d.ts +2 -0
- package/dist/RegistryContext.js +22 -6
- package/dist/ScopedAtom.d.ts +5 -4
- package/dist/ScopedAtom.js +5 -4
- package/dist/SvelteKit.d.ts +28 -3
- package/dist/SvelteKit.js +9 -42
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/internal/boundaries.d.ts +9 -0
- package/dist/internal/boundaries.js +8 -0
- package/dist/internal/disposal.d.ts +13 -0
- package/dist/internal/disposal.js +43 -0
- package/dist/internal/hydration.d.ts +11 -2
- package/dist/internal/hydration.js +23 -3
- package/dist/internal/nodeInternals.d.ts +30 -0
- package/dist/internal/nodeInternals.js +107 -0
- package/dist/internal/registries.d.ts +6 -0
- package/dist/internal/registries.js +40 -0
- package/dist/internal/renderEnd.d.ts +13 -0
- package/dist/internal/renderEnd.js +34 -0
- package/dist/internal/scope.svelte.d.ts +29 -0
- package/dist/internal/scope.svelte.js +23 -0
- package/dist/internal/teardown.svelte.d.ts +17 -0
- package/dist/internal/teardown.svelte.js +44 -0
- package/dist/internal/troubleshooting.d.ts +2 -0
- package/dist/internal/troubleshooting.js +3 -0
- package/package.json +5 -1
package/dist/Hooks.svelte.js
CHANGED
|
@@ -6,9 +6,15 @@
|
|
|
6
6
|
import { Cause, Effect, Exit } from "effect";
|
|
7
7
|
import { AsyncResult, Atom, AtomRegistry } from "effect/reactivity";
|
|
8
8
|
import { BROWSER, DEV } from "esm-env";
|
|
9
|
-
import { getAbortSignal, hydratable,
|
|
9
|
+
import { getAbortSignal, hydratable, untrack } from "svelte";
|
|
10
10
|
import { createSubscriber } from "svelte/reactivity";
|
|
11
|
-
import {
|
|
11
|
+
import { queuedByBoundary } from "./internal/boundaries.js";
|
|
12
|
+
import { onDispose } from "./internal/disposal.js";
|
|
13
|
+
import { decodeSeed, encodeSeed, isWaiting, noSeed, revalidatesOnHydrate, } from "./internal/hydration.js";
|
|
14
|
+
import { onRenderEnd } from "./internal/renderEnd.js";
|
|
15
|
+
import { reportReads } from "./internal/scope.svelte.js";
|
|
16
|
+
import { onTeardown } from "./internal/teardown.svelte.js";
|
|
17
|
+
import { troubleshooting } from "./internal/troubleshooting.js";
|
|
12
18
|
import { getRegistry } from "./RegistryContext.js";
|
|
13
19
|
const toGetter = (input) => typeof input === "function" && !Atom.isAtom(input)
|
|
14
20
|
? input
|
|
@@ -37,20 +43,6 @@ const duringRead = (f) => {
|
|
|
37
43
|
activeReads -= 1;
|
|
38
44
|
}
|
|
39
45
|
};
|
|
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
46
|
/** Holds read and write closures; a class so `.current` is a real accessor `bind:` can use. */
|
|
55
47
|
class AtomCell {
|
|
56
48
|
#read;
|
|
@@ -67,7 +59,7 @@ class AtomCell {
|
|
|
67
59
|
}
|
|
68
60
|
}
|
|
69
61
|
const readOnly = () => {
|
|
70
|
-
throw new Error(
|
|
62
|
+
throw new Error(`This atom value is read-only: useAtomValue and useAtomSuspense only read, as does useAtomRef for a ref without set, such as one from map. Write an atom with useAtom or useAtomSet, and a writable ref by assigning useAtomRef's current. ${troubleshooting("this-atom-value-is-read-only")}`);
|
|
71
63
|
};
|
|
72
64
|
const valueOrThrow = (exit) => {
|
|
73
65
|
if (Exit.isSuccess(exit)) {
|
|
@@ -75,13 +67,20 @@ const valueOrThrow = (exit) => {
|
|
|
75
67
|
}
|
|
76
68
|
throw Cause.squash(exit.cause);
|
|
77
69
|
};
|
|
70
|
+
/**
|
|
71
|
+
* Whether a result is `Initial` with nothing running to settle it, as an `Atom.fn` nothing has
|
|
72
|
+
* called. On the server nothing will start it during the render, so waiting would hang it.
|
|
73
|
+
*/
|
|
74
|
+
const notStarted = (result) => result._tag === "Initial" && !result.waiting;
|
|
78
75
|
/**
|
|
79
76
|
* Whether the atom has a `withServerValue` override. The server reads that instead and never
|
|
80
77
|
* computes the atom, as its real read is often browser-only.
|
|
81
78
|
*/
|
|
82
79
|
const hasServerValue = (atom) => Atom.ServerValueTypeId in atom;
|
|
83
80
|
// 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.
|
|
81
|
+
// (Effect 4.0.0); @effect/atom-react uses ensureNode the same way. Of the node's parts,
|
|
82
|
+
// preserveInitialValueOnBuild, _value, setValue and failed (set while a first build has thrown)
|
|
83
|
+
// are on its implementation too; the rest, currentState included, are on Effect's Node interface.
|
|
85
84
|
const internals = (registry) => registry;
|
|
86
85
|
/** The atom that holds a value given to `atom`: a wrapper such as `withRefresh` passes it on. */
|
|
87
86
|
const initialValueTarget = (atom) => {
|
|
@@ -91,14 +90,14 @@ const initialValueTarget = (atom) => {
|
|
|
91
90
|
}
|
|
92
91
|
return target;
|
|
93
92
|
};
|
|
94
|
-
const noop = () => undefined;
|
|
95
93
|
/**
|
|
96
94
|
* Keeps an atom's node in the registry without computing it, as a mount would but without the read.
|
|
97
95
|
* The registry sweeps a node without listeners, so a listener holds it; letting go schedules the
|
|
98
|
-
* sweep, as an unsubscribe does.
|
|
96
|
+
* sweep, as an unsubscribe does. Each hold has its own listener: the node keeps its listeners in a
|
|
97
|
+
* set, so holds sharing one would all end with the first to let go.
|
|
99
98
|
*/
|
|
100
99
|
const holdNode = (registry, node) => {
|
|
101
|
-
const unsubscribe = node.subscribe(
|
|
100
|
+
const unsubscribe = node.subscribe(() => undefined);
|
|
102
101
|
return () => {
|
|
103
102
|
unsubscribe();
|
|
104
103
|
if (node.canBeRemoved) {
|
|
@@ -106,34 +105,182 @@ const holdNode = (registry, node) => {
|
|
|
106
105
|
}
|
|
107
106
|
};
|
|
108
107
|
};
|
|
108
|
+
/**
|
|
109
|
+
* Lets a node that has taken a value since it was given an initial one compute as usual again.
|
|
110
|
+
* Effect's `setValue` (4.0.1) leaves the mark that tells a node's next build to keep its initial
|
|
111
|
+
* value, so a node given a seed or a `HydrationBoundary` value over one would keep that through its
|
|
112
|
+
* next build, a refresh's, and drop the build's result. Call it once the node holds a value of its
|
|
113
|
+
* own: after `setValue`, or after subscribing, which builds a node still waiting for its first build
|
|
114
|
+
* (using the mark up) and, as a lookup, applies a value a `HydrationBoundary` queued. Only a valid
|
|
115
|
+
* node loses the mark: a stale one still keeps its initial value through its first build. The atoms
|
|
116
|
+
* a wrapper such as `withRefresh` passes an initial value to are cleared too, as subscribing to the
|
|
117
|
+
* wrapper builds them.
|
|
118
|
+
*/
|
|
119
|
+
const dropInitialValueMark = (registry, atom) => {
|
|
120
|
+
let current = atom;
|
|
121
|
+
while (current) {
|
|
122
|
+
const node = internals(registry).ensureNode(current);
|
|
123
|
+
if (node.currentState() === "valid") {
|
|
124
|
+
node.preserveInitialValueOnBuild = false;
|
|
125
|
+
}
|
|
126
|
+
current = current.initialValueTarget;
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
/**
|
|
130
|
+
* On the server, the node holding a value from `useAtomInitialValues` for an atom: its own, or its
|
|
131
|
+
* source's for a wrapper such as `withRefresh`, which passes the value on and reads as its source.
|
|
132
|
+
* The server renders that value: mounting, reading or waiting for the atom would build it, and a
|
|
133
|
+
* build runs the atom's read, the source's included, even when it keeps the value. That read may be
|
|
134
|
+
* browser-only or start a request the value was there to save.
|
|
135
|
+
*
|
|
136
|
+
* For a wrapper, this assumes it reads as its source, as every wrapper Effect makes with
|
|
137
|
+
* `initialValueTarget` does. A user's `Atom.transform` given `initialValueTarget` that changes the
|
|
138
|
+
* value would render its source's value on the server, where Effect computes the transform.
|
|
139
|
+
*/
|
|
140
|
+
const initialNodeOnServer = (registry, atom) => {
|
|
141
|
+
if (BROWSER) {
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
const node = internals(registry).ensureNode(initialValueTarget(atom));
|
|
145
|
+
return node.preserveInitialValueOnBuild ? node : undefined;
|
|
146
|
+
};
|
|
147
|
+
/** Whether, on the server, an atom renders a value from `useAtomInitialValues`. */
|
|
148
|
+
const initialOnServer = (registry, atom) => initialNodeOnServer(registry, atom) !== undefined;
|
|
149
|
+
/** Keeps an atom for the server render: a node with an initial value is held, not mounted. */
|
|
150
|
+
const serverMount = (registry, atom) => {
|
|
151
|
+
const node = initialNodeOnServer(registry, atom);
|
|
152
|
+
return node ? holdNode(internals(registry), node) : registry.mount(atom);
|
|
153
|
+
};
|
|
154
|
+
/** Reads an atom without building it on the server while it renders an initial value. */
|
|
155
|
+
const serverGet = (registry, atom) => {
|
|
156
|
+
const node = initialNodeOnServer(registry, atom);
|
|
157
|
+
return node ? node._value : registry.get(atom);
|
|
158
|
+
};
|
|
109
159
|
const awaitResult = (registry, atom, options, signal) => Effect.runPromiseExit(AtomRegistry.getResult(registry, atom, options), {
|
|
110
160
|
signal,
|
|
111
161
|
});
|
|
112
|
-
|
|
162
|
+
/** Whether a hook stops waiting at a result: it has left `Initial`, and isn't refreshing if asked. */
|
|
163
|
+
const settles = (result, suspendOnWaiting) => result._tag !== "Initial" && !(suspendOnWaiting && result.waiting);
|
|
164
|
+
/**
|
|
165
|
+
* Waits for an async atom to settle, as `awaitResult` does, but resolves with the atom's own result
|
|
166
|
+
* rather than an `Exit` rebuilt from it, which would drop a `Failure`'s `previousSuccess`.
|
|
167
|
+
* Interrupted, with a failed `Exit`, once `signal` aborts.
|
|
168
|
+
*/
|
|
169
|
+
const awaitSettled = (registry, atom, options, signal) => {
|
|
170
|
+
const suspendOnWaiting = options.suspendOnWaiting ?? false;
|
|
171
|
+
return Effect.runPromiseExit(Effect.callback((resume) => {
|
|
172
|
+
const current = registry.get(atom);
|
|
173
|
+
if (settles(current, suspendOnWaiting)) {
|
|
174
|
+
resume(Effect.succeed(current));
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
const cancel = registry.subscribe(atom, (value) => {
|
|
178
|
+
if (settles(value, suspendOnWaiting)) {
|
|
179
|
+
resume(Effect.succeed(value));
|
|
180
|
+
cancel();
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
return Effect.sync(cancel);
|
|
184
|
+
}), { signal });
|
|
185
|
+
};
|
|
186
|
+
/**
|
|
187
|
+
* A promise-mode setter's wait: as `awaitResult` with `suspendOnWaiting`, but a change to a result
|
|
188
|
+
* from before the call, which is what `Atom.Reset` from another setter leaves (an idle `Initial`,
|
|
189
|
+
* or an `Atom.fn`'s `initialValue`), ends it as interrupted. The reset interrupts the call, so
|
|
190
|
+
* otherwise the wait would hang until the next call, or settle with the initial value.
|
|
191
|
+
*/
|
|
192
|
+
const awaitWrite = (registry, atom, start, signal) => Effect.runPromiseExit(Effect.callback((resume) => {
|
|
193
|
+
const first = registry.get(atom);
|
|
194
|
+
if (first._tag !== "Initial" && !first.waiting) {
|
|
195
|
+
resume(AsyncResult.toExit(first));
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
const cancel = registry.subscribe(atom, (result) => {
|
|
199
|
+
if (result.waiting) {
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
cancel();
|
|
203
|
+
const reset = result._tag === "Initial" ||
|
|
204
|
+
(result._tag === "Success" && result.timestamp < start);
|
|
205
|
+
resume(reset
|
|
206
|
+
? Exit.interrupt()
|
|
207
|
+
: AsyncResult.toExit(result));
|
|
208
|
+
});
|
|
209
|
+
return Effect.sync(cancel);
|
|
210
|
+
}), { signal });
|
|
211
|
+
// On the server, per registry, how many renders' seeds carry each key, until those renders end.
|
|
212
|
+
const seededKeys = new WeakMap();
|
|
213
|
+
/** Whether a render using the registry sends the key's result to the browser with a seed. */
|
|
214
|
+
const seededOnServer = (registry, key) => (seededKeys.get(registry)?.get(key) ?? 0) > 0;
|
|
215
|
+
/** Counts the key as seeded until the render ends. */
|
|
216
|
+
const countSeeded = (registry, key) => {
|
|
217
|
+
let keys = seededKeys.get(registry);
|
|
218
|
+
if (!keys) {
|
|
219
|
+
keys = new Map();
|
|
220
|
+
seededKeys.set(registry, keys);
|
|
221
|
+
}
|
|
222
|
+
keys.set(key, (keys.get(key) ?? 0) + 1);
|
|
223
|
+
onRenderEnd(() => {
|
|
224
|
+
const count = (keys.get(key) ?? 1) - 1;
|
|
225
|
+
if (count > 0) {
|
|
226
|
+
keys.set(key, count);
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
keys.delete(key);
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
};
|
|
233
|
+
// The keys of the atoms useAtomValue has warned about, so the server warns once per key, not per
|
|
234
|
+
// request.
|
|
235
|
+
const warnedUnsent = new Set();
|
|
236
|
+
/**
|
|
237
|
+
* In development, warns once per key that a plain read on the server met a serializable async atom
|
|
238
|
+
* still running: only useAtomResult and useAtomSuspense send an atom's result to the browser, so
|
|
239
|
+
* the browser runs it again, and the page can show a loading state over what the server rendered.
|
|
240
|
+
* Not for a key the page sends anyway, with a seed or a HydrationBoundary's value, nor for a result
|
|
241
|
+
* nothing is computing, such as an Atom.fn nobody called or a value written to the atom.
|
|
242
|
+
*/
|
|
243
|
+
const warnUnsent = (registry, atom, value, hook) => {
|
|
244
|
+
if (!(DEV && Atom.isSerializable(atom) && AsyncResult.isAsyncResult(value)) ||
|
|
245
|
+
!value.waiting) {
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
const { key } = atom[Atom.SerializableTypeId];
|
|
249
|
+
if (warnedUnsent.has(key) ||
|
|
250
|
+
queuedByBoundary(registry, key) ||
|
|
251
|
+
seededOnServer(registry, key)) {
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
warnedUnsent.add(key);
|
|
255
|
+
console.warn(`effect-atom-svelte: ${hook} read the serializable atom "${key}" on the server while it was still running, but only useAtomResult and useAtomSuspense send an atom's result to the browser, so it runs again there. Read it with one of them. See https://atom.jarrednorris.dev/hydration#how-the-result-travels`);
|
|
256
|
+
};
|
|
257
|
+
/**
|
|
258
|
+
* Reads the getter's atom and follows it. `plainRead` names the plain read, `useAtomValue` or
|
|
259
|
+
* `useAtom`, for `warnUnsent`; the async hooks send their atom's result themselves.
|
|
260
|
+
*/
|
|
261
|
+
const subscribedReader = (registry, getAtom, plainRead) => {
|
|
113
262
|
if (!BROWSER) {
|
|
114
263
|
// Nothing subscribes during SSR, so without a mount the registry would sweep the node while the
|
|
115
264
|
// 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
|
|
117
|
-
//
|
|
265
|
+
// await, keeps it for the request. The mounts are released when the server render ends,
|
|
266
|
+
// because a registry passed in by the caller outlives the request (JND-17).
|
|
118
267
|
// 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.
|
|
120
|
-
//
|
|
121
|
-
//
|
|
268
|
+
// mounting would run its real read, which is often browser-only. One with a value from
|
|
269
|
+
// useAtomInitialValues is held and read without building it (see initialNodeOnServer).
|
|
270
|
+
// Once the render has ended, a read that switches atoms, as from a reader kept past the render,
|
|
271
|
+
// takes no mount: nothing would release it.
|
|
272
|
+
let ended = false;
|
|
122
273
|
const releases = [];
|
|
123
|
-
|
|
274
|
+
onRenderEnd(() => {
|
|
275
|
+
ended = true;
|
|
124
276
|
for (const release of releases) {
|
|
125
277
|
release();
|
|
126
278
|
}
|
|
127
279
|
});
|
|
128
|
-
const nodes = internals(registry);
|
|
129
280
|
const mount = (atom) => {
|
|
130
|
-
if (hasServerValue(atom)) {
|
|
131
|
-
|
|
281
|
+
if (!ended && !hasServerValue(atom)) {
|
|
282
|
+
releases.push(serverMount(registry, atom));
|
|
132
283
|
}
|
|
133
|
-
const node = nodes.ensureNode(atom);
|
|
134
|
-
releases.push(node.preserveInitialValueOnBuild
|
|
135
|
-
? holdNode(nodes, node)
|
|
136
|
-
: registry.mount(atom));
|
|
137
284
|
return atom;
|
|
138
285
|
};
|
|
139
286
|
let mounted = mount(getAtom());
|
|
@@ -142,15 +289,21 @@ const subscribedReader = (registry, getAtom) => {
|
|
|
142
289
|
if (atom !== mounted) {
|
|
143
290
|
mounted = mount(atom);
|
|
144
291
|
}
|
|
145
|
-
if (
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
292
|
+
if (hasServerValue(atom)) {
|
|
293
|
+
return Atom.getServerValue(atom, registry);
|
|
294
|
+
}
|
|
295
|
+
// An initial value is what the browser starts from too.
|
|
296
|
+
if (initialOnServer(registry, atom)) {
|
|
297
|
+
return serverGet(registry, atom);
|
|
150
298
|
}
|
|
151
|
-
|
|
299
|
+
const value = registry.get(atom);
|
|
300
|
+
if (plainRead) {
|
|
301
|
+
warnUnsent(registry, atom, value, plainRead);
|
|
302
|
+
}
|
|
303
|
+
return value;
|
|
152
304
|
};
|
|
153
305
|
}
|
|
306
|
+
reportReads(registry, getAtom, "read");
|
|
154
307
|
// The atom is picked on every read, not held in a $derived. In async mode Svelte renders a batch
|
|
155
308
|
// with other pending batches' changes rolled back, deriveds included, but registry reads always
|
|
156
309
|
// see the latest state; a derived atom could then pair an old atom with new state, and that
|
|
@@ -168,23 +321,30 @@ const subscribedReader = (registry, getAtom) => {
|
|
|
168
321
|
kept?.cancel();
|
|
169
322
|
kept = undefined;
|
|
170
323
|
};
|
|
171
|
-
// Whether the component has mounted: its effects have run once.
|
|
172
|
-
let mounted = false;
|
|
173
324
|
// The atom this reader's own registry.get is reading. A subscribed node is rebuilt as soon as it
|
|
174
325
|
// goes stale, so the read builds it only the first time, and that build announces the very value
|
|
175
|
-
// the read returns
|
|
176
|
-
//
|
|
177
|
-
//
|
|
178
|
-
//
|
|
179
|
-
//
|
|
180
|
-
//
|
|
181
|
-
//
|
|
326
|
+
// the read returns, so it is dropped: it could only re-render a read that already has the value.
|
|
327
|
+
// Before mount, delivered on a microtask it started a Svelte batch that, when the first render ran
|
|
328
|
+
// after an await in markup (a HydrationBoundary awaiting its state), committed before hydration's
|
|
329
|
+
// own and made Svelte's dev build throw "Batch has scheduled effects" (JND-95). After mount, a
|
|
330
|
+
// getter switched to an atom not built yet ran a transform twice, made a new object for an
|
|
331
|
+
// unchanged value, and re-ran an $effect that first read the atom. Only the build's own
|
|
332
|
+
// announcement is dropped: a value set during a registry batch is announced at commit, after
|
|
333
|
+
// `reading` is reset, and later changes arrive as usual. A getter switched in onMount once
|
|
334
|
+
// depended on that update, by accident (JND-98, fixed in the effect below). After mount, only a
|
|
335
|
+
// first build is dropped: a stale node the read rebuilds, as after a failed rebuild, announces a
|
|
336
|
+
// value the reader's other reads haven't seen.
|
|
337
|
+
let mounted = false;
|
|
182
338
|
let reading;
|
|
183
|
-
const listen = (current, update) =>
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
339
|
+
const listen = (current, update) => {
|
|
340
|
+
const unsubscribe = registry.subscribe(current, () => {
|
|
341
|
+
if (reading !== current) {
|
|
342
|
+
update();
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
dropInitialValueMark(registry, current);
|
|
346
|
+
return unsubscribe;
|
|
347
|
+
};
|
|
188
348
|
const follow = (current) => {
|
|
189
349
|
if (kept?.atom === current) {
|
|
190
350
|
const { cancel: keptCancel } = kept;
|
|
@@ -211,8 +371,10 @@ const subscribedReader = (registry, getAtom) => {
|
|
|
211
371
|
if (atom !== undefined && atom !== committed) {
|
|
212
372
|
const previous = atom;
|
|
213
373
|
const previousCancel = cancel;
|
|
374
|
+
// Subscribing can throw, as it rebuilds a stale atom; nothing moves until it has succeeded.
|
|
375
|
+
const next = follow(committed);
|
|
214
376
|
atom = committed;
|
|
215
|
-
cancel =
|
|
377
|
+
cancel = next;
|
|
216
378
|
if (previousCancel) {
|
|
217
379
|
releaseKept();
|
|
218
380
|
kept = { atom: previous, cancel: previousCancel };
|
|
@@ -234,8 +396,10 @@ const subscribedReader = (registry, getAtom) => {
|
|
|
234
396
|
if (current !== atom) {
|
|
235
397
|
const previous = atom;
|
|
236
398
|
const previousCancel = cancel;
|
|
399
|
+
// As in the effect: a throwing subscribe leaves the reader on its old atom, to try again.
|
|
400
|
+
const next = follow(current);
|
|
237
401
|
atom = current;
|
|
238
|
-
cancel =
|
|
402
|
+
cancel = next;
|
|
239
403
|
if (previous === committed &&
|
|
240
404
|
previous !== undefined &&
|
|
241
405
|
previousCancel) {
|
|
@@ -248,8 +412,17 @@ const subscribedReader = (registry, getAtom) => {
|
|
|
248
412
|
}
|
|
249
413
|
subscribe();
|
|
250
414
|
const outer = reading;
|
|
251
|
-
|
|
415
|
+
if (!mounted) {
|
|
416
|
+
reading = current;
|
|
417
|
+
}
|
|
252
418
|
try {
|
|
419
|
+
// After mount, only a first build is dropped: a stale node rebuilt here, as after a failed
|
|
420
|
+
// rebuild, announces a change other reads of this reader have not seen. A node whose first
|
|
421
|
+
// build failed is still uninitialized, but the page has shown that failure, so it counts too.
|
|
422
|
+
const node = internals(registry).ensureNode(current);
|
|
423
|
+
if (node.currentState() === "uninitialized" && !node.failed) {
|
|
424
|
+
reading = current;
|
|
425
|
+
}
|
|
253
426
|
return registry.get(current);
|
|
254
427
|
}
|
|
255
428
|
finally {
|
|
@@ -258,7 +431,7 @@ const subscribedReader = (registry, getAtom) => {
|
|
|
258
431
|
});
|
|
259
432
|
};
|
|
260
433
|
export function useAtomValue(input, f) {
|
|
261
|
-
const read = subscribedReader(getRegistry(), toGetter(input));
|
|
434
|
+
const read = subscribedReader(getRegistry(), toGetter(input), "useAtomValue");
|
|
262
435
|
if (!f) {
|
|
263
436
|
return new AtomCell(read, readOnly);
|
|
264
437
|
}
|
|
@@ -288,7 +461,19 @@ export function useAtomValue(input, f) {
|
|
|
288
461
|
export const useAtom = (input) => {
|
|
289
462
|
const registry = getRegistry();
|
|
290
463
|
const getAtom = toGetter(input);
|
|
291
|
-
return new AtomCell(subscribedReader(registry, getAtom), (value) => registry.set(getAtom(), value));
|
|
464
|
+
return new AtomCell(subscribedReader(registry, getAtom, "useAtom"), (value) => registry.set(getAtom(), value));
|
|
465
|
+
};
|
|
466
|
+
/** useAtomMount, telling an inspector scope why the atom is held: `useAtomSet` holds it to write. */
|
|
467
|
+
const mountWhileAlive = (input, kind) => {
|
|
468
|
+
const registry = getRegistry();
|
|
469
|
+
const getAtom = toGetter(input);
|
|
470
|
+
reportReads(registry, getAtom, kind);
|
|
471
|
+
$effect(() => {
|
|
472
|
+
const atom = getAtom();
|
|
473
|
+
const release = registry.mount(atom);
|
|
474
|
+
dropInitialValueMark(registry, atom);
|
|
475
|
+
return release;
|
|
476
|
+
});
|
|
292
477
|
};
|
|
293
478
|
/**
|
|
294
479
|
* Keeps an atom alive for as long as this component is mounted, even when no component reads
|
|
@@ -310,14 +495,12 @@ export const useAtom = (input) => {
|
|
|
310
495
|
* @category hooks
|
|
311
496
|
*/
|
|
312
497
|
export const useAtomMount = (input) => {
|
|
313
|
-
|
|
314
|
-
const getAtom = toGetter(input);
|
|
315
|
-
$effect(() => registry.mount(getAtom()));
|
|
498
|
+
mountWhileAlive(input, "mount");
|
|
316
499
|
};
|
|
317
500
|
export function useAtomSet(input, options) {
|
|
318
501
|
const registry = getRegistry();
|
|
319
502
|
const getAtom = toGetter(input);
|
|
320
|
-
|
|
503
|
+
mountWhileAlive(getAtom, "write");
|
|
321
504
|
const mode = options?.mode ?? "value";
|
|
322
505
|
if (mode === "value") {
|
|
323
506
|
return (value) => {
|
|
@@ -342,8 +525,21 @@ export function useAtomSet(input, options) {
|
|
|
342
525
|
exit = Exit.interrupt();
|
|
343
526
|
}
|
|
344
527
|
else {
|
|
528
|
+
const start = Date.now();
|
|
345
529
|
registry.set(atom, value);
|
|
346
|
-
|
|
530
|
+
// A provider disposing of its registry ends the call without a last result, so the wait ends
|
|
531
|
+
// there too, as interrupted.
|
|
532
|
+
const controller = new AbortController();
|
|
533
|
+
const abort = () => controller.abort();
|
|
534
|
+
writeOptions?.signal?.addEventListener("abort", abort, { once: true });
|
|
535
|
+
const cancel = onDispose(registry, abort);
|
|
536
|
+
try {
|
|
537
|
+
exit = await awaitWrite(registry, atom, start, controller.signal);
|
|
538
|
+
}
|
|
539
|
+
finally {
|
|
540
|
+
cancel();
|
|
541
|
+
writeOptions?.signal?.removeEventListener("abort", abort);
|
|
542
|
+
}
|
|
347
543
|
}
|
|
348
544
|
return mode === "promiseExit" ? exit : valueOrThrow(exit);
|
|
349
545
|
};
|
|
@@ -442,8 +638,12 @@ const orderedDelivery = (f) => {
|
|
|
442
638
|
export const useAtomSubscribe = (input, f, options) => {
|
|
443
639
|
const registry = getRegistry();
|
|
444
640
|
const getAtom = toGetter(input);
|
|
641
|
+
reportReads(registry, getAtom, "subscribe");
|
|
642
|
+
// The effect follows the atom, not what the getter reads: a getter that runs again and returns
|
|
643
|
+
// the same atom must not call `immediate` again, nor drop a change still waiting for delivery.
|
|
644
|
+
const current = $derived(getAtom());
|
|
445
645
|
$effect(() => {
|
|
446
|
-
const atom =
|
|
646
|
+
const atom = current;
|
|
447
647
|
// `immediate` calls `f` now, inside this effect; what `f` reads must not re-run it.
|
|
448
648
|
return untrack(() => {
|
|
449
649
|
// registry.subscribe does not compute a node that has never been read, so an atom nothing
|
|
@@ -455,6 +655,7 @@ export const useAtomSubscribe = (input, f, options) => {
|
|
|
455
655
|
}
|
|
456
656
|
const deliver = orderedDelivery(f);
|
|
457
657
|
const cancel = registry.subscribe(atom, deliver);
|
|
658
|
+
dropInitialValueMark(registry, atom);
|
|
458
659
|
return () => {
|
|
459
660
|
deliver.stop();
|
|
460
661
|
cancel();
|
|
@@ -468,10 +669,14 @@ export const useAtomSubscribe = (input, f, options) => {
|
|
|
468
669
|
*/
|
|
469
670
|
const setNodeValue = (registry, atom, value) => {
|
|
470
671
|
internals(registry).ensureNode(atom).setValue(value);
|
|
672
|
+
dropInitialValueMark(registry, atom);
|
|
471
673
|
};
|
|
472
674
|
// The nodes given a value by useAtomInitialValues. A node belongs to one registry, and a node the
|
|
473
675
|
// registry sweeps is gone from here too, so the next component to start it gives it the value again.
|
|
474
676
|
const initialValuesApplied = new WeakSet();
|
|
677
|
+
// On the server, how many renders hold each applied node. The value applies again only once none
|
|
678
|
+
// does, so a request ending doesn't let a request starting overwrite the value another still renders.
|
|
679
|
+
const serverHolders = new WeakMap();
|
|
475
680
|
/**
|
|
476
681
|
* Sets starting values as `AtomRegistry.make({ initialValues })` would: on the atom that receives it
|
|
477
682
|
* (a wrapper such as `withRefresh` passes it to its source), kept as the value of the atom's first
|
|
@@ -481,9 +686,10 @@ const initialValuesApplied = new WeakSet();
|
|
|
481
686
|
* an atom that can only compute in the browser can start from a value on the server.
|
|
482
687
|
*
|
|
483
688
|
* A value applies once while the atom is held, so a component mounted later, or a second
|
|
484
|
-
* component with its own value, does not overwrite a value the atom has moved on from
|
|
485
|
-
*
|
|
486
|
-
*
|
|
689
|
+
* component with its own value, does not overwrite a value the atom has moved on from since the
|
|
690
|
+
* first applied one. An atom that is already in use without one, read by another component, is set
|
|
691
|
+
* to the value, as `@effect/atom-react` does. Once nothing holds the atom and the registry disposes
|
|
692
|
+
* it, the next component to start it applies its value again. On the server a value lasts one render, so each request against a shared registry applies
|
|
487
693
|
* its own; requests rendering at the same time share the registry's atoms, and with them the value
|
|
488
694
|
* the first one applied.
|
|
489
695
|
*
|
|
@@ -507,51 +713,46 @@ const initialValuesApplied = new WeakSet();
|
|
|
507
713
|
export const useAtomInitialValues = (initialValues) => {
|
|
508
714
|
const registry = internals(getRegistry());
|
|
509
715
|
const releases = [];
|
|
716
|
+
// Registered before the loop: an entry that throws midway leaves the earlier ones held otherwise.
|
|
717
|
+
onTeardown(() => {
|
|
718
|
+
for (const release of releases) {
|
|
719
|
+
release();
|
|
720
|
+
}
|
|
721
|
+
});
|
|
510
722
|
for (const [atom, value] of initialValues) {
|
|
511
723
|
const node = registry.ensureNode(initialValueTarget(atom));
|
|
512
724
|
if (!initialValuesApplied.has(node)) {
|
|
513
725
|
initialValuesApplied.add(node);
|
|
514
726
|
node.setInitialValue(value);
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
727
|
+
}
|
|
728
|
+
if (!BROWSER) {
|
|
729
|
+
// A registry passed in by the caller outlives the request, and its node may not be swept
|
|
730
|
+
// before the next request starts, whose value must apply too (JND-17). Requests rendering at
|
|
731
|
+
// the same time share the value the first applied, until the last of them ends.
|
|
732
|
+
serverHolders.set(node, (serverHolders.get(node) ?? 0) + 1);
|
|
733
|
+
releases.push(() => {
|
|
734
|
+
const holders = (serverHolders.get(node) ?? 1) - 1;
|
|
735
|
+
serverHolders.set(node, holders);
|
|
736
|
+
if (holders === 0) {
|
|
737
|
+
initialValuesApplied.delete(node);
|
|
738
|
+
}
|
|
739
|
+
});
|
|
520
740
|
}
|
|
521
741
|
releases.push(holdNode(registry, node));
|
|
522
742
|
}
|
|
523
|
-
onTeardown(() => {
|
|
524
|
-
for (const release of releases) {
|
|
525
|
-
release();
|
|
526
|
-
}
|
|
527
|
-
});
|
|
528
743
|
};
|
|
529
|
-
/**
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
*
|
|
539
|
-
* ```ts
|
|
540
|
-
* import { useAtomRef } from "effect-atom-svelte";
|
|
541
|
-
* import { profile } from "./refs.ts";
|
|
542
|
-
*
|
|
543
|
-
* const current = useAtomRef(profile);
|
|
544
|
-
* // In the markup: {current.current.name}
|
|
545
|
-
* ```
|
|
546
|
-
*
|
|
547
|
-
* @stability unstable
|
|
548
|
-
* @since 0.1.0
|
|
549
|
-
* @category hooks
|
|
550
|
-
*/
|
|
551
|
-
export const useAtomRef = (input) => {
|
|
744
|
+
/** Writes a ref that has `set`; a read-only ref, as from `map`, throws as `useAtomValue` would. */
|
|
745
|
+
const writeRef = (getRef) => (value) => {
|
|
746
|
+
const ref = getRef();
|
|
747
|
+
if (!("set" in ref)) {
|
|
748
|
+
readOnly();
|
|
749
|
+
}
|
|
750
|
+
ref.set(value);
|
|
751
|
+
};
|
|
752
|
+
export function useAtomRef(input) {
|
|
552
753
|
const getRef = toGetter(input);
|
|
553
754
|
if (!BROWSER) {
|
|
554
|
-
return new AtomCell(() => getRef().value,
|
|
755
|
+
return new AtomCell(() => getRef().value, writeRef(getRef));
|
|
555
756
|
}
|
|
556
757
|
const ref = $derived(getRef());
|
|
557
758
|
const subscribe = $derived.by(() => {
|
|
@@ -561,8 +762,8 @@ export const useAtomRef = (input) => {
|
|
|
561
762
|
return new AtomCell(() => duringRead(() => {
|
|
562
763
|
subscribe();
|
|
563
764
|
return ref.value;
|
|
564
|
-
}),
|
|
565
|
-
}
|
|
765
|
+
}), writeRef(() => ref));
|
|
766
|
+
}
|
|
566
767
|
/**
|
|
567
768
|
* Returns `ref.prop(prop)`, the `AtomRef` for one property of an `AtomRef`. It takes the ref
|
|
568
769
|
* itself, not a getter, and does not follow later changes to `ref` or `prop`; to read a property
|
|
@@ -585,7 +786,8 @@ export const useAtomRef = (input) => {
|
|
|
585
786
|
export const useAtomRefProp = (ref, prop) => ref.prop(prop);
|
|
586
787
|
/**
|
|
587
788
|
* Reads one property of an `AtomRef`, following the getter to a different ref. `prop` is taken
|
|
588
|
-
* once, when the component starts, and is not followed.
|
|
789
|
+
* once, when the component starts, and is not followed. Assigning `current` sets the property, so
|
|
790
|
+
* `bind:checked={done.current}` works.
|
|
589
791
|
*
|
|
590
792
|
* **Example** (Reading one property of a ref)
|
|
591
793
|
*
|
|
@@ -606,11 +808,6 @@ export const useAtomRefPropValue = (input, prop) => {
|
|
|
606
808
|
const propRef = $derived(getRef().prop(prop));
|
|
607
809
|
return useAtomRef(() => propRef);
|
|
608
810
|
};
|
|
609
|
-
/**
|
|
610
|
-
* Whether a result is `Initial` with nothing running to settle it, as an `Atom.fn` nothing has
|
|
611
|
-
* called. On the server nothing will start it during the render, so waiting would hang it.
|
|
612
|
-
*/
|
|
613
|
-
const notStarted = (result) => result._tag === "Initial" && !result.waiting;
|
|
614
811
|
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.`);
|
|
615
812
|
/**
|
|
616
813
|
* Puts the server's value in the registry as current, so nothing fetches it again (JND-19). Seeding
|
|
@@ -631,15 +828,25 @@ const applySeed = (registry, atom, value, revalidate) => {
|
|
|
631
828
|
const seeds = new WeakMap();
|
|
632
829
|
// hydratable returns one promise per key per render, so it identifies the atom that claimed a key.
|
|
633
830
|
const serverSeeds = new WeakMap();
|
|
831
|
+
// The keys, by their render's promise, that some reader reads with suspendOnWaiting: the seed is
|
|
832
|
+
// then the settled result that reader renders, whichever reader claimed the key first.
|
|
833
|
+
const settledSeeds = new WeakSet();
|
|
634
834
|
/**
|
|
635
835
|
* On the server each request seeds for itself: `hydratable` already shares one result per key
|
|
636
836
|
* within a render, and a per-registry record would outlive the request when the caller owns the
|
|
637
837
|
* registry, so later requests would skip embedding their seed (JND-17).
|
|
638
838
|
*/
|
|
639
|
-
const seedOnServer = (registry, atom, key, encode) => {
|
|
839
|
+
const seedOnServer = (registry, atom, key, encode, suspendOnWaiting) => {
|
|
640
840
|
// Mounted until the render ends, so the settled node is what the render reads.
|
|
641
|
-
const release = registry
|
|
642
|
-
|
|
841
|
+
const release = serverMount(registry, atom);
|
|
842
|
+
// A render ends before its seeds settle only when it fails, as when a later reader throws during
|
|
843
|
+
// setup. The provider has disposed of the registry by then, so the seed reads nothing from it.
|
|
844
|
+
let ended = false;
|
|
845
|
+
onRenderEnd(() => {
|
|
846
|
+
ended = true;
|
|
847
|
+
release();
|
|
848
|
+
});
|
|
849
|
+
countSeeded(registry, key);
|
|
643
850
|
// hydratable hands every later reader of a key the first reader's value, but Svelte's dev build
|
|
644
851
|
// also runs each later reader's callback and throws hydratable_clobbering if what it encodes
|
|
645
852
|
// differs. Reading the atom again there would encode whatever it holds by then, so a later
|
|
@@ -649,21 +856,38 @@ const seedOnServer = (registry, atom, key, encode) => {
|
|
|
649
856
|
const encoded = hydratable(key, () => {
|
|
650
857
|
claim.own = (async () => {
|
|
651
858
|
await undefined;
|
|
859
|
+
if (ended) {
|
|
860
|
+
return noSeed;
|
|
861
|
+
}
|
|
652
862
|
if (claim.first !== claim.own) {
|
|
653
863
|
return await claim.first;
|
|
654
864
|
}
|
|
655
|
-
// Not waited for if nothing has started it: the hook rejects once seeded.
|
|
656
|
-
|
|
657
|
-
|
|
865
|
+
// Not waited for if nothing has started it: the hook rejects once seeded. Waited for as the hook
|
|
866
|
+
// waits, so with suspendOnWaiting the seed is the settled result the render shows, not one the
|
|
867
|
+
// browser would run again because it was still waiting.
|
|
868
|
+
if (!initialOnServer(registry, atom) && !notStarted(registry.get(atom))) {
|
|
869
|
+
let settled = suspendOnWaiting === true;
|
|
870
|
+
await awaitResult(registry, atom, { suspendOnWaiting: settled });
|
|
871
|
+
// A later reader of the key may want the settled result once this one's wait has begun.
|
|
872
|
+
if (!settled && claim.first && settledSeeds.has(claim.first)) {
|
|
873
|
+
settled = true;
|
|
874
|
+
await awaitResult(registry, atom, { suspendOnWaiting: settled });
|
|
875
|
+
}
|
|
658
876
|
}
|
|
659
|
-
|
|
877
|
+
if (ended) {
|
|
878
|
+
return noSeed;
|
|
879
|
+
}
|
|
880
|
+
return encodeSeed(key, encode, serverGet(registry, atom));
|
|
660
881
|
})();
|
|
661
882
|
return claim.own;
|
|
662
883
|
});
|
|
663
884
|
claim.first = encoded;
|
|
885
|
+
if (suspendOnWaiting === true) {
|
|
886
|
+
settledSeeds.add(encoded);
|
|
887
|
+
}
|
|
664
888
|
const claimed = serverSeeds.get(encoded);
|
|
665
889
|
if (claimed && claimed !== atom) {
|
|
666
|
-
throw new Error(`Two different atoms share the serialization key "${key}"`);
|
|
890
|
+
throw new Error(`Two different atoms share the serialization key "${key}", so the server can't tell whose result to send. Put what tells the copies apart into the key, such as an id, and define atoms at module level. ${troubleshooting("two-different-atoms-share-the-serialization-key")}`);
|
|
667
891
|
}
|
|
668
892
|
serverSeeds.set(encoded, atom);
|
|
669
893
|
return (async () => {
|
|
@@ -686,7 +910,10 @@ const serverValues = () => {
|
|
|
686
910
|
};
|
|
687
911
|
const warnIfSent = async (key, sent) => {
|
|
688
912
|
try {
|
|
689
|
-
|
|
913
|
+
// Undefined when the key wasn't sent, null when the server sent that it had no seed for it, as
|
|
914
|
+
// for a defect: either way the browser was always going to compute the atom.
|
|
915
|
+
const value = await sent;
|
|
916
|
+
if (value === undefined || value === null) {
|
|
690
917
|
return;
|
|
691
918
|
}
|
|
692
919
|
}
|
|
@@ -730,7 +957,7 @@ const claimServerValue = (key, missed) => {
|
|
|
730
957
|
* serialization key. The atom is fetched again once seeded if any component using the key then
|
|
731
958
|
* asked to revalidate.
|
|
732
959
|
*/
|
|
733
|
-
const seedFromServer = (registry, getAtom, revalidateOption) => {
|
|
960
|
+
const seedFromServer = (registry, getAtom, revalidateOption, suspendOnWaiting) => {
|
|
734
961
|
const atom = getAtom();
|
|
735
962
|
// The server never computes an atom with a server value, so it has no value to pass on, and the
|
|
736
963
|
// browser must not ask for one: hydratable throws for a key the server didn't write (JND-58).
|
|
@@ -740,7 +967,7 @@ const seedFromServer = (registry, getAtom, revalidateOption) => {
|
|
|
740
967
|
const { decode, encode, key } = atom[Atom.SerializableTypeId];
|
|
741
968
|
if (!BROWSER) {
|
|
742
969
|
return {
|
|
743
|
-
done: seedOnServer(registry, atom, key, encode),
|
|
970
|
+
done: seedOnServer(registry, atom, key, encode, suspendOnWaiting),
|
|
744
971
|
letGo: noSeedMount,
|
|
745
972
|
};
|
|
746
973
|
}
|
|
@@ -751,10 +978,11 @@ const seedFromServer = (registry, getAtom, revalidateOption) => {
|
|
|
751
978
|
seeds.set(registry, registrySeeds);
|
|
752
979
|
}
|
|
753
980
|
const { held, spent } = registrySeeds;
|
|
981
|
+
// The server's value belongs to the key, not to an atom: the registry keeps a serializable atom's
|
|
982
|
+
// value under its key, so every atom with the key reads it, as HydrationBoundary assumes too.
|
|
983
|
+
// A remounted branch, as under {#key}, makes a new atom with the old one's key while the old one
|
|
984
|
+
// still holds it, until Svelte destroys the old branch or its outro ends: the new atom joins it.
|
|
754
985
|
let entry = held.get(key);
|
|
755
|
-
if (entry && entry.atom !== atom) {
|
|
756
|
-
throw new Error(`Two different atoms share the serialization key "${key}"`);
|
|
757
|
-
}
|
|
758
986
|
if (!entry && spent.has(key)) {
|
|
759
987
|
return undefined;
|
|
760
988
|
}
|
|
@@ -772,7 +1000,6 @@ const seedFromServer = (registry, getAtom, revalidateOption) => {
|
|
|
772
1000
|
let holders = 0;
|
|
773
1001
|
let revalidating = 0;
|
|
774
1002
|
entry = {
|
|
775
|
-
atom,
|
|
776
1003
|
done: (async () => {
|
|
777
1004
|
const value = await encoded;
|
|
778
1005
|
// Only the server's value is a seed, and only while someone is there to read it now: a seed
|
|
@@ -780,7 +1007,9 @@ const seedFromServer = (registry, getAtom, revalidateOption) => {
|
|
|
780
1007
|
// The server sends no seed for a result it can't or mustn't pass on, such as a defect: the
|
|
781
1008
|
// atom computes here instead. A seed still waiting, as a stream's between its values, runs
|
|
782
1009
|
// again too: the server's run ended with the render, so it would wait forever.
|
|
783
|
-
const seed = !computedHere && holders > 0
|
|
1010
|
+
const seed = !computedHere && holders > 0
|
|
1011
|
+
? decodeSeed(key, value, decode)
|
|
1012
|
+
: undefined;
|
|
784
1013
|
if (seed !== undefined) {
|
|
785
1014
|
applySeed(registry, atom, seed, revalidating > 0 || isWaiting(seed));
|
|
786
1015
|
}
|
|
@@ -870,7 +1099,7 @@ const seedFromServer = (registry, getAtom, revalidateOption) => {
|
|
|
870
1099
|
export const useAtomResult = async (input, options) => {
|
|
871
1100
|
const registry = getRegistry();
|
|
872
1101
|
const getAtom = toGetter(input);
|
|
873
|
-
const value =
|
|
1102
|
+
const value = new AtomCell(subscribedReader(registry, getAtom), readOnly);
|
|
874
1103
|
const atom = getAtom();
|
|
875
1104
|
let release;
|
|
876
1105
|
// Aborted when the component is destroyed, which interrupts the wait below so the atom is not held.
|
|
@@ -889,7 +1118,7 @@ export const useAtomResult = async (input, options) => {
|
|
|
889
1118
|
release = undefined;
|
|
890
1119
|
return unmount;
|
|
891
1120
|
});
|
|
892
|
-
const seed = seedFromServer(registry, getAtom, options?.revalidateOnHydrate);
|
|
1121
|
+
const seed = seedFromServer(registry, getAtom, options?.revalidateOnHydrate, options?.suspendOnWaiting);
|
|
893
1122
|
if (seed) {
|
|
894
1123
|
await seed.done;
|
|
895
1124
|
}
|
|
@@ -898,6 +1127,11 @@ export const useAtomResult = async (input, options) => {
|
|
|
898
1127
|
if (lifetime.signal.aborted || (!BROWSER && hasServerValue(atom))) {
|
|
899
1128
|
return value;
|
|
900
1129
|
}
|
|
1130
|
+
// On the server, an initial value is what the render shows, with nothing to wait for.
|
|
1131
|
+
if (initialOnServer(registry, atom)) {
|
|
1132
|
+
release = serverMount(registry, atom);
|
|
1133
|
+
return value;
|
|
1134
|
+
}
|
|
901
1135
|
if (effectHolds) {
|
|
902
1136
|
// Mounted while seeding, as when the promise is not awaited straight away: the effect holds the
|
|
903
1137
|
// getter's atom, and a mount here would never be released. Once the getter has switched away,
|
|
@@ -938,7 +1172,7 @@ const fromSettled = (current, options) => {
|
|
|
938
1172
|
*/
|
|
939
1173
|
const fromServerValue = (current, options) => {
|
|
940
1174
|
if (isPending(current, options)) {
|
|
941
|
-
return Promise.reject(new Error(
|
|
1175
|
+
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. ${troubleshooting("useatomsuspense-read-an-atom-whose-server-value-is-pending")}`));
|
|
942
1176
|
}
|
|
943
1177
|
try {
|
|
944
1178
|
return Promise.resolve(fromSettled(current, options));
|
|
@@ -952,17 +1186,16 @@ const suspend = async (registry, atom, current, options, signal) => {
|
|
|
952
1186
|
if (!BROWSER && notStarted(current)) {
|
|
953
1187
|
throw notStartedError("useAtomSuspense");
|
|
954
1188
|
}
|
|
955
|
-
const exit = await
|
|
1189
|
+
const exit = await awaitSettled(registry, atom, { suspendOnWaiting: options.suspendOnWaiting }, signal);
|
|
956
1190
|
// Abandoned: rejects with the reason its last reader's signal was aborted with. For a re-run
|
|
957
1191
|
// that is Svelte's STALE_REACTION, which it ignores, so a pending render that still awaits this
|
|
958
1192
|
// wait keeps waiting for its next run instead of showing the interruption (JND-22).
|
|
959
1193
|
signal.throwIfAborted();
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
: AsyncResult.failure(exit.cause);
|
|
1194
|
+
// Only an abort, handled above, ends the wait without a result.
|
|
1195
|
+
if (Exit.isFailure(exit)) {
|
|
1196
|
+
throw Cause.squash(exit.cause);
|
|
964
1197
|
}
|
|
965
|
-
return
|
|
1198
|
+
return fromSettled(exit.value, options);
|
|
966
1199
|
}
|
|
967
1200
|
return fromSettled(current, options);
|
|
968
1201
|
};
|
|
@@ -1048,13 +1281,14 @@ const sharedWait = (start, onAbandoned) => {
|
|
|
1048
1281
|
listeners.push([signal, listener]);
|
|
1049
1282
|
},
|
|
1050
1283
|
promise,
|
|
1284
|
+
settled: () => settled,
|
|
1051
1285
|
};
|
|
1052
1286
|
};
|
|
1053
1287
|
export function useAtomSuspense(input, options = {}) {
|
|
1054
1288
|
const registry = getRegistry();
|
|
1055
1289
|
const getAtom = toGetter(input);
|
|
1056
|
-
const result =
|
|
1057
|
-
const seed = seedFromServer(registry, getAtom, options.revalidateOnHydrate);
|
|
1290
|
+
const result = new AtomCell(subscribedReader(registry, getAtom), readOnly);
|
|
1291
|
+
const seed = seedFromServer(registry, getAtom, options.revalidateOnHydrate, options.suspendOnWaiting);
|
|
1058
1292
|
// A plain variable, not $state: a render in a batch with this write rolled back would take the
|
|
1059
1293
|
// pre-seed path and track nothing but the flag. Reading `trackSeed` asks for a re-read instead.
|
|
1060
1294
|
let seeded = seed === undefined;
|
|
@@ -1083,10 +1317,48 @@ export function useAtomSuspense(input, options = {}) {
|
|
|
1083
1317
|
// Holds waits read outside any derived or effect (a top-level await in the script, an event
|
|
1084
1318
|
// handler), which cannot say when they are done with them, until the component is destroyed.
|
|
1085
1319
|
const lifetime = new AbortController();
|
|
1086
|
-
|
|
1320
|
+
// Each reaction's signal, also aborted when the component is destroyed. Svelte aborts a derived's
|
|
1321
|
+
// signal when it runs again or loses its last reader, and doesn't destroy deriveds with their
|
|
1322
|
+
// component, so a derived only the script or an event handler reads would never abort it, and
|
|
1323
|
+
// the wait it holds would keep the atom after the component is gone. Kept per signal, so a signal
|
|
1324
|
+
// still holds a wait once (see sharedWait); those still live are aborted at teardown.
|
|
1325
|
+
const bounded = new WeakMap();
|
|
1326
|
+
const liveAborts = new Set();
|
|
1327
|
+
onTeardown(() => {
|
|
1328
|
+
lifetime.abort();
|
|
1329
|
+
// Each abort deletes itself, which a Set's iteration allows.
|
|
1330
|
+
for (const abort of liveAborts) {
|
|
1331
|
+
abort();
|
|
1332
|
+
}
|
|
1333
|
+
});
|
|
1334
|
+
const untilDestroyed = (signal) => {
|
|
1335
|
+
let combined = bounded.get(signal);
|
|
1336
|
+
if (!combined) {
|
|
1337
|
+
const controller = new AbortController();
|
|
1338
|
+
combined = controller.signal;
|
|
1339
|
+
bounded.set(signal, combined);
|
|
1340
|
+
// The reaction's reason, such as Svelte's STALE_REACTION, which it ignores (JND-22).
|
|
1341
|
+
const reason = () => signal.aborted ? signal.reason : lifetime.signal.reason;
|
|
1342
|
+
if (signal.aborted || lifetime.signal.aborted) {
|
|
1343
|
+
controller.abort(reason());
|
|
1344
|
+
}
|
|
1345
|
+
else {
|
|
1346
|
+
const abort = () => {
|
|
1347
|
+
liveAborts.delete(abort);
|
|
1348
|
+
signal.removeEventListener("abort", abort);
|
|
1349
|
+
controller.abort(reason());
|
|
1350
|
+
};
|
|
1351
|
+
liveAborts.add(abort);
|
|
1352
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
return combined;
|
|
1356
|
+
};
|
|
1087
1357
|
// Keyed by atom, then result: atoms can share a result object, as a derived atom returning its
|
|
1088
1358
|
// source's result does, and a wait holds and waits for one atom.
|
|
1089
1359
|
const waits = new WeakMap();
|
|
1360
|
+
// Per atom, the wait the last read got, for a read after the component is destroyed.
|
|
1361
|
+
const lastWaits = new WeakMap();
|
|
1090
1362
|
const settle = (atom, current, signal) => {
|
|
1091
1363
|
let atomWaits = waits.get(atom);
|
|
1092
1364
|
if (!atomWaits) {
|
|
@@ -1100,6 +1372,7 @@ export function useAtomSuspense(input, options = {}) {
|
|
|
1100
1372
|
atomWaits.set(current, wait);
|
|
1101
1373
|
}
|
|
1102
1374
|
wait.hold(signal);
|
|
1375
|
+
lastWaits.set(atom, wait);
|
|
1103
1376
|
return wait.promise;
|
|
1104
1377
|
};
|
|
1105
1378
|
const fromServer = new WeakMap();
|
|
@@ -1114,6 +1387,27 @@ export function useAtomSuspense(input, options = {}) {
|
|
|
1114
1387
|
return promise;
|
|
1115
1388
|
}
|
|
1116
1389
|
const signal = readerSignal();
|
|
1390
|
+
const holder = signal ? untilDestroyed(signal) : lifetime.signal;
|
|
1391
|
+
if (holder.aborted) {
|
|
1392
|
+
// Read once the reader is gone, as in onDestroy or a handler that resumes after the component
|
|
1393
|
+
// is destroyed. A result it had settled is still there; a new wait would hold the atom until it
|
|
1394
|
+
// settles, as nothing could let go of it. Handled here, so a read nobody awaits isn't reported
|
|
1395
|
+
// as unhandled.
|
|
1396
|
+
const last = lastWaits.get(getAtom());
|
|
1397
|
+
if (last?.settled()) {
|
|
1398
|
+
return last.promise;
|
|
1399
|
+
}
|
|
1400
|
+
const gone = Promise.reject(holder.reason);
|
|
1401
|
+
void (async () => {
|
|
1402
|
+
try {
|
|
1403
|
+
await gone;
|
|
1404
|
+
}
|
|
1405
|
+
catch {
|
|
1406
|
+
// Belongs to whoever awaits it.
|
|
1407
|
+
}
|
|
1408
|
+
})();
|
|
1409
|
+
return gone;
|
|
1410
|
+
}
|
|
1117
1411
|
if (!seeded && seed) {
|
|
1118
1412
|
// Reading the atom before the seed lands would fetch what hydration is about to provide.
|
|
1119
1413
|
const atom = getAtom();
|
|
@@ -1130,9 +1424,15 @@ export function useAtomSuspense(input, options = {}) {
|
|
|
1130
1424
|
if (live.length === 0) {
|
|
1131
1425
|
return undefined;
|
|
1132
1426
|
}
|
|
1133
|
-
|
|
1427
|
+
// A read outside any reaction can't say when it is done, so it doesn't hold the atom: held
|
|
1428
|
+
// by the component's lifetime, the atom would stay mounted after the getter moves on.
|
|
1429
|
+
const reactions = live.filter((reader) => reader !== lifetime.signal);
|
|
1430
|
+
if (reactions.length > 0) {
|
|
1431
|
+
holdWhileRead(registry, atom, reactions);
|
|
1432
|
+
}
|
|
1134
1433
|
seed.letGo();
|
|
1135
|
-
|
|
1434
|
+
// On the server, an initial value is read without building the atom.
|
|
1435
|
+
const current = serverGet(registry, atom);
|
|
1136
1436
|
let waited;
|
|
1137
1437
|
for (const reader of live) {
|
|
1138
1438
|
waited = settle(atom, current, reader);
|
|
@@ -1142,10 +1442,10 @@ export function useAtomSuspense(input, options = {}) {
|
|
|
1142
1442
|
pending = { promise, readers };
|
|
1143
1443
|
afterSeed.set(atom, pending);
|
|
1144
1444
|
}
|
|
1145
|
-
pending.readers.add(
|
|
1445
|
+
pending.readers.add(holder);
|
|
1146
1446
|
return pending.promise;
|
|
1147
1447
|
}
|
|
1148
|
-
const promise = settle(getAtom(), result.current,
|
|
1448
|
+
const promise = settle(getAtom(), result.current, holder);
|
|
1149
1449
|
if (signal) {
|
|
1150
1450
|
// A reaction read it, so the reader's subscription holds the atom from here.
|
|
1151
1451
|
seed?.letGo();
|