effect-atom-svelte 0.2.0 → 0.4.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 +5 -2
- package/dist/Form.d.ts +605 -0
- package/dist/Form.js +67 -0
- package/dist/Hooks.svelte.d.ts +70 -10
- package/dist/Hooks.svelte.js +489 -163
- package/dist/HydrationBoundary.svelte +134 -17
- package/dist/HydrationBoundary.svelte.d.ts +13 -4
- package/dist/Inspector.d.ts +7 -3
- package/dist/Inspector.js +113 -29
- package/dist/RegistryContext.d.ts +2 -0
- package/dist/RegistryContext.js +13 -5
- package/dist/ScopedAtom.d.ts +5 -4
- package/dist/ScopedAtom.js +20 -11
- package/dist/SvelteKit.d.ts +71 -12
- package/dist/SvelteKit.js +133 -54
- package/dist/index.d.ts +10 -1
- package/dist/index.js +10 -1
- package/dist/internal/awaitWrite.d.ts +11 -0
- package/dist/internal/awaitWrite.js +27 -0
- package/dist/internal/boundaries.d.ts +9 -0
- package/dist/internal/boundaries.js +10 -0
- package/dist/internal/disposal.d.ts +13 -0
- package/dist/internal/disposal.js +43 -0
- package/dist/internal/form/array.d.ts +21 -0
- package/dist/internal/form/array.js +57 -0
- package/dist/internal/form/constraints.d.ts +13 -0
- package/dist/internal/form/constraints.js +166 -0
- package/dist/internal/form/engine.d.ts +56 -0
- package/dist/internal/form/engine.js +842 -0
- package/dist/internal/form/engineOf.d.ts +26 -0
- package/dist/internal/form/engineOf.js +43 -0
- package/dist/internal/form/field.d.ts +30 -0
- package/dist/internal/form/field.js +68 -0
- package/dist/internal/form/group.d.ts +13 -0
- package/dist/internal/form/group.js +20 -0
- package/dist/internal/form/instance.d.ts +41 -0
- package/dist/internal/form/instance.js +135 -0
- package/dist/internal/form/issues.d.ts +23 -0
- package/dist/internal/form/issues.js +144 -0
- package/dist/internal/form/paths.d.ts +47 -0
- package/dist/internal/form/paths.js +175 -0
- package/dist/internal/form/types.d.ts +80 -0
- package/dist/internal/form/types.js +1 -0
- package/dist/internal/form/validationError.d.ts +13 -0
- package/dist/internal/form/validationError.js +6 -0
- package/dist/internal/hydration.d.ts +11 -2
- package/dist/internal/hydration.js +26 -4
- package/dist/internal/nodeInternals.d.ts +6 -0
- package/dist/internal/nodeInternals.js +64 -7
- package/dist/internal/renderEnd.d.ts +13 -0
- package/dist/internal/renderEnd.js +34 -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 +12 -10
package/dist/Hooks.svelte.js
CHANGED
|
@@ -6,14 +6,20 @@
|
|
|
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 { awaitWrite } from "./internal/awaitWrite.js";
|
|
12
|
+
import { queuedByBoundary } from "./internal/boundaries.js";
|
|
13
|
+
import { onDispose } from "./internal/disposal.js";
|
|
14
|
+
import { engineOf, initialGetter } from "./internal/form/engineOf.js";
|
|
15
|
+
import { decodeSeed, encodeSeed, isWaiting, noSeed, revalidatesOnHydrate, } from "./internal/hydration.js";
|
|
16
|
+
import { onRenderEnd } from "./internal/renderEnd.js";
|
|
12
17
|
import { reportReads } from "./internal/scope.svelte.js";
|
|
18
|
+
import { onTeardown } from "./internal/teardown.svelte.js";
|
|
19
|
+
import { troubleshooting } from "./internal/troubleshooting.js";
|
|
13
20
|
import { getRegistry } from "./RegistryContext.js";
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
: () => input;
|
|
21
|
+
const isGetter = (input) => typeof input === "function" && !Atom.isAtom(input);
|
|
22
|
+
const toGetter = (input) => isGetter(input) ? input : () => input;
|
|
17
23
|
// The registry notifies subscribers synchronously while it computes an atom, and Svelte throws
|
|
18
24
|
// state_unsafe_mutation if state changes while a template or $derived is evaluating. Reads are
|
|
19
25
|
// counted, and a notification raised during one is delivered on a microtask instead. Outside a
|
|
@@ -28,7 +34,9 @@ const afterReads = (update) => {
|
|
|
28
34
|
update();
|
|
29
35
|
}
|
|
30
36
|
};
|
|
31
|
-
const notifyAfterReads = (update) => () =>
|
|
37
|
+
const notifyAfterReads = (update) => () => {
|
|
38
|
+
afterReads(update);
|
|
39
|
+
};
|
|
32
40
|
const duringRead = (f) => {
|
|
33
41
|
activeReads += 1;
|
|
34
42
|
try {
|
|
@@ -38,20 +46,6 @@ const duringRead = (f) => {
|
|
|
38
46
|
activeReads -= 1;
|
|
39
47
|
}
|
|
40
48
|
};
|
|
41
|
-
/**
|
|
42
|
-
* Runs `f` when the component is destroyed, even while its script is still awaiting. In the browser
|
|
43
|
-
* `onDestroy` only takes effect once the component mounts, so a component removed while pending
|
|
44
|
-
* would never call it. A pre effect runs during init and is torn down with the component (JND-16).
|
|
45
|
-
* On the server, `onDestroy` runs when the render ends.
|
|
46
|
-
*/
|
|
47
|
-
const onTeardown = (f) => {
|
|
48
|
-
if (BROWSER) {
|
|
49
|
-
$effect.pre(() => f);
|
|
50
|
-
}
|
|
51
|
-
else {
|
|
52
|
-
onDestroy(f);
|
|
53
|
-
}
|
|
54
|
-
};
|
|
55
49
|
/** Holds read and write closures; a class so `.current` is a real accessor `bind:` can use. */
|
|
56
50
|
class AtomCell {
|
|
57
51
|
#read;
|
|
@@ -60,6 +54,7 @@ class AtomCell {
|
|
|
60
54
|
this.#read = read;
|
|
61
55
|
this.#write = write;
|
|
62
56
|
}
|
|
57
|
+
// oxlint-disable-next-line typescript/related-getter-setter-pairs -- implements AtomState, whose read and write types differ
|
|
63
58
|
get current() {
|
|
64
59
|
return this.#read();
|
|
65
60
|
}
|
|
@@ -68,7 +63,7 @@ class AtomCell {
|
|
|
68
63
|
}
|
|
69
64
|
}
|
|
70
65
|
const readOnly = () => {
|
|
71
|
-
throw new Error(
|
|
66
|
+
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")}`);
|
|
72
67
|
};
|
|
73
68
|
const valueOrThrow = (exit) => {
|
|
74
69
|
if (Exit.isSuccess(exit)) {
|
|
@@ -76,14 +71,23 @@ const valueOrThrow = (exit) => {
|
|
|
76
71
|
}
|
|
77
72
|
throw Cause.squash(exit.cause);
|
|
78
73
|
};
|
|
74
|
+
/**
|
|
75
|
+
* Whether a result is `Initial` with nothing running to settle it, as an `Atom.fn` nothing has
|
|
76
|
+
* called. On the server nothing will start it during the render, so waiting would hang it.
|
|
77
|
+
*/
|
|
78
|
+
const notStarted = (result) => result._tag === "Initial" && !result.waiting;
|
|
79
79
|
/**
|
|
80
80
|
* Whether the atom has a `withServerValue` override. The server reads that instead and never
|
|
81
81
|
* computes the atom, as its real read is often browser-only.
|
|
82
82
|
*/
|
|
83
83
|
const hasServerValue = (atom) => Atom.ServerValueTypeId in atom;
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
84
|
+
/** The atom's node, created if the registry has none yet. */
|
|
85
|
+
const nodeOf = (registry, atom) =>
|
|
86
|
+
// SAFETY: ensureNode returns the implementation's node (Effect 4.0.5), which has
|
|
87
|
+
// preserveInitialValueOnBuild, _value, setValue and failed (set while a first build has thrown)
|
|
88
|
+
// beside the Node interface; @effect/atom-react calls its setValue the same way.
|
|
89
|
+
// oxlint-disable-next-line typescript/no-unsafe-type-assertion -- those members are on the implementation only, which Effect doesn't export
|
|
90
|
+
registry.ensureNode(atom);
|
|
87
91
|
/** The atom that holds a value given to `atom`: a wrapper such as `withRefresh` passes it on. */
|
|
88
92
|
const initialValueTarget = (atom) => {
|
|
89
93
|
let target = atom;
|
|
@@ -92,14 +96,16 @@ const initialValueTarget = (atom) => {
|
|
|
92
96
|
}
|
|
93
97
|
return target;
|
|
94
98
|
};
|
|
95
|
-
const noop = () => undefined;
|
|
96
99
|
/**
|
|
97
100
|
* Keeps an atom's node in the registry without computing it, as a mount would but without the read.
|
|
98
101
|
* The registry sweeps a node without listeners, so a listener holds it; letting go schedules the
|
|
99
|
-
* sweep, as an unsubscribe does.
|
|
102
|
+
* sweep, as an unsubscribe does. Each hold has its own listener: the node keeps its listeners in a
|
|
103
|
+
* set, so holds sharing one would all end with the first to let go.
|
|
100
104
|
*/
|
|
101
105
|
const holdNode = (registry, node) => {
|
|
102
|
-
const unsubscribe = node.subscribe(
|
|
106
|
+
const unsubscribe = node.subscribe(() => {
|
|
107
|
+
// Only there to hold the node.
|
|
108
|
+
});
|
|
103
109
|
return () => {
|
|
104
110
|
unsubscribe();
|
|
105
111
|
if (node.canBeRemoved) {
|
|
@@ -107,34 +113,157 @@ const holdNode = (registry, node) => {
|
|
|
107
113
|
}
|
|
108
114
|
};
|
|
109
115
|
};
|
|
116
|
+
/**
|
|
117
|
+
* Lets a node that has taken a value since it was given an initial one compute as usual again.
|
|
118
|
+
* Effect's `setValue` (4.0.1) leaves the mark that tells a node's next build to keep its initial
|
|
119
|
+
* value, so a node given a seed or a `HydrationBoundary` value over one would keep that through its
|
|
120
|
+
* next build, a refresh's, and drop the build's result. Call it once the node holds a value of its
|
|
121
|
+
* own: after `setValue`, or after subscribing, which builds a node still waiting for its first build
|
|
122
|
+
* (using the mark up) and, as a lookup, applies a value a `HydrationBoundary` queued. Only a valid
|
|
123
|
+
* node loses the mark: a stale one still keeps its initial value through its first build. The atoms
|
|
124
|
+
* a wrapper such as `withRefresh` passes an initial value to are cleared too, as subscribing to the
|
|
125
|
+
* wrapper builds them.
|
|
126
|
+
*/
|
|
127
|
+
const dropInitialValueMark = (registry, atom) => {
|
|
128
|
+
let current = atom;
|
|
129
|
+
while (current) {
|
|
130
|
+
const node = nodeOf(registry, current);
|
|
131
|
+
if (node.currentState() === "valid") {
|
|
132
|
+
node.preserveInitialValueOnBuild = false;
|
|
133
|
+
}
|
|
134
|
+
current = current.initialValueTarget;
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
/**
|
|
138
|
+
* On the server, the node holding a value from `useAtomInitialValues` for an atom: its own, or its
|
|
139
|
+
* source's for a wrapper such as `withRefresh`, which passes the value on and reads as its source.
|
|
140
|
+
* The server renders that value: mounting, reading or waiting for the atom would build it, and a
|
|
141
|
+
* build runs the atom's read, the source's included, even when it keeps the value. That read may be
|
|
142
|
+
* browser-only or start a request the value was there to save.
|
|
143
|
+
*
|
|
144
|
+
* For a wrapper, this assumes it reads as its source, as every wrapper Effect makes with
|
|
145
|
+
* `initialValueTarget` does. A user's `Atom.transform` given `initialValueTarget` that changes the
|
|
146
|
+
* value would render its source's value on the server, where Effect computes the transform.
|
|
147
|
+
*/
|
|
148
|
+
const initialNodeOnServer = (registry, atom) => {
|
|
149
|
+
if (BROWSER) {
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
const node = nodeOf(registry, initialValueTarget(atom));
|
|
153
|
+
return node.preserveInitialValueOnBuild ? node : undefined;
|
|
154
|
+
};
|
|
155
|
+
/** Whether, on the server, an atom renders a value from `useAtomInitialValues`. */
|
|
156
|
+
const initialOnServer = (registry, atom) => initialNodeOnServer(registry, atom) !== undefined;
|
|
157
|
+
/** Keeps an atom for the server render: a node with an initial value is held, not mounted. */
|
|
158
|
+
const serverMount = (registry, atom) => {
|
|
159
|
+
const node = initialNodeOnServer(registry, atom);
|
|
160
|
+
return node ? holdNode(registry, node) : registry.mount(atom);
|
|
161
|
+
};
|
|
162
|
+
/** Reads an atom without building it on the server while it renders an initial value. */
|
|
163
|
+
const serverGet = (registry, atom) => {
|
|
164
|
+
const node = initialNodeOnServer(registry, atom);
|
|
165
|
+
return node ? node._value : registry.get(atom);
|
|
166
|
+
};
|
|
110
167
|
const awaitResult = (registry, atom, options, signal) => Effect.runPromiseExit(AtomRegistry.getResult(registry, atom, options), {
|
|
111
168
|
signal,
|
|
112
169
|
});
|
|
113
|
-
|
|
170
|
+
/** Whether a hook stops waiting at a result: it has left `Initial`, and isn't refreshing if asked. */
|
|
171
|
+
const settles = (result, suspendOnWaiting) => result._tag !== "Initial" && !(suspendOnWaiting && result.waiting);
|
|
172
|
+
/**
|
|
173
|
+
* Waits for an async atom to settle, as `awaitResult` does, but resolves with the atom's own result
|
|
174
|
+
* rather than an `Exit` rebuilt from it, which would drop a `Failure`'s `previousSuccess`.
|
|
175
|
+
* Interrupted, with a failed `Exit`, once `signal` aborts.
|
|
176
|
+
*/
|
|
177
|
+
const awaitSettled = (registry, atom, options, signal) => {
|
|
178
|
+
const suspendOnWaiting = options.suspendOnWaiting ?? false;
|
|
179
|
+
return Effect.runPromiseExit(Effect.callback((resume) => {
|
|
180
|
+
const current = registry.get(atom);
|
|
181
|
+
if (settles(current, suspendOnWaiting)) {
|
|
182
|
+
resume(Effect.succeed(current));
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
const cancel = registry.subscribe(atom, (value) => {
|
|
186
|
+
if (settles(value, suspendOnWaiting)) {
|
|
187
|
+
resume(Effect.succeed(value));
|
|
188
|
+
cancel();
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
return Effect.sync(cancel);
|
|
192
|
+
}), { signal });
|
|
193
|
+
};
|
|
194
|
+
// On the server, per registry, how many renders' seeds carry each key, until those renders end.
|
|
195
|
+
const seededKeys = new WeakMap();
|
|
196
|
+
/** Whether a render using the registry sends the key's result to the browser with a seed. */
|
|
197
|
+
const seededOnServer = (registry, key) => (seededKeys.get(registry)?.get(key) ?? 0) > 0;
|
|
198
|
+
/** Counts the key as seeded until the render ends. */
|
|
199
|
+
const countSeeded = (registry, key) => {
|
|
200
|
+
let keys = seededKeys.get(registry);
|
|
201
|
+
if (!keys) {
|
|
202
|
+
keys = new Map();
|
|
203
|
+
seededKeys.set(registry, keys);
|
|
204
|
+
}
|
|
205
|
+
keys.set(key, (keys.get(key) ?? 0) + 1);
|
|
206
|
+
onRenderEnd(() => {
|
|
207
|
+
const count = (keys.get(key) ?? 1) - 1;
|
|
208
|
+
if (count > 0) {
|
|
209
|
+
keys.set(key, count);
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
keys.delete(key);
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
};
|
|
216
|
+
// The keys of the atoms useAtomValue has warned about, so the server warns once per key, not per
|
|
217
|
+
// request.
|
|
218
|
+
const warnedUnsent = new Set();
|
|
219
|
+
/**
|
|
220
|
+
* In development, warns once per key that a plain read on the server met a serializable async atom
|
|
221
|
+
* still running: only useAtomResult and useAtomSuspense send an atom's result to the browser, so
|
|
222
|
+
* the browser runs it again, and the page can show a loading state over what the server rendered.
|
|
223
|
+
* Not for a key the page sends anyway, with a seed or a HydrationBoundary's value, nor for a result
|
|
224
|
+
* nothing is computing, such as an Atom.fn nobody called or a value written to the atom.
|
|
225
|
+
*/
|
|
226
|
+
const warnUnsent = (registry, atom, value, hook) => {
|
|
227
|
+
if (!(DEV && Atom.isSerializable(atom) && AsyncResult.isAsyncResult(value)) ||
|
|
228
|
+
!value.waiting) {
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
const { key } = atom[Atom.SerializableTypeId];
|
|
232
|
+
if (warnedUnsent.has(key) ||
|
|
233
|
+
queuedByBoundary(registry, key) ||
|
|
234
|
+
seededOnServer(registry, key)) {
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
warnedUnsent.add(key);
|
|
238
|
+
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`);
|
|
239
|
+
};
|
|
240
|
+
/**
|
|
241
|
+
* Reads the getter's atom and follows it. `plainRead` names the plain read, `useAtomValue` or
|
|
242
|
+
* `useAtom`, for `warnUnsent`; the async hooks send their atom's result themselves.
|
|
243
|
+
*/
|
|
244
|
+
const subscribedReader = (registry, getAtom, plainRead) => {
|
|
114
245
|
if (!BROWSER) {
|
|
115
246
|
// Nothing subscribes during SSR, so without a mount the registry would sweep the node while the
|
|
116
247
|
// render awaits, losing initial values and refetching async atoms. Mounting at setup, before any
|
|
117
|
-
// await, keeps it for the request. The mounts are released
|
|
118
|
-
//
|
|
248
|
+
// await, keeps it for the request. The mounts are released when the server render ends,
|
|
249
|
+
// because a registry passed in by the caller outlives the request (JND-17).
|
|
119
250
|
// An atom with a withServerValue override is never computed on the server, so it is not mounted:
|
|
120
|
-
// mounting would run its real read, which is often browser-only.
|
|
121
|
-
//
|
|
122
|
-
//
|
|
251
|
+
// mounting would run its real read, which is often browser-only. One with a value from
|
|
252
|
+
// useAtomInitialValues is held and read without building it (see initialNodeOnServer).
|
|
253
|
+
// Once the render has ended, a read that switches atoms, as from a reader kept past the render,
|
|
254
|
+
// takes no mount: nothing would release it.
|
|
255
|
+
let ended = false;
|
|
123
256
|
const releases = [];
|
|
124
|
-
|
|
257
|
+
onRenderEnd(() => {
|
|
258
|
+
ended = true;
|
|
125
259
|
for (const release of releases) {
|
|
126
260
|
release();
|
|
127
261
|
}
|
|
128
262
|
});
|
|
129
|
-
const nodes = internals(registry);
|
|
130
263
|
const mount = (atom) => {
|
|
131
|
-
if (hasServerValue(atom)) {
|
|
132
|
-
|
|
264
|
+
if (!ended && !hasServerValue(atom)) {
|
|
265
|
+
releases.push(serverMount(registry, atom));
|
|
133
266
|
}
|
|
134
|
-
const node = nodes.ensureNode(atom);
|
|
135
|
-
releases.push(node.preserveInitialValueOnBuild
|
|
136
|
-
? holdNode(nodes, node)
|
|
137
|
-
: registry.mount(atom));
|
|
138
267
|
return atom;
|
|
139
268
|
};
|
|
140
269
|
let mounted = mount(getAtom());
|
|
@@ -143,13 +272,18 @@ const subscribedReader = (registry, getAtom) => {
|
|
|
143
272
|
if (atom !== mounted) {
|
|
144
273
|
mounted = mount(atom);
|
|
145
274
|
}
|
|
146
|
-
if (
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
275
|
+
if (hasServerValue(atom)) {
|
|
276
|
+
return Atom.getServerValue(atom, registry);
|
|
277
|
+
}
|
|
278
|
+
// An initial value is what the browser starts from too.
|
|
279
|
+
if (initialOnServer(registry, atom)) {
|
|
280
|
+
return serverGet(registry, atom);
|
|
151
281
|
}
|
|
152
|
-
|
|
282
|
+
const value = registry.get(atom);
|
|
283
|
+
if (plainRead) {
|
|
284
|
+
warnUnsent(registry, atom, value, plainRead);
|
|
285
|
+
}
|
|
286
|
+
return value;
|
|
153
287
|
};
|
|
154
288
|
}
|
|
155
289
|
reportReads(registry, getAtom, "read");
|
|
@@ -170,23 +304,30 @@ const subscribedReader = (registry, getAtom) => {
|
|
|
170
304
|
kept?.cancel();
|
|
171
305
|
kept = undefined;
|
|
172
306
|
};
|
|
173
|
-
// Whether the component has mounted: its effects have run once.
|
|
174
|
-
let mounted = false;
|
|
175
307
|
// The atom this reader's own registry.get is reading. A subscribed node is rebuilt as soon as it
|
|
176
308
|
// goes stale, so the read builds it only the first time, and that build announces the very value
|
|
177
|
-
// the read returns
|
|
178
|
-
//
|
|
179
|
-
//
|
|
180
|
-
//
|
|
181
|
-
//
|
|
182
|
-
//
|
|
183
|
-
//
|
|
309
|
+
// the read returns, so it is dropped: it could only re-render a read that already has the value.
|
|
310
|
+
// Before mount, delivered on a microtask it started a Svelte batch that, when the first render ran
|
|
311
|
+
// after an await in markup (a HydrationBoundary awaiting its state), committed before hydration's
|
|
312
|
+
// own and made Svelte's dev build throw "Batch has scheduled effects" (JND-95). After mount, a
|
|
313
|
+
// getter switched to an atom not built yet ran a transform twice, made a new object for an
|
|
314
|
+
// unchanged value, and re-ran an $effect that first read the atom. Only the build's own
|
|
315
|
+
// announcement is dropped: a value set during a registry batch is announced at commit, after
|
|
316
|
+
// `reading` is reset, and later changes arrive as usual. A getter switched in onMount once
|
|
317
|
+
// depended on that update, by accident (JND-98, fixed in the effect below). After mount, only a
|
|
318
|
+
// first build is dropped: a stale node the read rebuilds, as after a failed rebuild, announces a
|
|
319
|
+
// value the reader's other reads haven't seen.
|
|
320
|
+
let mounted = false;
|
|
184
321
|
let reading;
|
|
185
|
-
const listen = (current, update) =>
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
322
|
+
const listen = (current, update) => {
|
|
323
|
+
const unsubscribe = registry.subscribe(current, () => {
|
|
324
|
+
if (reading !== current) {
|
|
325
|
+
update();
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
dropInitialValueMark(registry, current);
|
|
329
|
+
return unsubscribe;
|
|
330
|
+
};
|
|
190
331
|
const follow = (current) => {
|
|
191
332
|
if (kept?.atom === current) {
|
|
192
333
|
const { cancel: keptCancel } = kept;
|
|
@@ -213,8 +354,10 @@ const subscribedReader = (registry, getAtom) => {
|
|
|
213
354
|
if (atom !== undefined && atom !== committed) {
|
|
214
355
|
const previous = atom;
|
|
215
356
|
const previousCancel = cancel;
|
|
357
|
+
// Subscribing can throw, as it rebuilds a stale atom; nothing moves until it has succeeded.
|
|
358
|
+
const next = follow(committed);
|
|
216
359
|
atom = committed;
|
|
217
|
-
cancel =
|
|
360
|
+
cancel = next;
|
|
218
361
|
if (previousCancel) {
|
|
219
362
|
releaseKept();
|
|
220
363
|
kept = { atom: previous, cancel: previousCancel };
|
|
@@ -236,8 +379,10 @@ const subscribedReader = (registry, getAtom) => {
|
|
|
236
379
|
if (current !== atom) {
|
|
237
380
|
const previous = atom;
|
|
238
381
|
const previousCancel = cancel;
|
|
382
|
+
// As in the effect: a throwing subscribe leaves the reader on its old atom, to try again.
|
|
383
|
+
const next = follow(current);
|
|
239
384
|
atom = current;
|
|
240
|
-
cancel =
|
|
385
|
+
cancel = next;
|
|
241
386
|
if (previous === committed &&
|
|
242
387
|
previous !== undefined &&
|
|
243
388
|
previousCancel) {
|
|
@@ -250,8 +395,17 @@ const subscribedReader = (registry, getAtom) => {
|
|
|
250
395
|
}
|
|
251
396
|
subscribe();
|
|
252
397
|
const outer = reading;
|
|
253
|
-
|
|
398
|
+
if (!mounted) {
|
|
399
|
+
reading = current;
|
|
400
|
+
}
|
|
254
401
|
try {
|
|
402
|
+
// After mount, only a first build is dropped: a stale node rebuilt here, as after a failed
|
|
403
|
+
// rebuild, announces a change other reads of this reader have not seen. A node whose first
|
|
404
|
+
// build failed is still uninitialized, but the page has shown that failure, so it counts too.
|
|
405
|
+
const node = nodeOf(registry, current);
|
|
406
|
+
if (node.currentState() === "uninitialized" && !node.failed) {
|
|
407
|
+
reading = current;
|
|
408
|
+
}
|
|
255
409
|
return registry.get(current);
|
|
256
410
|
}
|
|
257
411
|
finally {
|
|
@@ -260,7 +414,7 @@ const subscribedReader = (registry, getAtom) => {
|
|
|
260
414
|
});
|
|
261
415
|
};
|
|
262
416
|
export function useAtomValue(input, f) {
|
|
263
|
-
const read = subscribedReader(getRegistry(), toGetter(input));
|
|
417
|
+
const read = subscribedReader(getRegistry(), toGetter(input), "useAtomValue");
|
|
264
418
|
if (!f) {
|
|
265
419
|
return new AtomCell(read, readOnly);
|
|
266
420
|
}
|
|
@@ -290,14 +444,21 @@ export function useAtomValue(input, f) {
|
|
|
290
444
|
export const useAtom = (input) => {
|
|
291
445
|
const registry = getRegistry();
|
|
292
446
|
const getAtom = toGetter(input);
|
|
293
|
-
return new AtomCell(subscribedReader(registry, getAtom), (value) =>
|
|
447
|
+
return new AtomCell(subscribedReader(registry, getAtom, "useAtom"), (value) => {
|
|
448
|
+
registry.set(getAtom(), value);
|
|
449
|
+
});
|
|
294
450
|
};
|
|
295
451
|
/** useAtomMount, telling an inspector scope why the atom is held: `useAtomSet` holds it to write. */
|
|
296
452
|
const mountWhileAlive = (input, kind) => {
|
|
297
453
|
const registry = getRegistry();
|
|
298
454
|
const getAtom = toGetter(input);
|
|
299
455
|
reportReads(registry, getAtom, kind);
|
|
300
|
-
$effect(() =>
|
|
456
|
+
$effect(() => {
|
|
457
|
+
const atom = getAtom();
|
|
458
|
+
const release = registry.mount(atom);
|
|
459
|
+
dropInitialValueMark(registry, atom);
|
|
460
|
+
return release;
|
|
461
|
+
});
|
|
301
462
|
};
|
|
302
463
|
/**
|
|
303
464
|
* Keeps an atom alive for as long as this component is mounted, even when no component reads
|
|
@@ -329,9 +490,14 @@ export function useAtomSet(input, options) {
|
|
|
329
490
|
if (mode === "value") {
|
|
330
491
|
return (value) => {
|
|
331
492
|
const atom = getAtom();
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
493
|
+
if (typeof value !== "function") {
|
|
494
|
+
registry.set(atom, value);
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
// SAFETY: the overloads type a function value as an update from the current value.
|
|
498
|
+
// oxlint-disable-next-line typescript/no-unsafe-type-assertion -- a typeof check can't recover the function's signature
|
|
499
|
+
const updateFrom = value;
|
|
500
|
+
registry.set(atom, updateFrom(registry.get(atom)));
|
|
335
501
|
};
|
|
336
502
|
}
|
|
337
503
|
return async (value, writeOptions) => {
|
|
@@ -340,17 +506,35 @@ export function useAtomSet(input, options) {
|
|
|
340
506
|
// never end.
|
|
341
507
|
throw new TypeError(`useAtomSet's ${mode} mode cannot wait for Atom.Reset; reset with a value mode setter`);
|
|
342
508
|
}
|
|
509
|
+
// SAFETY: the overloads offer the promise modes only for an atom that holds an AsyncResult.
|
|
510
|
+
// oxlint-disable-next-line typescript/no-unsafe-type-assertion -- the implementation signature can't carry the overloads' atom type
|
|
343
511
|
const atom = getAtom();
|
|
344
512
|
// An already aborted signal settles as interrupted without writing, as fetch does (JND-25). It
|
|
345
513
|
// skips the wait too: Effect checks the signal only after a first run, which would return a
|
|
346
514
|
// result the atom already holds from an earlier call.
|
|
347
515
|
let exit;
|
|
348
|
-
if (writeOptions?.signal?.aborted) {
|
|
516
|
+
if (writeOptions?.signal?.aborted === true) {
|
|
349
517
|
exit = Exit.interrupt();
|
|
350
518
|
}
|
|
351
519
|
else {
|
|
520
|
+
// oxlint-disable-next-line effecttsgo/global-date -- compared with an AsyncResult's timestamp, which Effect takes from Date.now
|
|
521
|
+
const start = Date.now();
|
|
352
522
|
registry.set(atom, value);
|
|
353
|
-
|
|
523
|
+
// A provider disposing of its registry ends the call without a last result, so the wait ends
|
|
524
|
+
// there too, as interrupted.
|
|
525
|
+
const controller = new AbortController();
|
|
526
|
+
const abort = () => {
|
|
527
|
+
controller.abort();
|
|
528
|
+
};
|
|
529
|
+
writeOptions?.signal?.addEventListener("abort", abort, { once: true });
|
|
530
|
+
const cancel = onDispose(registry, abort);
|
|
531
|
+
try {
|
|
532
|
+
exit = await awaitWrite(registry, atom, start, controller.signal);
|
|
533
|
+
}
|
|
534
|
+
finally {
|
|
535
|
+
cancel();
|
|
536
|
+
writeOptions?.signal?.removeEventListener("abort", abort);
|
|
537
|
+
}
|
|
354
538
|
}
|
|
355
539
|
return mode === "promiseExit" ? exit : valueOrThrow(exit);
|
|
356
540
|
};
|
|
@@ -376,7 +560,9 @@ export const useAtomRefresh = (input) => {
|
|
|
376
560
|
const registry = getRegistry();
|
|
377
561
|
const getAtom = toGetter(input);
|
|
378
562
|
useAtomMount(getAtom);
|
|
379
|
-
return () =>
|
|
563
|
+
return () => {
|
|
564
|
+
registry.refresh(getAtom());
|
|
565
|
+
};
|
|
380
566
|
};
|
|
381
567
|
/**
|
|
382
568
|
* Delivers each value to `f` as afterReads would, but in order and only while subscribed. Once a
|
|
@@ -386,12 +572,13 @@ export const useAtomRefresh = (input) => {
|
|
|
386
572
|
const orderedDelivery = (f) => {
|
|
387
573
|
let live = true;
|
|
388
574
|
let flushing = false;
|
|
575
|
+
// Boxed, so an empty queue is told apart from a queued undefined.
|
|
389
576
|
const queue = [];
|
|
390
577
|
// Stopping empties the queue, which ends a flush in progress.
|
|
391
578
|
const flush = () => {
|
|
392
579
|
try {
|
|
393
|
-
|
|
394
|
-
f(
|
|
580
|
+
for (let next = queue.shift(); next; next = queue.shift()) {
|
|
581
|
+
f(next.value);
|
|
395
582
|
}
|
|
396
583
|
}
|
|
397
584
|
finally {
|
|
@@ -407,7 +594,7 @@ const orderedDelivery = (f) => {
|
|
|
407
594
|
return;
|
|
408
595
|
}
|
|
409
596
|
if (flushing || activeReads > 0) {
|
|
410
|
-
queue.push(value);
|
|
597
|
+
queue.push({ value });
|
|
411
598
|
if (!flushing) {
|
|
412
599
|
flushing = true;
|
|
413
600
|
queueMicrotask(flush);
|
|
@@ -450,8 +637,11 @@ export const useAtomSubscribe = (input, f, options) => {
|
|
|
450
637
|
const registry = getRegistry();
|
|
451
638
|
const getAtom = toGetter(input);
|
|
452
639
|
reportReads(registry, getAtom, "subscribe");
|
|
640
|
+
// The effect follows the atom, not what the getter reads: a getter that runs again and returns
|
|
641
|
+
// the same atom must not call `immediate` again, nor drop a change still waiting for delivery.
|
|
642
|
+
const current = $derived(getAtom());
|
|
453
643
|
$effect(() => {
|
|
454
|
-
const atom =
|
|
644
|
+
const atom = current;
|
|
455
645
|
// `immediate` calls `f` now, inside this effect; what `f` reads must not re-run it.
|
|
456
646
|
return untrack(() => {
|
|
457
647
|
// registry.subscribe does not compute a node that has never been read, so an atom nothing
|
|
@@ -463,6 +653,7 @@ export const useAtomSubscribe = (input, f, options) => {
|
|
|
463
653
|
}
|
|
464
654
|
const deliver = orderedDelivery(f);
|
|
465
655
|
const cancel = registry.subscribe(atom, deliver);
|
|
656
|
+
dropInitialValueMark(registry, atom);
|
|
466
657
|
return () => {
|
|
467
658
|
deliver.stop();
|
|
468
659
|
cancel();
|
|
@@ -475,11 +666,15 @@ export const useAtomSubscribe = (input, f, options) => {
|
|
|
475
666
|
* atom's next read; this sets it on the node now, which is valid, so nothing reads it again.
|
|
476
667
|
*/
|
|
477
668
|
const setNodeValue = (registry, atom, value) => {
|
|
478
|
-
|
|
669
|
+
nodeOf(registry, atom).setValue(value);
|
|
670
|
+
dropInitialValueMark(registry, atom);
|
|
479
671
|
};
|
|
480
672
|
// The nodes given a value by useAtomInitialValues. A node belongs to one registry, and a node the
|
|
481
673
|
// registry sweeps is gone from here too, so the next component to start it gives it the value again.
|
|
482
674
|
const initialValuesApplied = new WeakSet();
|
|
675
|
+
// On the server, how many renders hold each applied node. The value applies again only once none
|
|
676
|
+
// does, so a request ending doesn't let a request starting overwrite the value another still renders.
|
|
677
|
+
const serverHolders = new WeakMap();
|
|
483
678
|
/**
|
|
484
679
|
* Sets starting values as `AtomRegistry.make({ initialValues })` would: on the atom that receives it
|
|
485
680
|
* (a wrapper such as `withRefresh` passes it to its source), kept as the value of the atom's first
|
|
@@ -489,9 +684,10 @@ const initialValuesApplied = new WeakSet();
|
|
|
489
684
|
* an atom that can only compute in the browser can start from a value on the server.
|
|
490
685
|
*
|
|
491
686
|
* A value applies once while the atom is held, so a component mounted later, or a second
|
|
492
|
-
* component with its own value, does not overwrite a value the atom has moved on from
|
|
493
|
-
*
|
|
494
|
-
*
|
|
687
|
+
* component with its own value, does not overwrite a value the atom has moved on from since the
|
|
688
|
+
* first applied one. An atom that is already in use without one, read by another component, is set
|
|
689
|
+
* to the value, as `@effect/atom-react` does. Once nothing holds the atom and the registry disposes
|
|
690
|
+
* 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
|
|
495
691
|
* its own; requests rendering at the same time share the registry's atoms, and with them the value
|
|
496
692
|
* the first one applied.
|
|
497
693
|
*
|
|
@@ -513,53 +709,49 @@ const initialValuesApplied = new WeakSet();
|
|
|
513
709
|
* @category hooks
|
|
514
710
|
*/
|
|
515
711
|
export const useAtomInitialValues = (initialValues) => {
|
|
516
|
-
const registry =
|
|
712
|
+
const registry = getRegistry();
|
|
517
713
|
const releases = [];
|
|
714
|
+
// Registered before the loop: an entry that throws midway leaves the earlier ones held otherwise.
|
|
715
|
+
onTeardown(() => {
|
|
716
|
+
for (const release of releases) {
|
|
717
|
+
release();
|
|
718
|
+
}
|
|
719
|
+
});
|
|
518
720
|
for (const [atom, value] of initialValues) {
|
|
519
721
|
const node = registry.ensureNode(initialValueTarget(atom));
|
|
520
722
|
if (!initialValuesApplied.has(node)) {
|
|
521
723
|
initialValuesApplied.add(node);
|
|
522
724
|
node.setInitialValue(value);
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
725
|
+
}
|
|
726
|
+
if (!BROWSER) {
|
|
727
|
+
// A registry passed in by the caller outlives the request, and its node may not be swept
|
|
728
|
+
// before the next request starts, whose value must apply too (JND-17). Requests rendering at
|
|
729
|
+
// the same time share the value the first applied, until the last of them ends.
|
|
730
|
+
serverHolders.set(node, (serverHolders.get(node) ?? 0) + 1);
|
|
731
|
+
releases.push(() => {
|
|
732
|
+
const holders = (serverHolders.get(node) ?? 1) - 1;
|
|
733
|
+
serverHolders.set(node, holders);
|
|
734
|
+
if (holders === 0) {
|
|
735
|
+
initialValuesApplied.delete(node);
|
|
736
|
+
}
|
|
737
|
+
});
|
|
528
738
|
}
|
|
529
739
|
releases.push(holdNode(registry, node));
|
|
530
740
|
}
|
|
531
|
-
onTeardown(() => {
|
|
532
|
-
for (const release of releases) {
|
|
533
|
-
release();
|
|
534
|
-
}
|
|
535
|
-
});
|
|
536
741
|
};
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
* ```ts
|
|
548
|
-
* import { useAtomRef } from "effect-atom-svelte";
|
|
549
|
-
* import { profile } from "./refs.ts";
|
|
550
|
-
*
|
|
551
|
-
* const current = useAtomRef(profile);
|
|
552
|
-
* // In the markup: {current.current.name}
|
|
553
|
-
* ```
|
|
554
|
-
*
|
|
555
|
-
* @stability unstable
|
|
556
|
-
* @since 0.1.0
|
|
557
|
-
* @category hooks
|
|
558
|
-
*/
|
|
559
|
-
export const useAtomRef = (input) => {
|
|
742
|
+
const isWritableRef = (ref) => "set" in ref;
|
|
743
|
+
/** Writes a ref that has `set`; a read-only ref, as from `map`, throws as `useAtomValue` would. */
|
|
744
|
+
const writeRef = (getRef) => (value) => {
|
|
745
|
+
const ref = getRef();
|
|
746
|
+
if (!isWritableRef(ref)) {
|
|
747
|
+
return readOnly();
|
|
748
|
+
}
|
|
749
|
+
ref.set(value);
|
|
750
|
+
};
|
|
751
|
+
export function useAtomRef(input) {
|
|
560
752
|
const getRef = toGetter(input);
|
|
561
753
|
if (!BROWSER) {
|
|
562
|
-
return new AtomCell(() => getRef().value,
|
|
754
|
+
return new AtomCell(() => getRef().value, writeRef(getRef));
|
|
563
755
|
}
|
|
564
756
|
const ref = $derived(getRef());
|
|
565
757
|
const subscribe = $derived.by(() => {
|
|
@@ -569,8 +761,8 @@ export const useAtomRef = (input) => {
|
|
|
569
761
|
return new AtomCell(() => duringRead(() => {
|
|
570
762
|
subscribe();
|
|
571
763
|
return ref.value;
|
|
572
|
-
}),
|
|
573
|
-
}
|
|
764
|
+
}), writeRef(() => ref));
|
|
765
|
+
}
|
|
574
766
|
/**
|
|
575
767
|
* Returns `ref.prop(prop)`, the `AtomRef` for one property of an `AtomRef`. It takes the ref
|
|
576
768
|
* itself, not a getter, and does not follow later changes to `ref` or `prop`; to read a property
|
|
@@ -593,7 +785,8 @@ export const useAtomRef = (input) => {
|
|
|
593
785
|
export const useAtomRefProp = (ref, prop) => ref.prop(prop);
|
|
594
786
|
/**
|
|
595
787
|
* Reads one property of an `AtomRef`, following the getter to a different ref. `prop` is taken
|
|
596
|
-
* once, when the component starts, and is not followed.
|
|
788
|
+
* once, when the component starts, and is not followed. Assigning `current` sets the property, so
|
|
789
|
+
* `bind:checked={done.current}` works.
|
|
597
790
|
*
|
|
598
791
|
* **Example** (Reading one property of a ref)
|
|
599
792
|
*
|
|
@@ -614,11 +807,6 @@ export const useAtomRefPropValue = (input, prop) => {
|
|
|
614
807
|
const propRef = $derived(getRef().prop(prop));
|
|
615
808
|
return useAtomRef(() => propRef);
|
|
616
809
|
};
|
|
617
|
-
/**
|
|
618
|
-
* Whether a result is `Initial` with nothing running to settle it, as an `Atom.fn` nothing has
|
|
619
|
-
* called. On the server nothing will start it during the render, so waiting would hang it.
|
|
620
|
-
*/
|
|
621
|
-
const notStarted = (result) => result._tag === "Initial" && !result.waiting;
|
|
622
810
|
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.`);
|
|
623
811
|
/**
|
|
624
812
|
* Puts the server's value in the registry as current, so nothing fetches it again (JND-19). Seeding
|
|
@@ -639,15 +827,25 @@ const applySeed = (registry, atom, value, revalidate) => {
|
|
|
639
827
|
const seeds = new WeakMap();
|
|
640
828
|
// hydratable returns one promise per key per render, so it identifies the atom that claimed a key.
|
|
641
829
|
const serverSeeds = new WeakMap();
|
|
830
|
+
// The keys, by their render's promise, that some reader reads with suspendOnWaiting: the seed is
|
|
831
|
+
// then the settled result that reader renders, whichever reader claimed the key first.
|
|
832
|
+
const settledSeeds = new WeakSet();
|
|
642
833
|
/**
|
|
643
834
|
* On the server each request seeds for itself: `hydratable` already shares one result per key
|
|
644
835
|
* within a render, and a per-registry record would outlive the request when the caller owns the
|
|
645
836
|
* registry, so later requests would skip embedding their seed (JND-17).
|
|
646
837
|
*/
|
|
647
|
-
const seedOnServer = (registry, atom, key, encode) => {
|
|
838
|
+
const seedOnServer = (registry, atom, key, encode, suspendOnWaiting = false) => {
|
|
648
839
|
// Mounted until the render ends, so the settled node is what the render reads.
|
|
649
|
-
const release = registry
|
|
650
|
-
|
|
840
|
+
const release = serverMount(registry, atom);
|
|
841
|
+
// A render ends before its seeds settle only when it fails, as when a later reader throws during
|
|
842
|
+
// setup. The provider has disposed of the registry by then, so the seed reads nothing from it.
|
|
843
|
+
let ended = false;
|
|
844
|
+
onRenderEnd(() => {
|
|
845
|
+
ended = true;
|
|
846
|
+
release();
|
|
847
|
+
});
|
|
848
|
+
countSeeded(registry, key);
|
|
651
849
|
// hydratable hands every later reader of a key the first reader's value, but Svelte's dev build
|
|
652
850
|
// also runs each later reader's callback and throws hydratable_clobbering if what it encodes
|
|
653
851
|
// differs. Reading the atom again there would encode whatever it holds by then, so a later
|
|
@@ -656,22 +854,39 @@ const seedOnServer = (registry, atom, key, encode) => {
|
|
|
656
854
|
const claim = {};
|
|
657
855
|
const encoded = hydratable(key, () => {
|
|
658
856
|
claim.own = (async () => {
|
|
659
|
-
await
|
|
857
|
+
await Promise.resolve();
|
|
858
|
+
if (ended) {
|
|
859
|
+
return noSeed;
|
|
860
|
+
}
|
|
660
861
|
if (claim.first !== claim.own) {
|
|
661
862
|
return await claim.first;
|
|
662
863
|
}
|
|
663
|
-
// Not waited for if nothing has started it: the hook rejects once seeded.
|
|
664
|
-
|
|
665
|
-
|
|
864
|
+
// Not waited for if nothing has started it: the hook rejects once seeded. Waited for as the hook
|
|
865
|
+
// waits, so with suspendOnWaiting the seed is the settled result the render shows, not one the
|
|
866
|
+
// browser would run again because it was still waiting.
|
|
867
|
+
if (!initialOnServer(registry, atom) && !notStarted(registry.get(atom))) {
|
|
868
|
+
let settled = suspendOnWaiting;
|
|
869
|
+
await awaitResult(registry, atom, { suspendOnWaiting: settled });
|
|
870
|
+
// A later reader of the key may want the settled result once this one's wait has begun.
|
|
871
|
+
if (!settled && claim.first && settledSeeds.has(claim.first)) {
|
|
872
|
+
settled = true;
|
|
873
|
+
await awaitResult(registry, atom, { suspendOnWaiting: settled });
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
if (ended) {
|
|
877
|
+
return noSeed;
|
|
666
878
|
}
|
|
667
|
-
return encodeSeed(key, encode, registry
|
|
879
|
+
return encodeSeed(key, encode, serverGet(registry, atom));
|
|
668
880
|
})();
|
|
669
881
|
return claim.own;
|
|
670
882
|
});
|
|
671
883
|
claim.first = encoded;
|
|
884
|
+
if (suspendOnWaiting) {
|
|
885
|
+
settledSeeds.add(encoded);
|
|
886
|
+
}
|
|
672
887
|
const claimed = serverSeeds.get(encoded);
|
|
673
888
|
if (claimed && claimed !== atom) {
|
|
674
|
-
throw new Error(`Two different atoms share the serialization key "${key}"`);
|
|
889
|
+
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")}`);
|
|
675
890
|
}
|
|
676
891
|
serverSeeds.set(encoded, atom);
|
|
677
892
|
return (async () => {
|
|
@@ -689,12 +904,18 @@ const claimedKeys = new WeakMap();
|
|
|
689
904
|
// The store is not public API, so a Svelte that changes its shape or drops it turns the warning off
|
|
690
905
|
// rather than throwing.
|
|
691
906
|
const serverValues = () => {
|
|
692
|
-
const
|
|
907
|
+
const svelte = Object.getOwnPropertyDescriptor(globalThis, "__svelte")?.value;
|
|
908
|
+
const store = typeof svelte === "object" && svelte !== null && "h" in svelte
|
|
909
|
+
? svelte.h
|
|
910
|
+
: undefined;
|
|
693
911
|
return store instanceof Map ? store : undefined;
|
|
694
912
|
};
|
|
695
913
|
const warnIfSent = async (key, sent) => {
|
|
696
914
|
try {
|
|
697
|
-
|
|
915
|
+
// Undefined when the key wasn't sent, null when the server sent that it had no seed for it, as
|
|
916
|
+
// for a defect: either way the browser was always going to compute the atom.
|
|
917
|
+
const value = await sent;
|
|
918
|
+
if (value === undefined || value === null) {
|
|
698
919
|
return;
|
|
699
920
|
}
|
|
700
921
|
}
|
|
@@ -738,7 +959,9 @@ const claimServerValue = (key, missed) => {
|
|
|
738
959
|
* serialization key. The atom is fetched again once seeded if any component using the key then
|
|
739
960
|
* asked to revalidate.
|
|
740
961
|
*/
|
|
741
|
-
const seedFromServer = (registry, getAtom,
|
|
962
|
+
const seedFromServer = (registry, getAtom,
|
|
963
|
+
// oxlint-disable-next-line sonarjs/bool-param-default -- undefined is "the hook gave no option", so the provider's default applies
|
|
964
|
+
revalidateOption, suspendOnWaiting = false) => {
|
|
742
965
|
const atom = getAtom();
|
|
743
966
|
// The server never computes an atom with a server value, so it has no value to pass on, and the
|
|
744
967
|
// browser must not ask for one: hydratable throws for a key the server didn't write (JND-58).
|
|
@@ -748,7 +971,7 @@ const seedFromServer = (registry, getAtom, revalidateOption) => {
|
|
|
748
971
|
const { decode, encode, key } = atom[Atom.SerializableTypeId];
|
|
749
972
|
if (!BROWSER) {
|
|
750
973
|
return {
|
|
751
|
-
done: seedOnServer(registry, atom, key, encode),
|
|
974
|
+
done: seedOnServer(registry, atom, key, encode, suspendOnWaiting),
|
|
752
975
|
letGo: noSeedMount,
|
|
753
976
|
};
|
|
754
977
|
}
|
|
@@ -759,10 +982,11 @@ const seedFromServer = (registry, getAtom, revalidateOption) => {
|
|
|
759
982
|
seeds.set(registry, registrySeeds);
|
|
760
983
|
}
|
|
761
984
|
const { held, spent } = registrySeeds;
|
|
985
|
+
// The server's value belongs to the key, not to an atom: the registry keeps a serializable atom's
|
|
986
|
+
// value under its key, so every atom with the key reads it, as HydrationBoundary assumes too.
|
|
987
|
+
// A remounted branch, as under {#key}, makes a new atom with the old one's key while the old one
|
|
988
|
+
// still holds it, until Svelte destroys the old branch or its outro ends: the new atom joins it.
|
|
762
989
|
let entry = held.get(key);
|
|
763
|
-
if (entry && entry.atom !== atom) {
|
|
764
|
-
throw new Error(`Two different atoms share the serialization key "${key}"`);
|
|
765
|
-
}
|
|
766
990
|
if (!entry && spent.has(key)) {
|
|
767
991
|
return undefined;
|
|
768
992
|
}
|
|
@@ -780,7 +1004,6 @@ const seedFromServer = (registry, getAtom, revalidateOption) => {
|
|
|
780
1004
|
let holders = 0;
|
|
781
1005
|
let revalidating = 0;
|
|
782
1006
|
entry = {
|
|
783
|
-
atom,
|
|
784
1007
|
done: (async () => {
|
|
785
1008
|
const value = await encoded;
|
|
786
1009
|
// Only the server's value is a seed, and only while someone is there to read it now: a seed
|
|
@@ -788,7 +1011,9 @@ const seedFromServer = (registry, getAtom, revalidateOption) => {
|
|
|
788
1011
|
// The server sends no seed for a result it can't or mustn't pass on, such as a defect: the
|
|
789
1012
|
// atom computes here instead. A seed still waiting, as a stream's between its values, runs
|
|
790
1013
|
// again too: the server's run ended with the render, so it would wait forever.
|
|
791
|
-
const seed = !computedHere && holders > 0
|
|
1014
|
+
const seed = !computedHere && holders > 0
|
|
1015
|
+
? decodeSeed(key, value, decode)
|
|
1016
|
+
: undefined;
|
|
792
1017
|
if (seed !== undefined) {
|
|
793
1018
|
applySeed(registry, atom, seed, revalidating > 0 || isWaiting(seed));
|
|
794
1019
|
}
|
|
@@ -878,7 +1103,7 @@ const seedFromServer = (registry, getAtom, revalidateOption) => {
|
|
|
878
1103
|
export const useAtomResult = async (input, options) => {
|
|
879
1104
|
const registry = getRegistry();
|
|
880
1105
|
const getAtom = toGetter(input);
|
|
881
|
-
const value =
|
|
1106
|
+
const value = new AtomCell(subscribedReader(registry, getAtom), readOnly);
|
|
882
1107
|
const atom = getAtom();
|
|
883
1108
|
let release;
|
|
884
1109
|
// Aborted when the component is destroyed, which interrupts the wait below so the atom is not held.
|
|
@@ -897,7 +1122,7 @@ export const useAtomResult = async (input, options) => {
|
|
|
897
1122
|
release = undefined;
|
|
898
1123
|
return unmount;
|
|
899
1124
|
});
|
|
900
|
-
const seed = seedFromServer(registry, getAtom, options?.revalidateOnHydrate);
|
|
1125
|
+
const seed = seedFromServer(registry, getAtom, options?.revalidateOnHydrate, options?.suspendOnWaiting);
|
|
901
1126
|
if (seed) {
|
|
902
1127
|
await seed.done;
|
|
903
1128
|
}
|
|
@@ -906,6 +1131,11 @@ export const useAtomResult = async (input, options) => {
|
|
|
906
1131
|
if (lifetime.signal.aborted || (!BROWSER && hasServerValue(atom))) {
|
|
907
1132
|
return value;
|
|
908
1133
|
}
|
|
1134
|
+
// On the server, an initial value is what the render shows, with nothing to wait for.
|
|
1135
|
+
if (initialOnServer(registry, atom)) {
|
|
1136
|
+
release = serverMount(registry, atom);
|
|
1137
|
+
return value;
|
|
1138
|
+
}
|
|
909
1139
|
if (effectHolds) {
|
|
910
1140
|
// Mounted while seeding, as when the promise is not awaited straight away: the effect holds the
|
|
911
1141
|
// getter's atom, and a mount here would never be released. Once the getter has switched away,
|
|
@@ -932,9 +1162,9 @@ const isPending = (current, options) => current._tag === "Initial" ||
|
|
|
932
1162
|
/** What the promise resolves with, or the error it rejects with, for a result that is not pending. */
|
|
933
1163
|
const fromSettled = (current, options) => {
|
|
934
1164
|
if (current._tag === "Success") {
|
|
935
|
-
return options.includeFailure ? current : current.value;
|
|
1165
|
+
return options.includeFailure === true ? current : current.value;
|
|
936
1166
|
}
|
|
937
|
-
if (current._tag === "Failure" &&
|
|
1167
|
+
if (current._tag === "Failure" && options.includeFailure !== true) {
|
|
938
1168
|
throw Cause.squash(current.cause);
|
|
939
1169
|
}
|
|
940
1170
|
return current;
|
|
@@ -946,12 +1176,13 @@ const fromSettled = (current, options) => {
|
|
|
946
1176
|
*/
|
|
947
1177
|
const fromServerValue = (current, options) => {
|
|
948
1178
|
if (isPending(current, options)) {
|
|
949
|
-
return Promise.reject(new Error(
|
|
1179
|
+
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")}`));
|
|
950
1180
|
}
|
|
951
1181
|
try {
|
|
952
1182
|
return Promise.resolve(fromSettled(current, options));
|
|
953
1183
|
}
|
|
954
1184
|
catch (error) {
|
|
1185
|
+
// oxlint-disable-next-line typescript/prefer-promise-reject-errors -- rejects with the atom's squashed error, as awaiting the atom would, whatever its type
|
|
955
1186
|
return Promise.reject(error);
|
|
956
1187
|
}
|
|
957
1188
|
};
|
|
@@ -960,17 +1191,16 @@ const suspend = async (registry, atom, current, options, signal) => {
|
|
|
960
1191
|
if (!BROWSER && notStarted(current)) {
|
|
961
1192
|
throw notStartedError("useAtomSuspense");
|
|
962
1193
|
}
|
|
963
|
-
const exit = await
|
|
1194
|
+
const exit = await awaitSettled(registry, atom, { suspendOnWaiting: options.suspendOnWaiting }, signal);
|
|
964
1195
|
// Abandoned: rejects with the reason its last reader's signal was aborted with. For a re-run
|
|
965
1196
|
// that is Svelte's STALE_REACTION, which it ignores, so a pending render that still awaits this
|
|
966
1197
|
// wait keeps waiting for its next run instead of showing the interruption (JND-22).
|
|
967
1198
|
signal.throwIfAborted();
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
: AsyncResult.failure(exit.cause);
|
|
1199
|
+
// Only an abort, handled above, ends the wait without a result.
|
|
1200
|
+
if (Exit.isFailure(exit)) {
|
|
1201
|
+
throw Cause.squash(exit.cause);
|
|
972
1202
|
}
|
|
973
|
-
return
|
|
1203
|
+
return fromSettled(exit.value, options);
|
|
974
1204
|
}
|
|
975
1205
|
return fromSettled(current, options);
|
|
976
1206
|
};
|
|
@@ -1051,18 +1281,21 @@ const sharedWait = (start, onAbandoned) => {
|
|
|
1051
1281
|
}
|
|
1052
1282
|
held.add(signal);
|
|
1053
1283
|
holders += 1;
|
|
1054
|
-
const listener = () =>
|
|
1284
|
+
const listener = () => {
|
|
1285
|
+
release(signal.reason);
|
|
1286
|
+
};
|
|
1055
1287
|
signal.addEventListener("abort", listener, { once: true });
|
|
1056
1288
|
listeners.push([signal, listener]);
|
|
1057
1289
|
},
|
|
1058
1290
|
promise,
|
|
1291
|
+
settled: () => settled,
|
|
1059
1292
|
};
|
|
1060
1293
|
};
|
|
1061
1294
|
export function useAtomSuspense(input, options = {}) {
|
|
1062
1295
|
const registry = getRegistry();
|
|
1063
1296
|
const getAtom = toGetter(input);
|
|
1064
|
-
const result =
|
|
1065
|
-
const seed = seedFromServer(registry, getAtom, options.revalidateOnHydrate);
|
|
1297
|
+
const result = new AtomCell(subscribedReader(registry, getAtom), readOnly);
|
|
1298
|
+
const seed = seedFromServer(registry, getAtom, options.revalidateOnHydrate, options.suspendOnWaiting);
|
|
1066
1299
|
// A plain variable, not $state: a render in a batch with this write rolled back would take the
|
|
1067
1300
|
// pre-seed path and track nothing but the flag. Reading `trackSeed` asks for a re-read instead.
|
|
1068
1301
|
let seeded = seed === undefined;
|
|
@@ -1091,10 +1324,48 @@ export function useAtomSuspense(input, options = {}) {
|
|
|
1091
1324
|
// Holds waits read outside any derived or effect (a top-level await in the script, an event
|
|
1092
1325
|
// handler), which cannot say when they are done with them, until the component is destroyed.
|
|
1093
1326
|
const lifetime = new AbortController();
|
|
1094
|
-
|
|
1327
|
+
// Each reaction's signal, also aborted when the component is destroyed. Svelte aborts a derived's
|
|
1328
|
+
// signal when it runs again or loses its last reader, and doesn't destroy deriveds with their
|
|
1329
|
+
// component, so a derived only the script or an event handler reads would never abort it, and
|
|
1330
|
+
// the wait it holds would keep the atom after the component is gone. Kept per signal, so a signal
|
|
1331
|
+
// still holds a wait once (see sharedWait); those still live are aborted at teardown.
|
|
1332
|
+
const bounded = new WeakMap();
|
|
1333
|
+
const liveAborts = new Set();
|
|
1334
|
+
onTeardown(() => {
|
|
1335
|
+
lifetime.abort();
|
|
1336
|
+
// Each abort deletes itself, which a Set's iteration allows.
|
|
1337
|
+
for (const abort of liveAborts) {
|
|
1338
|
+
abort();
|
|
1339
|
+
}
|
|
1340
|
+
});
|
|
1341
|
+
const untilDestroyed = (signal) => {
|
|
1342
|
+
let combined = bounded.get(signal);
|
|
1343
|
+
if (!combined) {
|
|
1344
|
+
const controller = new AbortController();
|
|
1345
|
+
combined = controller.signal;
|
|
1346
|
+
bounded.set(signal, combined);
|
|
1347
|
+
// The reaction's reason, such as Svelte's STALE_REACTION, which it ignores (JND-22).
|
|
1348
|
+
const reason = () => signal.aborted ? signal.reason : lifetime.signal.reason;
|
|
1349
|
+
if (signal.aborted || lifetime.signal.aborted) {
|
|
1350
|
+
controller.abort(reason());
|
|
1351
|
+
}
|
|
1352
|
+
else {
|
|
1353
|
+
const abort = () => {
|
|
1354
|
+
liveAborts.delete(abort);
|
|
1355
|
+
signal.removeEventListener("abort", abort);
|
|
1356
|
+
controller.abort(reason());
|
|
1357
|
+
};
|
|
1358
|
+
liveAborts.add(abort);
|
|
1359
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
return combined;
|
|
1363
|
+
};
|
|
1095
1364
|
// Keyed by atom, then result: atoms can share a result object, as a derived atom returning its
|
|
1096
1365
|
// source's result does, and a wait holds and waits for one atom.
|
|
1097
1366
|
const waits = new WeakMap();
|
|
1367
|
+
// Per atom, the wait the last read got, for a read after the component is destroyed.
|
|
1368
|
+
const lastWaits = new WeakMap();
|
|
1098
1369
|
const settle = (atom, current, signal) => {
|
|
1099
1370
|
let atomWaits = waits.get(atom);
|
|
1100
1371
|
if (!atomWaits) {
|
|
@@ -1108,6 +1379,7 @@ export function useAtomSuspense(input, options = {}) {
|
|
|
1108
1379
|
atomWaits.set(current, wait);
|
|
1109
1380
|
}
|
|
1110
1381
|
wait.hold(signal);
|
|
1382
|
+
lastWaits.set(atom, wait);
|
|
1111
1383
|
return wait.promise;
|
|
1112
1384
|
};
|
|
1113
1385
|
const fromServer = new WeakMap();
|
|
@@ -1122,6 +1394,28 @@ export function useAtomSuspense(input, options = {}) {
|
|
|
1122
1394
|
return promise;
|
|
1123
1395
|
}
|
|
1124
1396
|
const signal = readerSignal();
|
|
1397
|
+
const holder = signal ? untilDestroyed(signal) : lifetime.signal;
|
|
1398
|
+
if (holder.aborted) {
|
|
1399
|
+
// Read once the reader is gone, as in onDestroy or a handler that resumes after the component
|
|
1400
|
+
// is destroyed. A result it had settled is still there; a new wait would hold the atom until it
|
|
1401
|
+
// settles, as nothing could let go of it. Handled here, so a read nobody awaits isn't reported
|
|
1402
|
+
// as unhandled.
|
|
1403
|
+
const last = lastWaits.get(getAtom());
|
|
1404
|
+
if (last?.settled() === true) {
|
|
1405
|
+
return last.promise;
|
|
1406
|
+
}
|
|
1407
|
+
// oxlint-disable-next-line typescript/prefer-promise-reject-errors -- rejects with the reader's abort reason, such as Svelte's STALE_REACTION, which Svelte recognises
|
|
1408
|
+
const gone = Promise.reject(holder.reason);
|
|
1409
|
+
void (async () => {
|
|
1410
|
+
try {
|
|
1411
|
+
await gone;
|
|
1412
|
+
}
|
|
1413
|
+
catch {
|
|
1414
|
+
// Belongs to whoever awaits it.
|
|
1415
|
+
}
|
|
1416
|
+
})();
|
|
1417
|
+
return gone;
|
|
1418
|
+
}
|
|
1125
1419
|
if (!seeded && seed) {
|
|
1126
1420
|
// Reading the atom before the seed lands would fetch what hydration is about to provide.
|
|
1127
1421
|
const atom = getAtom();
|
|
@@ -1136,24 +1430,30 @@ export function useAtomSuspense(input, options = {}) {
|
|
|
1136
1430
|
// reading would compute the atom for nobody.
|
|
1137
1431
|
const live = [...readers].filter((reader) => !reader.aborted);
|
|
1138
1432
|
if (live.length === 0) {
|
|
1139
|
-
return
|
|
1433
|
+
return;
|
|
1434
|
+
}
|
|
1435
|
+
// A read outside any reaction can't say when it is done, so it doesn't hold the atom: held
|
|
1436
|
+
// by the component's lifetime, the atom would stay mounted after the getter moves on.
|
|
1437
|
+
const reactions = live.filter((reader) => reader !== lifetime.signal);
|
|
1438
|
+
if (reactions.length > 0) {
|
|
1439
|
+
holdWhileRead(registry, atom, reactions);
|
|
1140
1440
|
}
|
|
1141
|
-
holdWhileRead(registry, atom, live);
|
|
1142
1441
|
seed.letGo();
|
|
1143
|
-
|
|
1442
|
+
// On the server, an initial value is read without building the atom.
|
|
1443
|
+
const current = serverGet(registry, atom);
|
|
1144
1444
|
let waited;
|
|
1145
1445
|
for (const reader of live) {
|
|
1146
1446
|
waited = settle(atom, current, reader);
|
|
1147
1447
|
}
|
|
1148
|
-
return waited;
|
|
1448
|
+
return await waited;
|
|
1149
1449
|
})();
|
|
1150
1450
|
pending = { promise, readers };
|
|
1151
1451
|
afterSeed.set(atom, pending);
|
|
1152
1452
|
}
|
|
1153
|
-
pending.readers.add(
|
|
1453
|
+
pending.readers.add(holder);
|
|
1154
1454
|
return pending.promise;
|
|
1155
1455
|
}
|
|
1156
|
-
const promise = settle(getAtom(), result.current,
|
|
1456
|
+
const promise = settle(getAtom(), result.current, holder);
|
|
1157
1457
|
if (signal) {
|
|
1158
1458
|
// A reaction read it, so the reader's subscription holds the atom from here.
|
|
1159
1459
|
seed?.letGo();
|
|
@@ -1161,3 +1461,29 @@ export function useAtomSuspense(input, options = {}) {
|
|
|
1161
1461
|
return promise;
|
|
1162
1462
|
}, readOnly);
|
|
1163
1463
|
}
|
|
1464
|
+
export function useForm(form, options) {
|
|
1465
|
+
const registry = getRegistry();
|
|
1466
|
+
const initial = options.initialValues;
|
|
1467
|
+
const getInitial = initialGetter(initial);
|
|
1468
|
+
const engine = engineOf(form, {
|
|
1469
|
+
...options,
|
|
1470
|
+
initialValues: getInitial ? getInitial() : initial,
|
|
1471
|
+
registry,
|
|
1472
|
+
validates: BROWSER,
|
|
1473
|
+
});
|
|
1474
|
+
const state = useAtomValue(engine.stateAtom);
|
|
1475
|
+
const values = useAtomValue(engine.valuesAtom);
|
|
1476
|
+
engine.setReaders(() => state.current, () => values.current);
|
|
1477
|
+
if (getInitial && BROWSER) {
|
|
1478
|
+
$effect.pre(() => {
|
|
1479
|
+
const next = getInitial();
|
|
1480
|
+
untrack(() => {
|
|
1481
|
+
engine.setInitial(next);
|
|
1482
|
+
});
|
|
1483
|
+
});
|
|
1484
|
+
}
|
|
1485
|
+
onTeardown(() => {
|
|
1486
|
+
engine.dispose();
|
|
1487
|
+
});
|
|
1488
|
+
return engine.root;
|
|
1489
|
+
}
|