effect-atom-svelte 0.3.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 +4 -1
- package/dist/Form.d.ts +605 -0
- package/dist/Form.js +67 -0
- package/dist/Hooks.svelte.d.ts +54 -0
- package/dist/Hooks.svelte.js +98 -64
- package/dist/HydrationBoundary.svelte +4 -5
- package/dist/Inspector.js +49 -32
- package/dist/RegistryContext.js +3 -1
- package/dist/ScopedAtom.js +15 -7
- package/dist/SvelteKit.d.ts +46 -12
- package/dist/SvelteKit.js +130 -18
- package/dist/index.d.ts +9 -0
- package/dist/index.js +9 -0
- package/dist/internal/awaitWrite.d.ts +11 -0
- package/dist/internal/awaitWrite.js +27 -0
- package/dist/internal/boundaries.js +3 -1
- 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.js +3 -1
- package/dist/internal/nodeInternals.js +44 -11
- package/package.json +12 -10
package/dist/Hooks.svelte.d.ts
CHANGED
|
@@ -4,8 +4,10 @@
|
|
|
4
4
|
* @since 0.1.0
|
|
5
5
|
*/
|
|
6
6
|
import { Exit } from "effect";
|
|
7
|
+
import type { Schema } from "effect";
|
|
7
8
|
import { AsyncResult, Atom } from "effect/reactivity";
|
|
8
9
|
import type { AtomRef } from "effect/reactivity";
|
|
10
|
+
import type * as Form from "./Form.ts";
|
|
9
11
|
/**
|
|
10
12
|
* An atom, or a getter so the hook follows the atom when reactive state picks a different one.
|
|
11
13
|
*
|
|
@@ -400,3 +402,55 @@ export declare function useAtomSuspense<A, E>(input: AtomInput<Atom.Atom<AsyncRe
|
|
|
400
402
|
export declare function useAtomSuspense<A, E>(input: AtomInput<Atom.Atom<AsyncResult.AsyncResult<A, E>>>, options?: SuspenseOptions & {
|
|
401
403
|
readonly includeFailure?: boolean | undefined;
|
|
402
404
|
}): AtomValue<Promise<A | AsyncResult.Success<A, E> | AsyncResult.Failure<A, E>>>;
|
|
405
|
+
/**
|
|
406
|
+
* Creates an instance of a form from `Form.make` in a component. Its state lives in the
|
|
407
|
+
* component's registry and is released when the component is destroyed; a submit in flight keeps
|
|
408
|
+
* it, and keeps its atom running, until the submit settles, so a save finishes after a dialog
|
|
409
|
+
* closes. Share an instance by passing it down, or create it in the parent whose lifetime it
|
|
410
|
+
* should have, such as a wizard around its steps.
|
|
411
|
+
*
|
|
412
|
+
* Pass `id` from `$props.id()`: field ids are built from it, and must match between the server and
|
|
413
|
+
* the browser. On the server the form renders from `initialValues` and doesn't validate, so there
|
|
414
|
+
* is nothing to hydrate. A getter for `initialValues` is followed: when it changes, a clean form
|
|
415
|
+
* takes the new values and a dirty one keeps its edits.
|
|
416
|
+
*
|
|
417
|
+
* **Example** (A login form)
|
|
418
|
+
*
|
|
419
|
+
* ```svelte
|
|
420
|
+
* <script lang="ts">
|
|
421
|
+
* import { useForm } from "effect-atom-svelte";
|
|
422
|
+
* import { Login, loginAtom } from "./forms.ts";
|
|
423
|
+
*
|
|
424
|
+
* const id = $props.id();
|
|
425
|
+
* const form = useForm(Login, {
|
|
426
|
+
* id,
|
|
427
|
+
* initialValues: { email: "", password: "" },
|
|
428
|
+
* submit: { atom: loginAtom },
|
|
429
|
+
* mapError: (error) =>
|
|
430
|
+
* error._tag === "WrongPassword"
|
|
431
|
+
* ? { form: "Wrong email or password" }
|
|
432
|
+
* : undefined,
|
|
433
|
+
* });
|
|
434
|
+
* const email = form.field("email");
|
|
435
|
+
* </script>
|
|
436
|
+
*
|
|
437
|
+
* <form {...form.attrs}>
|
|
438
|
+
* <label for={email.id}>Email</label>
|
|
439
|
+
* <input {...email.attrs} bind:value={email.value} />
|
|
440
|
+
* {#if email.error}
|
|
441
|
+
* <p id={email.errorId}>{email.error}</p>
|
|
442
|
+
* {/if}
|
|
443
|
+
* {#if form.rootError}
|
|
444
|
+
* <p role="alert">{form.rootError}</p>
|
|
445
|
+
* {/if}
|
|
446
|
+
* <button disabled={form.submitting}>Log in</button>
|
|
447
|
+
* </form>
|
|
448
|
+
* ```
|
|
449
|
+
*
|
|
450
|
+
* @see {@link https://atom.jarrednorris.dev/forms | Forms}
|
|
451
|
+
* @stability unstable
|
|
452
|
+
* @since 0.4.0
|
|
453
|
+
* @category hooks
|
|
454
|
+
*/
|
|
455
|
+
export declare function useForm<F extends Form.Form<Schema.Top>, A, E, W, Args = undefined>(form: F, options: Form.InstanceOptions<Form.Encoded<F>, Form.Decoded<F>, A, E, W, Args>): Form.Instance<Form.Encoded<F>, Form.Decoded<F>, A, E, Args>;
|
|
456
|
+
export declare function useForm<F extends Form.Form<Schema.Top>>(form: F, options: Form.ValidateOnlyOptions<Form.Encoded<F>>): Form.Instance<Form.Encoded<F>, Form.Decoded<F>, Form.Decoded<F>, never, void>;
|
package/dist/Hooks.svelte.js
CHANGED
|
@@ -8,17 +8,18 @@ import { AsyncResult, Atom, AtomRegistry } from "effect/reactivity";
|
|
|
8
8
|
import { BROWSER, DEV } from "esm-env";
|
|
9
9
|
import { getAbortSignal, hydratable, untrack } from "svelte";
|
|
10
10
|
import { createSubscriber } from "svelte/reactivity";
|
|
11
|
+
import { awaitWrite } from "./internal/awaitWrite.js";
|
|
11
12
|
import { queuedByBoundary } from "./internal/boundaries.js";
|
|
12
13
|
import { onDispose } from "./internal/disposal.js";
|
|
14
|
+
import { engineOf, initialGetter } from "./internal/form/engineOf.js";
|
|
13
15
|
import { decodeSeed, encodeSeed, isWaiting, noSeed, revalidatesOnHydrate, } from "./internal/hydration.js";
|
|
14
16
|
import { onRenderEnd } from "./internal/renderEnd.js";
|
|
15
17
|
import { reportReads } from "./internal/scope.svelte.js";
|
|
16
18
|
import { onTeardown } from "./internal/teardown.svelte.js";
|
|
17
19
|
import { troubleshooting } from "./internal/troubleshooting.js";
|
|
18
20
|
import { getRegistry } from "./RegistryContext.js";
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
: () => input;
|
|
21
|
+
const isGetter = (input) => typeof input === "function" && !Atom.isAtom(input);
|
|
22
|
+
const toGetter = (input) => isGetter(input) ? input : () => input;
|
|
22
23
|
// The registry notifies subscribers synchronously while it computes an atom, and Svelte throws
|
|
23
24
|
// state_unsafe_mutation if state changes while a template or $derived is evaluating. Reads are
|
|
24
25
|
// counted, and a notification raised during one is delivered on a microtask instead. Outside a
|
|
@@ -33,7 +34,9 @@ const afterReads = (update) => {
|
|
|
33
34
|
update();
|
|
34
35
|
}
|
|
35
36
|
};
|
|
36
|
-
const notifyAfterReads = (update) => () =>
|
|
37
|
+
const notifyAfterReads = (update) => () => {
|
|
38
|
+
afterReads(update);
|
|
39
|
+
};
|
|
37
40
|
const duringRead = (f) => {
|
|
38
41
|
activeReads += 1;
|
|
39
42
|
try {
|
|
@@ -51,6 +54,7 @@ class AtomCell {
|
|
|
51
54
|
this.#read = read;
|
|
52
55
|
this.#write = write;
|
|
53
56
|
}
|
|
57
|
+
// oxlint-disable-next-line typescript/related-getter-setter-pairs -- implements AtomState, whose read and write types differ
|
|
54
58
|
get current() {
|
|
55
59
|
return this.#read();
|
|
56
60
|
}
|
|
@@ -77,11 +81,13 @@ const notStarted = (result) => result._tag === "Initial" && !result.waiting;
|
|
|
77
81
|
* computes the atom, as its real read is often browser-only.
|
|
78
82
|
*/
|
|
79
83
|
const hasServerValue = (atom) => Atom.ServerValueTypeId in atom;
|
|
80
|
-
|
|
81
|
-
|
|
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
|
|
82
87
|
// preserveInitialValueOnBuild, _value, setValue and failed (set while a first build has thrown)
|
|
83
|
-
//
|
|
84
|
-
|
|
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);
|
|
85
91
|
/** The atom that holds a value given to `atom`: a wrapper such as `withRefresh` passes it on. */
|
|
86
92
|
const initialValueTarget = (atom) => {
|
|
87
93
|
let target = atom;
|
|
@@ -97,7 +103,9 @@ const initialValueTarget = (atom) => {
|
|
|
97
103
|
* set, so holds sharing one would all end with the first to let go.
|
|
98
104
|
*/
|
|
99
105
|
const holdNode = (registry, node) => {
|
|
100
|
-
const unsubscribe = node.subscribe(() =>
|
|
106
|
+
const unsubscribe = node.subscribe(() => {
|
|
107
|
+
// Only there to hold the node.
|
|
108
|
+
});
|
|
101
109
|
return () => {
|
|
102
110
|
unsubscribe();
|
|
103
111
|
if (node.canBeRemoved) {
|
|
@@ -119,7 +127,7 @@ const holdNode = (registry, node) => {
|
|
|
119
127
|
const dropInitialValueMark = (registry, atom) => {
|
|
120
128
|
let current = atom;
|
|
121
129
|
while (current) {
|
|
122
|
-
const node =
|
|
130
|
+
const node = nodeOf(registry, current);
|
|
123
131
|
if (node.currentState() === "valid") {
|
|
124
132
|
node.preserveInitialValueOnBuild = false;
|
|
125
133
|
}
|
|
@@ -141,7 +149,7 @@ const initialNodeOnServer = (registry, atom) => {
|
|
|
141
149
|
if (BROWSER) {
|
|
142
150
|
return undefined;
|
|
143
151
|
}
|
|
144
|
-
const node =
|
|
152
|
+
const node = nodeOf(registry, initialValueTarget(atom));
|
|
145
153
|
return node.preserveInitialValueOnBuild ? node : undefined;
|
|
146
154
|
};
|
|
147
155
|
/** Whether, on the server, an atom renders a value from `useAtomInitialValues`. */
|
|
@@ -149,7 +157,7 @@ const initialOnServer = (registry, atom) => initialNodeOnServer(registry, atom)
|
|
|
149
157
|
/** Keeps an atom for the server render: a node with an initial value is held, not mounted. */
|
|
150
158
|
const serverMount = (registry, atom) => {
|
|
151
159
|
const node = initialNodeOnServer(registry, atom);
|
|
152
|
-
return node ? holdNode(
|
|
160
|
+
return node ? holdNode(registry, node) : registry.mount(atom);
|
|
153
161
|
};
|
|
154
162
|
/** Reads an atom without building it on the server while it renders an initial value. */
|
|
155
163
|
const serverGet = (registry, atom) => {
|
|
@@ -183,31 +191,6 @@ const awaitSettled = (registry, atom, options, signal) => {
|
|
|
183
191
|
return Effect.sync(cancel);
|
|
184
192
|
}), { signal });
|
|
185
193
|
};
|
|
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
194
|
// On the server, per registry, how many renders' seeds carry each key, until those renders end.
|
|
212
195
|
const seededKeys = new WeakMap();
|
|
213
196
|
/** Whether a render using the registry sends the key's result to the browser with a seed. */
|
|
@@ -419,7 +402,7 @@ const subscribedReader = (registry, getAtom, plainRead) => {
|
|
|
419
402
|
// After mount, only a first build is dropped: a stale node rebuilt here, as after a failed
|
|
420
403
|
// rebuild, announces a change other reads of this reader have not seen. A node whose first
|
|
421
404
|
// build failed is still uninitialized, but the page has shown that failure, so it counts too.
|
|
422
|
-
const node =
|
|
405
|
+
const node = nodeOf(registry, current);
|
|
423
406
|
if (node.currentState() === "uninitialized" && !node.failed) {
|
|
424
407
|
reading = current;
|
|
425
408
|
}
|
|
@@ -461,7 +444,9 @@ export function useAtomValue(input, f) {
|
|
|
461
444
|
export const useAtom = (input) => {
|
|
462
445
|
const registry = getRegistry();
|
|
463
446
|
const getAtom = toGetter(input);
|
|
464
|
-
return new AtomCell(subscribedReader(registry, getAtom, "useAtom"), (value) =>
|
|
447
|
+
return new AtomCell(subscribedReader(registry, getAtom, "useAtom"), (value) => {
|
|
448
|
+
registry.set(getAtom(), value);
|
|
449
|
+
});
|
|
465
450
|
};
|
|
466
451
|
/** useAtomMount, telling an inspector scope why the atom is held: `useAtomSet` holds it to write. */
|
|
467
452
|
const mountWhileAlive = (input, kind) => {
|
|
@@ -505,9 +490,14 @@ export function useAtomSet(input, options) {
|
|
|
505
490
|
if (mode === "value") {
|
|
506
491
|
return (value) => {
|
|
507
492
|
const atom = getAtom();
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
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)));
|
|
511
501
|
};
|
|
512
502
|
}
|
|
513
503
|
return async (value, writeOptions) => {
|
|
@@ -516,21 +506,26 @@ export function useAtomSet(input, options) {
|
|
|
516
506
|
// never end.
|
|
517
507
|
throw new TypeError(`useAtomSet's ${mode} mode cannot wait for Atom.Reset; reset with a value mode setter`);
|
|
518
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
|
|
519
511
|
const atom = getAtom();
|
|
520
512
|
// An already aborted signal settles as interrupted without writing, as fetch does (JND-25). It
|
|
521
513
|
// skips the wait too: Effect checks the signal only after a first run, which would return a
|
|
522
514
|
// result the atom already holds from an earlier call.
|
|
523
515
|
let exit;
|
|
524
|
-
if (writeOptions?.signal?.aborted) {
|
|
516
|
+
if (writeOptions?.signal?.aborted === true) {
|
|
525
517
|
exit = Exit.interrupt();
|
|
526
518
|
}
|
|
527
519
|
else {
|
|
520
|
+
// oxlint-disable-next-line effecttsgo/global-date -- compared with an AsyncResult's timestamp, which Effect takes from Date.now
|
|
528
521
|
const start = Date.now();
|
|
529
522
|
registry.set(atom, value);
|
|
530
523
|
// A provider disposing of its registry ends the call without a last result, so the wait ends
|
|
531
524
|
// there too, as interrupted.
|
|
532
525
|
const controller = new AbortController();
|
|
533
|
-
const abort = () =>
|
|
526
|
+
const abort = () => {
|
|
527
|
+
controller.abort();
|
|
528
|
+
};
|
|
534
529
|
writeOptions?.signal?.addEventListener("abort", abort, { once: true });
|
|
535
530
|
const cancel = onDispose(registry, abort);
|
|
536
531
|
try {
|
|
@@ -565,7 +560,9 @@ export const useAtomRefresh = (input) => {
|
|
|
565
560
|
const registry = getRegistry();
|
|
566
561
|
const getAtom = toGetter(input);
|
|
567
562
|
useAtomMount(getAtom);
|
|
568
|
-
return () =>
|
|
563
|
+
return () => {
|
|
564
|
+
registry.refresh(getAtom());
|
|
565
|
+
};
|
|
569
566
|
};
|
|
570
567
|
/**
|
|
571
568
|
* Delivers each value to `f` as afterReads would, but in order and only while subscribed. Once a
|
|
@@ -575,12 +572,13 @@ export const useAtomRefresh = (input) => {
|
|
|
575
572
|
const orderedDelivery = (f) => {
|
|
576
573
|
let live = true;
|
|
577
574
|
let flushing = false;
|
|
575
|
+
// Boxed, so an empty queue is told apart from a queued undefined.
|
|
578
576
|
const queue = [];
|
|
579
577
|
// Stopping empties the queue, which ends a flush in progress.
|
|
580
578
|
const flush = () => {
|
|
581
579
|
try {
|
|
582
|
-
|
|
583
|
-
f(
|
|
580
|
+
for (let next = queue.shift(); next; next = queue.shift()) {
|
|
581
|
+
f(next.value);
|
|
584
582
|
}
|
|
585
583
|
}
|
|
586
584
|
finally {
|
|
@@ -596,7 +594,7 @@ const orderedDelivery = (f) => {
|
|
|
596
594
|
return;
|
|
597
595
|
}
|
|
598
596
|
if (flushing || activeReads > 0) {
|
|
599
|
-
queue.push(value);
|
|
597
|
+
queue.push({ value });
|
|
600
598
|
if (!flushing) {
|
|
601
599
|
flushing = true;
|
|
602
600
|
queueMicrotask(flush);
|
|
@@ -668,7 +666,7 @@ export const useAtomSubscribe = (input, f, options) => {
|
|
|
668
666
|
* atom's next read; this sets it on the node now, which is valid, so nothing reads it again.
|
|
669
667
|
*/
|
|
670
668
|
const setNodeValue = (registry, atom, value) => {
|
|
671
|
-
|
|
669
|
+
nodeOf(registry, atom).setValue(value);
|
|
672
670
|
dropInitialValueMark(registry, atom);
|
|
673
671
|
};
|
|
674
672
|
// The nodes given a value by useAtomInitialValues. A node belongs to one registry, and a node the
|
|
@@ -711,7 +709,7 @@ const serverHolders = new WeakMap();
|
|
|
711
709
|
* @category hooks
|
|
712
710
|
*/
|
|
713
711
|
export const useAtomInitialValues = (initialValues) => {
|
|
714
|
-
const registry =
|
|
712
|
+
const registry = getRegistry();
|
|
715
713
|
const releases = [];
|
|
716
714
|
// Registered before the loop: an entry that throws midway leaves the earlier ones held otherwise.
|
|
717
715
|
onTeardown(() => {
|
|
@@ -741,11 +739,12 @@ export const useAtomInitialValues = (initialValues) => {
|
|
|
741
739
|
releases.push(holdNode(registry, node));
|
|
742
740
|
}
|
|
743
741
|
};
|
|
742
|
+
const isWritableRef = (ref) => "set" in ref;
|
|
744
743
|
/** Writes a ref that has `set`; a read-only ref, as from `map`, throws as `useAtomValue` would. */
|
|
745
744
|
const writeRef = (getRef) => (value) => {
|
|
746
745
|
const ref = getRef();
|
|
747
|
-
if (!(
|
|
748
|
-
readOnly();
|
|
746
|
+
if (!isWritableRef(ref)) {
|
|
747
|
+
return readOnly();
|
|
749
748
|
}
|
|
750
749
|
ref.set(value);
|
|
751
750
|
};
|
|
@@ -836,7 +835,7 @@ const settledSeeds = new WeakSet();
|
|
|
836
835
|
* within a render, and a per-registry record would outlive the request when the caller owns the
|
|
837
836
|
* registry, so later requests would skip embedding their seed (JND-17).
|
|
838
837
|
*/
|
|
839
|
-
const seedOnServer = (registry, atom, key, encode, suspendOnWaiting) => {
|
|
838
|
+
const seedOnServer = (registry, atom, key, encode, suspendOnWaiting = false) => {
|
|
840
839
|
// Mounted until the render ends, so the settled node is what the render reads.
|
|
841
840
|
const release = serverMount(registry, atom);
|
|
842
841
|
// A render ends before its seeds settle only when it fails, as when a later reader throws during
|
|
@@ -855,7 +854,7 @@ const seedOnServer = (registry, atom, key, encode, suspendOnWaiting) => {
|
|
|
855
854
|
const claim = {};
|
|
856
855
|
const encoded = hydratable(key, () => {
|
|
857
856
|
claim.own = (async () => {
|
|
858
|
-
await
|
|
857
|
+
await Promise.resolve();
|
|
859
858
|
if (ended) {
|
|
860
859
|
return noSeed;
|
|
861
860
|
}
|
|
@@ -866,7 +865,7 @@ const seedOnServer = (registry, atom, key, encode, suspendOnWaiting) => {
|
|
|
866
865
|
// waits, so with suspendOnWaiting the seed is the settled result the render shows, not one the
|
|
867
866
|
// browser would run again because it was still waiting.
|
|
868
867
|
if (!initialOnServer(registry, atom) && !notStarted(registry.get(atom))) {
|
|
869
|
-
let settled = suspendOnWaiting
|
|
868
|
+
let settled = suspendOnWaiting;
|
|
870
869
|
await awaitResult(registry, atom, { suspendOnWaiting: settled });
|
|
871
870
|
// A later reader of the key may want the settled result once this one's wait has begun.
|
|
872
871
|
if (!settled && claim.first && settledSeeds.has(claim.first)) {
|
|
@@ -882,7 +881,7 @@ const seedOnServer = (registry, atom, key, encode, suspendOnWaiting) => {
|
|
|
882
881
|
return claim.own;
|
|
883
882
|
});
|
|
884
883
|
claim.first = encoded;
|
|
885
|
-
if (suspendOnWaiting
|
|
884
|
+
if (suspendOnWaiting) {
|
|
886
885
|
settledSeeds.add(encoded);
|
|
887
886
|
}
|
|
888
887
|
const claimed = serverSeeds.get(encoded);
|
|
@@ -905,7 +904,10 @@ const claimedKeys = new WeakMap();
|
|
|
905
904
|
// The store is not public API, so a Svelte that changes its shape or drops it turns the warning off
|
|
906
905
|
// rather than throwing.
|
|
907
906
|
const serverValues = () => {
|
|
908
|
-
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;
|
|
909
911
|
return store instanceof Map ? store : undefined;
|
|
910
912
|
};
|
|
911
913
|
const warnIfSent = async (key, sent) => {
|
|
@@ -957,7 +959,9 @@ const claimServerValue = (key, missed) => {
|
|
|
957
959
|
* serialization key. The atom is fetched again once seeded if any component using the key then
|
|
958
960
|
* asked to revalidate.
|
|
959
961
|
*/
|
|
960
|
-
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) => {
|
|
961
965
|
const atom = getAtom();
|
|
962
966
|
// The server never computes an atom with a server value, so it has no value to pass on, and the
|
|
963
967
|
// browser must not ask for one: hydratable throws for a key the server didn't write (JND-58).
|
|
@@ -1158,9 +1162,9 @@ const isPending = (current, options) => current._tag === "Initial" ||
|
|
|
1158
1162
|
/** What the promise resolves with, or the error it rejects with, for a result that is not pending. */
|
|
1159
1163
|
const fromSettled = (current, options) => {
|
|
1160
1164
|
if (current._tag === "Success") {
|
|
1161
|
-
return options.includeFailure ? current : current.value;
|
|
1165
|
+
return options.includeFailure === true ? current : current.value;
|
|
1162
1166
|
}
|
|
1163
|
-
if (current._tag === "Failure" &&
|
|
1167
|
+
if (current._tag === "Failure" && options.includeFailure !== true) {
|
|
1164
1168
|
throw Cause.squash(current.cause);
|
|
1165
1169
|
}
|
|
1166
1170
|
return current;
|
|
@@ -1178,6 +1182,7 @@ const fromServerValue = (current, options) => {
|
|
|
1178
1182
|
return Promise.resolve(fromSettled(current, options));
|
|
1179
1183
|
}
|
|
1180
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
|
|
1181
1186
|
return Promise.reject(error);
|
|
1182
1187
|
}
|
|
1183
1188
|
};
|
|
@@ -1276,7 +1281,9 @@ const sharedWait = (start, onAbandoned) => {
|
|
|
1276
1281
|
}
|
|
1277
1282
|
held.add(signal);
|
|
1278
1283
|
holders += 1;
|
|
1279
|
-
const listener = () =>
|
|
1284
|
+
const listener = () => {
|
|
1285
|
+
release(signal.reason);
|
|
1286
|
+
};
|
|
1280
1287
|
signal.addEventListener("abort", listener, { once: true });
|
|
1281
1288
|
listeners.push([signal, listener]);
|
|
1282
1289
|
},
|
|
@@ -1394,9 +1401,10 @@ export function useAtomSuspense(input, options = {}) {
|
|
|
1394
1401
|
// settles, as nothing could let go of it. Handled here, so a read nobody awaits isn't reported
|
|
1395
1402
|
// as unhandled.
|
|
1396
1403
|
const last = lastWaits.get(getAtom());
|
|
1397
|
-
if (last?.settled()) {
|
|
1404
|
+
if (last?.settled() === true) {
|
|
1398
1405
|
return last.promise;
|
|
1399
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
|
|
1400
1408
|
const gone = Promise.reject(holder.reason);
|
|
1401
1409
|
void (async () => {
|
|
1402
1410
|
try {
|
|
@@ -1422,7 +1430,7 @@ export function useAtomSuspense(input, options = {}) {
|
|
|
1422
1430
|
// reading would compute the atom for nobody.
|
|
1423
1431
|
const live = [...readers].filter((reader) => !reader.aborted);
|
|
1424
1432
|
if (live.length === 0) {
|
|
1425
|
-
return
|
|
1433
|
+
return;
|
|
1426
1434
|
}
|
|
1427
1435
|
// A read outside any reaction can't say when it is done, so it doesn't hold the atom: held
|
|
1428
1436
|
// by the component's lifetime, the atom would stay mounted after the getter moves on.
|
|
@@ -1437,7 +1445,7 @@ export function useAtomSuspense(input, options = {}) {
|
|
|
1437
1445
|
for (const reader of live) {
|
|
1438
1446
|
waited = settle(atom, current, reader);
|
|
1439
1447
|
}
|
|
1440
|
-
return waited;
|
|
1448
|
+
return await waited;
|
|
1441
1449
|
})();
|
|
1442
1450
|
pending = { promise, readers };
|
|
1443
1451
|
afterSeed.set(atom, pending);
|
|
@@ -1453,3 +1461,29 @@ export function useAtomSuspense(input, options = {}) {
|
|
|
1453
1461
|
return promise;
|
|
1454
1462
|
}, readOnly);
|
|
1455
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
|
+
}
|
|
@@ -56,11 +56,10 @@
|
|
|
56
56
|
const { children, state }: Props = $props();
|
|
57
57
|
const registry = getRegistry();
|
|
58
58
|
|
|
59
|
-
//
|
|
60
|
-
// 4.0.
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
};
|
|
59
|
+
// The encoded values Hydration.hydrate queued, until a lookup of the key takes one. Public since
|
|
60
|
+
// Effect 4.0.2; typed as possibly missing for a registry that isn't Effect's own.
|
|
61
|
+
const preloaded: Map<string, unknown> | undefined =
|
|
62
|
+
registry.preloadedSerializable;
|
|
64
63
|
// The values this boundary queued, by key, so it can drop the ones nobody took.
|
|
65
64
|
const queued = new Map<string, unknown>();
|
|
66
65
|
// Whether the boundary has ended, as at the end of the server render or once the browser destroys
|
package/dist/Inspector.js
CHANGED
|
@@ -86,10 +86,10 @@ const install = (registry) => {
|
|
|
86
86
|
// finalizer that interrupts the fiber.
|
|
87
87
|
// A wrapper that passes on another atom's result (debounce, makeRefreshOnSignal) holds a
|
|
88
88
|
// value that atom made, and has no fiber of its own. A runtime's fiber isn't interrupted.
|
|
89
|
-
|
|
89
|
+
const ownWait = AsyncResult.isAsyncResult(value) &&
|
|
90
90
|
value.waiting &&
|
|
91
|
-
(origins.get(value) ?? node) === node
|
|
92
|
-
|
|
91
|
+
(origins.get(value) ?? node) === node;
|
|
92
|
+
if (ownWait && !isRuntime(node.atom)) {
|
|
93
93
|
emit({ _tag: "Interrupted", node, time: performance.now() });
|
|
94
94
|
}
|
|
95
95
|
emit({ _tag: "Finalized", finalizers, node, time: performance.now() });
|
|
@@ -145,8 +145,11 @@ const install = (registry) => {
|
|
|
145
145
|
});
|
|
146
146
|
complete &&= instrumented;
|
|
147
147
|
};
|
|
148
|
-
const
|
|
149
|
-
const
|
|
148
|
+
const { onNodeAdded, onNodeRemoved } = registry;
|
|
149
|
+
const modify = registry.modify.bind(registry);
|
|
150
|
+
const refresh = registry.refresh.bind(registry);
|
|
151
|
+
const set = registry.set.bind(registry);
|
|
152
|
+
const update = registry.update.bind(registry);
|
|
150
153
|
const writes = (f) => {
|
|
151
154
|
writing += 1;
|
|
152
155
|
try {
|
|
@@ -156,33 +159,45 @@ const install = (registry) => {
|
|
|
156
159
|
writing -= 1;
|
|
157
160
|
}
|
|
158
161
|
};
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
record(node).refreshed = true;
|
|
168
|
-
}
|
|
169
|
-
try {
|
|
170
|
-
refresh.call(registry, atom);
|
|
171
|
-
}
|
|
172
|
-
finally {
|
|
173
|
-
// Still valid: the refresh didn't reach it (what it wraps computed an equal value), or it
|
|
174
|
-
// computed already. Either way the mark isn't the cause of a later computation.
|
|
175
|
-
if (node?.currentState() === "valid") {
|
|
176
|
-
record(node).refreshed = false;
|
|
162
|
+
const patched = {
|
|
163
|
+
modify: (atom, f) => writes(() => modify(atom, f)),
|
|
164
|
+
// An atom with its own refresh (a wrapper such as withRefresh) calls the registry's refresh for
|
|
165
|
+
// the atoms it wraps, through this replacement, so each of those is marked too.
|
|
166
|
+
refresh: (atom) => {
|
|
167
|
+
const node = registry.getNodes().get(nodeKey(atom));
|
|
168
|
+
if (node !== undefined) {
|
|
169
|
+
record(node).refreshed = true;
|
|
177
170
|
}
|
|
178
|
-
|
|
171
|
+
try {
|
|
172
|
+
refresh(atom);
|
|
173
|
+
}
|
|
174
|
+
finally {
|
|
175
|
+
// Still valid: the refresh didn't reach it (what it wraps computed an equal value), or it
|
|
176
|
+
// computed already. Either way the mark isn't the cause of a later computation.
|
|
177
|
+
if (node?.currentState() === "valid") {
|
|
178
|
+
record(node).refreshed = false;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
set: (atom, value) => {
|
|
183
|
+
writes(() => {
|
|
184
|
+
set(atom, value);
|
|
185
|
+
});
|
|
186
|
+
},
|
|
187
|
+
update: (atom, f) => {
|
|
188
|
+
writes(() => {
|
|
189
|
+
update(atom, f);
|
|
190
|
+
});
|
|
191
|
+
},
|
|
179
192
|
};
|
|
180
|
-
|
|
193
|
+
// Readonly in the interface: replaced on this registry only, as its nodes are.
|
|
194
|
+
Object.assign(registry, patched);
|
|
195
|
+
registry.onNodeAdded = (node) => {
|
|
181
196
|
onNodeAdded?.(node);
|
|
182
197
|
instrument(node);
|
|
183
198
|
emit({ _tag: "NodeAdded", node, time: performance.now() });
|
|
184
199
|
};
|
|
185
|
-
|
|
200
|
+
registry.onNodeRemoved = (node) => {
|
|
186
201
|
onNodeRemoved?.(node);
|
|
187
202
|
// An idle node is announced before it is removed; its teardown is reported first.
|
|
188
203
|
if (node.currentState() !== "removed" && records.has(node)) {
|
|
@@ -207,7 +222,7 @@ const install = (registry) => {
|
|
|
207
222
|
idleTTL: (atom) => {
|
|
208
223
|
// An idle TTL of 0 is none: the registry removes the atom as soon as nothing reads it.
|
|
209
224
|
if (atom.keepAlive || atom.idleTTL === 0) {
|
|
210
|
-
return
|
|
225
|
+
return;
|
|
211
226
|
}
|
|
212
227
|
return atom.idleTTL ?? defaultIdleTTL(registry);
|
|
213
228
|
},
|
|
@@ -284,7 +299,9 @@ const emptySnapshot = { edges: [], nodes: [], readers: [] };
|
|
|
284
299
|
/** The scope on the server: always empty. */
|
|
285
300
|
const inertScope = {
|
|
286
301
|
snapshot: () => emptySnapshot,
|
|
287
|
-
subscribe: () => () =>
|
|
302
|
+
subscribe: () => () => {
|
|
303
|
+
// Nothing to unsubscribe from.
|
|
304
|
+
},
|
|
288
305
|
};
|
|
289
306
|
const stateOf = (node) => {
|
|
290
307
|
// A valid node's value is read without computing anything.
|
|
@@ -342,9 +359,7 @@ class Scope {
|
|
|
342
359
|
if (this.#listeners.size === 0) {
|
|
343
360
|
return this.#walk();
|
|
344
361
|
}
|
|
345
|
-
|
|
346
|
-
this.#members = this.#walk();
|
|
347
|
-
}
|
|
362
|
+
this.#members ??= this.#walk();
|
|
348
363
|
return this.#members;
|
|
349
364
|
}
|
|
350
365
|
/** Works out the scope's nodes from the graph as it is now, without keeping them. */
|
|
@@ -445,7 +460,9 @@ class Scope {
|
|
|
445
460
|
if (this.#listeners.size === 0 || this.#following.has(registry)) {
|
|
446
461
|
return;
|
|
447
462
|
}
|
|
448
|
-
this.#following.set(registry, inspect(registry).subscribe((event) =>
|
|
463
|
+
this.#following.set(registry, inspect(registry).subscribe((event) => {
|
|
464
|
+
this.#receive(event);
|
|
465
|
+
}));
|
|
449
466
|
}
|
|
450
467
|
#receive(event) {
|
|
451
468
|
// Events arrive in the middle of the registry's work, while a computation is still changing
|
package/dist/RegistryContext.js
CHANGED
|
@@ -91,7 +91,9 @@ export const provideRegistry = (options = {}) => {
|
|
|
91
91
|
}
|
|
92
92
|
// Not onDestroy: in the browser it never runs for a component destroyed while its script awaits.
|
|
93
93
|
if (!provided) {
|
|
94
|
-
onTeardownAfterChildren(() =>
|
|
94
|
+
onTeardownAfterChildren(() => {
|
|
95
|
+
disposeRegistry(registry);
|
|
96
|
+
});
|
|
95
97
|
}
|
|
96
98
|
if (DEV && BROWSER) {
|
|
97
99
|
// For developer tools to find (effect-atom-svelte/inspector).
|