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/ScopedAtom.js
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
import { createContext } from "svelte";
|
|
2
|
-
// A const and a type of the same name, as in Effect's modules.
|
|
3
|
-
/* oxlint-disable eslint/no-redeclare */
|
|
4
2
|
/**
|
|
5
3
|
* Marks a value as a `ScopedAtom`: every scoped atom has this key, set to this same string, as
|
|
6
4
|
* Effect's own types carry a type id.
|
|
@@ -13,10 +11,11 @@ export const TypeId = "~effect-atom-svelte/ScopedAtom";
|
|
|
13
11
|
/**
|
|
14
12
|
* Creates a scoped atom from a factory, which runs once per providing component.
|
|
15
13
|
*
|
|
16
|
-
* Each provider gets its own atom, so a factory that
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
14
|
+
* Each provider gets its own atom, so a factory that makes a serializable atom gives every copy of
|
|
15
|
+
* one input the same key, and two such providers on a server-rendered page make the render throw:
|
|
16
|
+
* two different atoms share the serialization key. For a value from the server, return a family's
|
|
17
|
+
* atom instead, `ScopedAtom.make((id: string) => userAtom(id))`, so every provider of one input
|
|
18
|
+
* holds the same atom.
|
|
20
19
|
*
|
|
21
20
|
* **Example** (A counter per subtree, started from an input)
|
|
22
21
|
*
|
|
@@ -36,17 +35,27 @@ export const TypeId = "~effect-atom-svelte/ScopedAtom";
|
|
|
36
35
|
*/
|
|
37
36
|
export const make = (f, options) => {
|
|
38
37
|
const [get, set, has] = createContext();
|
|
39
|
-
const provide = (...args) =>
|
|
40
|
-
|
|
41
|
-
|
|
38
|
+
const provide = (...args) => {
|
|
39
|
+
if (args.length === 1) {
|
|
40
|
+
return set(f(args[0]));
|
|
41
|
+
}
|
|
42
|
+
// SAFETY: `provide`'s type allows no input only when `Input` is never or includes undefined, so
|
|
43
|
+
// `f` is the factory without input or one whose input is optional.
|
|
44
|
+
// oxlint-disable-next-line typescript/no-unsafe-type-assertion -- the union of factories can't be narrowed by the argument count
|
|
45
|
+
return set(f());
|
|
46
|
+
};
|
|
42
47
|
return {
|
|
43
48
|
[TypeId]: TypeId,
|
|
49
|
+
// SAFETY: `provide` takes no input or one, which every branch of the conditional type accepts;
|
|
50
|
+
// TypeScript can't pick a branch while `Input` is generic.
|
|
51
|
+
// oxlint-disable-next-line typescript/no-unsafe-type-assertion -- the conditional type can't be narrowed for a generic `Input`
|
|
44
52
|
provide: provide,
|
|
45
53
|
use: () => {
|
|
46
54
|
if (!has()) {
|
|
47
55
|
const name = options?.name;
|
|
48
|
-
const
|
|
49
|
-
const
|
|
56
|
+
const named = name !== undefined && name !== "";
|
|
57
|
+
const which = named ? `ScopedAtom "${name}"` : "ScopedAtom";
|
|
58
|
+
const call = named ? `${name}.provide()` : "its provide()";
|
|
50
59
|
throw new Error(`${which} used outside of the component that provides it. Call ${call} in a parent component's script.`);
|
|
51
60
|
}
|
|
52
61
|
return get();
|
package/dist/SvelteKit.d.ts
CHANGED
|
@@ -1,13 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* SvelteKit `handleError` hooks that keep an Effect error's `_tag`, so a `<svelte:boundary>`
|
|
3
|
-
* `failed` snippet can tell typed errors apart. Import them from `effect-atom-svelte/sveltekit`.
|
|
4
|
-
*
|
|
5
|
-
* The module does not import SvelteKit: the hooks take the fields of SvelteKit's input they read
|
|
6
|
-
* and return the fields they set, so they type-check against `HandleClientError` and
|
|
7
|
-
* `HandleServerError` once `App.Error` declares `tag?: string`.
|
|
8
|
-
*
|
|
9
|
-
* @since 0.1.0
|
|
10
|
-
*/
|
|
11
1
|
/**
|
|
12
2
|
* The part of SvelteKit's `handleError` input that the hooks read. In SvelteKit 3, `kind` is
|
|
13
3
|
* `"unknown"` for errors thrown by your code, `"app"` for `error(...)`, `"framework"` for
|
|
@@ -38,6 +28,27 @@ export interface EffectErrorBody {
|
|
|
38
28
|
readonly message?: string;
|
|
39
29
|
readonly tag?: string;
|
|
40
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* SvelteKit 2's `handleError` input: no `kind`, and always a message, which the hooks fall back on.
|
|
33
|
+
*
|
|
34
|
+
* @stability unstable
|
|
35
|
+
* @since 0.2.1
|
|
36
|
+
* @category models
|
|
37
|
+
*/
|
|
38
|
+
export interface CaughtErrorWithoutKind extends CaughtError {
|
|
39
|
+
readonly kind?: undefined;
|
|
40
|
+
readonly message: string;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* What the hooks return for SvelteKit 2's input: always a message, as its `App.Error` requires one.
|
|
44
|
+
*
|
|
45
|
+
* @stability unstable
|
|
46
|
+
* @since 0.2.1
|
|
47
|
+
* @category models
|
|
48
|
+
*/
|
|
49
|
+
export interface EffectErrorBodyWithMessage extends EffectErrorBody {
|
|
50
|
+
readonly message: string;
|
|
51
|
+
}
|
|
41
52
|
/**
|
|
42
53
|
* A client `handleError` hook (`src/hooks.client.ts`) that keeps the message and `_tag` of errors
|
|
43
54
|
* thrown by your code, so a boundary's `failed` snippet shows them instead of
|
|
@@ -56,7 +67,8 @@ export interface EffectErrorBody {
|
|
|
56
67
|
* @since 0.1.0
|
|
57
68
|
* @category hooks
|
|
58
69
|
*/
|
|
59
|
-
export declare
|
|
70
|
+
export declare function handleClientError(input: CaughtErrorWithoutKind): EffectErrorBodyWithMessage;
|
|
71
|
+
export declare function handleClientError(input: CaughtError): EffectErrorBody | undefined;
|
|
60
72
|
/**
|
|
61
73
|
* A server `handleError` hook (`src/hooks.server.ts`) that keeps the `_tag` of errors thrown by
|
|
62
74
|
* your code but not their message, which could expose details of the server to users: the
|
|
@@ -75,4 +87,51 @@ export declare const handleClientError: (input: CaughtError) => EffectErrorBody
|
|
|
75
87
|
* @since 0.1.0
|
|
76
88
|
* @category hooks
|
|
77
89
|
*/
|
|
78
|
-
export declare
|
|
90
|
+
export declare function handleServerError(input: CaughtErrorWithoutKind): EffectErrorBodyWithMessage;
|
|
91
|
+
export declare function handleServerError(input: CaughtError): EffectErrorBody | undefined;
|
|
92
|
+
/**
|
|
93
|
+
* Options for `useUnsavedChangesGuard`.
|
|
94
|
+
*
|
|
95
|
+
* @stability unstable
|
|
96
|
+
* @since 0.4.0
|
|
97
|
+
* @category models
|
|
98
|
+
*/
|
|
99
|
+
export interface UnsavedChangesGuardOptions {
|
|
100
|
+
/**
|
|
101
|
+
* Asks whether to leave and lose the changes, resolving `true` to leave. Called for links,
|
|
102
|
+
* `goto`, form navigations and the back and forward buttons. Closing the tab, reloading and
|
|
103
|
+
* typing a URL show the browser's own dialog instead, as a page can't show its own then.
|
|
104
|
+
*/
|
|
105
|
+
readonly confirm: () => boolean | Promise<boolean>;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Asks before the user navigates away from a page whose form is dirty, using SvelteKit's
|
|
109
|
+
* `beforeNavigate`. Call it while a component initializes, with any object that has `dirty`, such
|
|
110
|
+
* as a form instance from `useForm`.
|
|
111
|
+
*
|
|
112
|
+
* While the form is dirty, a navigation is cancelled and `confirm` asked; on yes it is retried, and
|
|
113
|
+
* the retry goes through. Closing the tab, reloading or typing a URL show the browser's own dialog
|
|
114
|
+
* instead. SvelteKit doesn't call `beforeNavigate` for back and forward over shallow history entries
|
|
115
|
+
* (from `pushState` or a hash change) or for links to a hash on the same page, so those aren't
|
|
116
|
+
* guarded.
|
|
117
|
+
*
|
|
118
|
+
* **Example** (Asking before leaving an edit page)
|
|
119
|
+
*
|
|
120
|
+
* ```ts
|
|
121
|
+
* import { useForm } from "effect-atom-svelte";
|
|
122
|
+
* import { useUnsavedChangesGuard } from "effect-atom-svelte/sveltekit";
|
|
123
|
+
*
|
|
124
|
+
* const form = useForm(EditProject, { id, initialValues, submit });
|
|
125
|
+
* useUnsavedChangesGuard(form, {
|
|
126
|
+
* confirm: () => window.confirm("Leave without saving your changes?"),
|
|
127
|
+
* });
|
|
128
|
+
* ```
|
|
129
|
+
*
|
|
130
|
+
* @see {@link https://atom.jarrednorris.dev/forms | Forms}
|
|
131
|
+
* @stability unstable
|
|
132
|
+
* @since 0.4.0
|
|
133
|
+
* @category forms
|
|
134
|
+
*/
|
|
135
|
+
export declare const useUnsavedChangesGuard: (form: {
|
|
136
|
+
readonly dirty: boolean;
|
|
137
|
+
}, options: UnsavedChangesGuardOptions) => void;
|
package/dist/SvelteKit.js
CHANGED
|
@@ -1,19 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* SvelteKit
|
|
3
|
-
* `failed` snippet can tell typed errors apart. Import them from `effect-atom-svelte/sveltekit`.
|
|
2
|
+
* SvelteKit helpers. Import them from `effect-atom-svelte/sveltekit`.
|
|
4
3
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* `handleClientError` and `handleServerError` are `handleError` hooks that keep an Effect error's
|
|
5
|
+
* `_tag`, so a `<svelte:boundary>` `failed` snippet can tell typed errors apart. They take the
|
|
6
|
+
* fields of SvelteKit's input they read and return the fields they set, so they type-check against
|
|
7
|
+
* `HandleClientError` and `HandleServerError` once `App.Error` declares `tag?: string`. In
|
|
8
|
+
* SvelteKit 2, whose `App.Error` requires a message, they are typed to return one when there is no
|
|
9
|
+
* `kind`, as they then always set one.
|
|
10
|
+
*
|
|
11
|
+
* `useUnsavedChangesGuard` asks before leaving a page with a dirty form.
|
|
8
12
|
*
|
|
9
13
|
* @since 0.1.0
|
|
10
14
|
*/
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
/// <reference types="@sveltejs/kit" />
|
|
16
|
+
import { beforeNavigate, goto } from "$app/navigation";
|
|
17
|
+
import { Effect } from "effect";
|
|
18
|
+
const tagOf = (error) => {
|
|
19
|
+
if (typeof error !== "object" || error === null || !("_tag" in error)) {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
return typeof error._tag === "string" ? error._tag : undefined;
|
|
23
|
+
};
|
|
17
24
|
const messageOf = (error) => {
|
|
18
25
|
if (error instanceof Error) {
|
|
19
26
|
return error.message;
|
|
@@ -26,57 +33,29 @@ const messageOf = (error) => {
|
|
|
26
33
|
const fallbackMessage = (input) => input.kind === undefined ? input.message : undefined;
|
|
27
34
|
// Leaves out empty fields, so SvelteKit's defaults apply, and returns nothing when both are empty.
|
|
28
35
|
const body = (message, tag) => {
|
|
29
|
-
|
|
30
|
-
|
|
36
|
+
const fields = {};
|
|
37
|
+
if (message !== undefined && message !== "") {
|
|
38
|
+
fields.message = message;
|
|
31
39
|
}
|
|
32
|
-
|
|
40
|
+
if (tag !== undefined && tag !== "") {
|
|
41
|
+
fields.tag = tag;
|
|
42
|
+
}
|
|
43
|
+
return fields.message === undefined && fields.tag === undefined
|
|
44
|
+
? undefined
|
|
45
|
+
: fields;
|
|
33
46
|
};
|
|
34
|
-
|
|
35
|
-
* A client `handleError` hook (`src/hooks.client.ts`) that keeps the message and `_tag` of errors
|
|
36
|
-
* thrown by your code, so a boundary's `failed` snippet shows them instead of
|
|
37
|
-
* `{ status: 500, message: "Internal Error" }`. It logs those errors like SvelteKit's default
|
|
38
|
-
* hook, and leaves `error(...)` and SvelteKit's own errors as they are. Made for SvelteKit 3:
|
|
39
|
-
* SvelteKit 2 doesn't say which errors are its own, so there every error is kept and logged.
|
|
40
|
-
*
|
|
41
|
-
* **Example** (Using it as the client hook)
|
|
42
|
-
*
|
|
43
|
-
* ```ts
|
|
44
|
-
* // src/hooks.client.ts
|
|
45
|
-
* export { handleClientError as handleError } from "effect-atom-svelte/sveltekit";
|
|
46
|
-
* ```
|
|
47
|
-
*
|
|
48
|
-
* @stability unstable
|
|
49
|
-
* @since 0.1.0
|
|
50
|
-
* @category hooks
|
|
51
|
-
*/
|
|
52
|
-
export const handleClientError = (input) => {
|
|
47
|
+
export function handleClientError(input) {
|
|
53
48
|
// Destructured here, not in the parameter list, so the API reference shows a named parameter.
|
|
54
49
|
const { error, kind = "unknown" } = input;
|
|
55
50
|
if (kind !== "unknown") {
|
|
56
51
|
return undefined;
|
|
57
52
|
}
|
|
58
53
|
console.error(error);
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
* message stays SvelteKit's `"Internal Error"`. It logs errors with `console.error`.
|
|
65
|
-
* Server errors reach a boundary's `failed` snippet when it renders on the server. Made for
|
|
66
|
-
* SvelteKit 3: SvelteKit 2 doesn't say which errors are its own, so there every error is logged.
|
|
67
|
-
*
|
|
68
|
-
* **Example** (Using it as the server hook)
|
|
69
|
-
*
|
|
70
|
-
* ```ts
|
|
71
|
-
* // src/hooks.server.ts
|
|
72
|
-
* export { handleServerError as handleError } from "effect-atom-svelte/sveltekit";
|
|
73
|
-
* ```
|
|
74
|
-
*
|
|
75
|
-
* @stability unstable
|
|
76
|
-
* @since 0.1.0
|
|
77
|
-
* @category hooks
|
|
78
|
-
*/
|
|
79
|
-
export const handleServerError = (input) => {
|
|
54
|
+
// An error with an empty message gets SvelteKit 2's instead.
|
|
55
|
+
const message = messageOf(error);
|
|
56
|
+
return body(message === undefined || message === "" ? fallbackMessage(input) : message, tagOf(error));
|
|
57
|
+
}
|
|
58
|
+
export function handleServerError(input) {
|
|
80
59
|
const { error, issues, kind = "unknown" } = input;
|
|
81
60
|
if (kind === "validation") {
|
|
82
61
|
console.error("Remote function schema validation failed:", issues);
|
|
@@ -87,4 +66,104 @@ export const handleServerError = (input) => {
|
|
|
87
66
|
}
|
|
88
67
|
console.error(error);
|
|
89
68
|
return body(fallbackMessage(input), tagOf(error));
|
|
69
|
+
}
|
|
70
|
+
// Waits for the next popstate, or a second, should none come.
|
|
71
|
+
const nextPopState = () => Effect.runPromise(Effect.callback((resume) => {
|
|
72
|
+
const done = () => {
|
|
73
|
+
resume(Effect.undefined);
|
|
74
|
+
};
|
|
75
|
+
addEventListener("popstate", done, { once: true });
|
|
76
|
+
return Effect.sync(() => {
|
|
77
|
+
removeEventListener("popstate", done);
|
|
78
|
+
});
|
|
79
|
+
}).pipe(Effect.timeoutOption("1 second")));
|
|
80
|
+
// How long a "yes" lets the retried navigation through. The retry reaches beforeNavigate at once;
|
|
81
|
+
// a navigation that never does, such as a link to a download, doesn't leave a bypass behind.
|
|
82
|
+
const bypassFor = 2000;
|
|
83
|
+
/**
|
|
84
|
+
* Asks before the user navigates away from a page whose form is dirty, using SvelteKit's
|
|
85
|
+
* `beforeNavigate`. Call it while a component initializes, with any object that has `dirty`, such
|
|
86
|
+
* as a form instance from `useForm`.
|
|
87
|
+
*
|
|
88
|
+
* While the form is dirty, a navigation is cancelled and `confirm` asked; on yes it is retried, and
|
|
89
|
+
* the retry goes through. Closing the tab, reloading or typing a URL show the browser's own dialog
|
|
90
|
+
* instead. SvelteKit doesn't call `beforeNavigate` for back and forward over shallow history entries
|
|
91
|
+
* (from `pushState` or a hash change) or for links to a hash on the same page, so those aren't
|
|
92
|
+
* guarded.
|
|
93
|
+
*
|
|
94
|
+
* **Example** (Asking before leaving an edit page)
|
|
95
|
+
*
|
|
96
|
+
* ```ts
|
|
97
|
+
* import { useForm } from "effect-atom-svelte";
|
|
98
|
+
* import { useUnsavedChangesGuard } from "effect-atom-svelte/sveltekit";
|
|
99
|
+
*
|
|
100
|
+
* const form = useForm(EditProject, { id, initialValues, submit });
|
|
101
|
+
* useUnsavedChangesGuard(form, {
|
|
102
|
+
* confirm: () => window.confirm("Leave without saving your changes?"),
|
|
103
|
+
* });
|
|
104
|
+
* ```
|
|
105
|
+
*
|
|
106
|
+
* @see {@link https://atom.jarrednorris.dev/forms | Forms}
|
|
107
|
+
* @stability unstable
|
|
108
|
+
* @since 0.4.0
|
|
109
|
+
* @category forms
|
|
110
|
+
*/
|
|
111
|
+
export const useUnsavedChangesGuard = (form, options) => {
|
|
112
|
+
// The retry of a navigation the user agreed to: where it goes, whether it unloads the page, and
|
|
113
|
+
// until when to expect it.
|
|
114
|
+
let retry;
|
|
115
|
+
beforeNavigate((navigation) => {
|
|
116
|
+
// Let the retry through, once. Any other navigation is asked about as usual.
|
|
117
|
+
if (retry !== undefined &&
|
|
118
|
+
// oxlint-disable-next-line effecttsgo/global-date -- SvelteKit calls beforeNavigate synchronously, outside any Effect program
|
|
119
|
+
retry.until > Date.now() &&
|
|
120
|
+
(navigation.type === "leave"
|
|
121
|
+
? retry.unloads
|
|
122
|
+
: navigation.to?.url.href === retry.href)) {
|
|
123
|
+
retry = undefined;
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
if (!form.dirty) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
// Closing the tab, reloading or typing a URL: cancelling shows the browser's dialog, and no
|
|
130
|
+
// code of ours can run before the page goes.
|
|
131
|
+
navigation.cancel();
|
|
132
|
+
const { to } = navigation;
|
|
133
|
+
if (navigation.type === "leave" || to === null) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
// Cancelling back or forward makes SvelteKit undo the move with history.go, which finishes with
|
|
137
|
+
// a popstate. A retry before then would be lost to it, so the retry waits for it.
|
|
138
|
+
const undone = navigation.type === "popstate" ? nextPopState() : undefined;
|
|
139
|
+
// oxlint-disable-next-line effecttsgo/async-function -- awaits the app's confirm, which may return a Promise, from SvelteKit's beforeNavigate, outside any Effect program
|
|
140
|
+
void (async () => {
|
|
141
|
+
if (!(await options.confirm())) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
retry = {
|
|
145
|
+
href: to.url.href,
|
|
146
|
+
unloads: navigation.type !== "popstate" && navigation.willUnload,
|
|
147
|
+
// oxlint-disable-next-line effecttsgo/global-date -- compared with Date.now in beforeNavigate, outside any Effect program
|
|
148
|
+
until: Date.now() + bypassFor,
|
|
149
|
+
};
|
|
150
|
+
if (navigation.type === "popstate") {
|
|
151
|
+
await undone;
|
|
152
|
+
history.go(navigation.delta);
|
|
153
|
+
}
|
|
154
|
+
else if (navigation.willUnload) {
|
|
155
|
+
// An external link, or one SvelteKit hands to the browser: goto can't follow it. The page
|
|
156
|
+
// unloading fires a "leave", which the bypass lets through.
|
|
157
|
+
location.href = to.url.href;
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
// SvelteKit 3 warns in dev whenever the `replaceState` key is there, even false, so it is
|
|
161
|
+
// passed only when true.
|
|
162
|
+
await goto(to.url, to.url.href === location.href
|
|
163
|
+
? // oxlint-disable-next-line typescript/no-deprecated -- the peer range includes SvelteKit 2, whose goto has no `replace`, only `replaceState`
|
|
164
|
+
{ replaceState: true }
|
|
165
|
+
: {});
|
|
166
|
+
}
|
|
167
|
+
})();
|
|
168
|
+
});
|
|
90
169
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -40,6 +40,15 @@ export { default as RegistryProvider } from "./RegistryProvider.svelte";
|
|
|
40
40
|
* @category scoped atoms
|
|
41
41
|
*/
|
|
42
42
|
export * as ScopedAtom from "./ScopedAtom.ts";
|
|
43
|
+
/**
|
|
44
|
+
* Forms driven by one Effect Schema: `Form.make` defines one, `useForm` creates an instance in a
|
|
45
|
+
* component.
|
|
46
|
+
*
|
|
47
|
+
* @stability unstable
|
|
48
|
+
* @since 0.4.0
|
|
49
|
+
* @category forms
|
|
50
|
+
*/
|
|
51
|
+
export * as Form from "./Form.ts";
|
|
43
52
|
/**
|
|
44
53
|
* Effect Atom's modules, so an app can import everything from one place.
|
|
45
54
|
*
|
|
@@ -47,4 +56,4 @@ export * as ScopedAtom from "./ScopedAtom.ts";
|
|
|
47
56
|
* @since 0.1.0
|
|
48
57
|
* @category re-exports
|
|
49
58
|
*/
|
|
50
|
-
export { AsyncResult, Atom, AtomHttpApi, AtomRef, AtomRegistry, AtomRpc, Hydration, } from "effect/reactivity";
|
|
59
|
+
export { AsyncResult, Atom, AtomHttpApi, AtomRef, AtomRegistry, AtomRpc, Hydration, Reactivity, } from "effect/reactivity";
|
package/dist/index.js
CHANGED
|
@@ -40,6 +40,15 @@ export { default as RegistryProvider } from "./RegistryProvider.svelte";
|
|
|
40
40
|
* @category scoped atoms
|
|
41
41
|
*/
|
|
42
42
|
export * as ScopedAtom from "./ScopedAtom.js";
|
|
43
|
+
/**
|
|
44
|
+
* Forms driven by one Effect Schema: `Form.make` defines one, `useForm` creates an instance in a
|
|
45
|
+
* component.
|
|
46
|
+
*
|
|
47
|
+
* @stability unstable
|
|
48
|
+
* @since 0.4.0
|
|
49
|
+
* @category forms
|
|
50
|
+
*/
|
|
51
|
+
export * as Form from "./Form.js";
|
|
43
52
|
/**
|
|
44
53
|
* Effect Atom's modules, so an app can import everything from one place.
|
|
45
54
|
*
|
|
@@ -47,4 +56,4 @@ export * as ScopedAtom from "./ScopedAtom.js";
|
|
|
47
56
|
* @since 0.1.0
|
|
48
57
|
* @category re-exports
|
|
49
58
|
*/
|
|
50
|
-
export { AsyncResult, Atom, AtomHttpApi, AtomRef, AtomRegistry, AtomRpc, Hydration, } from "effect/reactivity";
|
|
59
|
+
export { AsyncResult, Atom, AtomHttpApi, AtomRef, AtomRegistry, AtomRpc, Hydration, Reactivity, } from "effect/reactivity";
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Exit } from "effect";
|
|
2
|
+
import type { Cause } from "effect";
|
|
3
|
+
import { AsyncResult } from "effect/reactivity";
|
|
4
|
+
import type { Atom, AtomRegistry } from "effect/reactivity";
|
|
5
|
+
/**
|
|
6
|
+
* A promise-mode setter's wait: as `awaitResult` with `suspendOnWaiting`, but a change to a result
|
|
7
|
+
* from before the call, which is what `Atom.Reset` from another setter leaves (an idle `Initial`,
|
|
8
|
+
* or an `Atom.fn`'s `initialValue`), ends it as interrupted. The reset interrupts the call, so
|
|
9
|
+
* otherwise the wait would hang until the next call, or settle with the initial value.
|
|
10
|
+
*/
|
|
11
|
+
export declare const awaitWrite: <A, E>(registry: AtomRegistry.AtomRegistry, atom: Atom.Atom<AsyncResult.AsyncResult<A, E>>, start: number, signal: AbortSignal) => Promise<Exit.Exit<A, E | Cause.NoSuchElementError>>;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Waiting for a write to an async atom to settle, for useAtomSet's promise modes and a form's
|
|
2
|
+
// submit. Internal: not exported from the package.
|
|
3
|
+
import { Effect, Exit } from "effect";
|
|
4
|
+
import { AsyncResult } from "effect/reactivity";
|
|
5
|
+
/**
|
|
6
|
+
* A promise-mode setter's wait: as `awaitResult` with `suspendOnWaiting`, but a change to a result
|
|
7
|
+
* from before the call, which is what `Atom.Reset` from another setter leaves (an idle `Initial`,
|
|
8
|
+
* or an `Atom.fn`'s `initialValue`), ends it as interrupted. The reset interrupts the call, so
|
|
9
|
+
* otherwise the wait would hang until the next call, or settle with the initial value.
|
|
10
|
+
*/
|
|
11
|
+
export const awaitWrite = (registry, atom, start, signal) => Effect.runPromiseExit(Effect.callback((resume) => {
|
|
12
|
+
const first = registry.get(atom);
|
|
13
|
+
if (first._tag !== "Initial" && !first.waiting) {
|
|
14
|
+
resume(AsyncResult.toExit(first));
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const cancel = registry.subscribe(atom, (result) => {
|
|
18
|
+
if (result.waiting) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
cancel();
|
|
22
|
+
const reset = result._tag === "Initial" ||
|
|
23
|
+
(result._tag === "Success" && result.timestamp < start);
|
|
24
|
+
resume(reset ? Exit.interrupt() : AsyncResult.toExit(result));
|
|
25
|
+
});
|
|
26
|
+
return Effect.sync(cancel);
|
|
27
|
+
}), { signal });
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { AtomRegistry } from "effect/reactivity";
|
|
2
|
+
/**
|
|
3
|
+
* Per registry, key and queued value, how many live boundaries queued it. Boundaries given the
|
|
4
|
+
* same state, as a layout and its page might be, queue the same values, and one destroyed first
|
|
5
|
+
* must leave them for the other's children.
|
|
6
|
+
*/
|
|
7
|
+
export declare const queuedBy: WeakMap<AtomRegistry.AtomRegistry, Map<string, Map<unknown, number>>>;
|
|
8
|
+
/** Whether a live boundary brought a value for the key: it sends that value to the browser itself. */
|
|
9
|
+
export declare const queuedByBoundary: (registry: AtomRegistry.AtomRegistry, key: string) => boolean;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per registry, key and queued value, how many live boundaries queued it. Boundaries given the
|
|
3
|
+
* same state, as a layout and its page might be, queue the same values, and one destroyed first
|
|
4
|
+
* must leave them for the other's children.
|
|
5
|
+
*/
|
|
6
|
+
export const queuedBy = new WeakMap();
|
|
7
|
+
/** Whether a live boundary brought a value for the key: it sends that value to the browser itself. */
|
|
8
|
+
export const queuedByBoundary = (registry, key) =>
|
|
9
|
+
// oxlint-disable-next-line sonarjs/no-empty-collection -- HydrationBoundary.svelte fills it; this module only declares it
|
|
10
|
+
queuedBy.get(registry)?.has(key) === true;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { AtomRegistry } from "effect/reactivity";
|
|
2
|
+
/** Calls `f` once if `registry` is disposed of through `disposeRegistry`; returns the canceler. */
|
|
3
|
+
export declare const onDispose: (registry: AtomRegistry.AtomRegistry, f: () => void) => (() => void);
|
|
4
|
+
/**
|
|
5
|
+
* Disposes of `registry`, then runs what waited on it, even when a finalizer the disposal runs
|
|
6
|
+
* throws.
|
|
7
|
+
*
|
|
8
|
+
* In the browser that error is rethrown on a microtask, so it is still reported as uncaught but
|
|
9
|
+
* stays out of Svelte's teardown: thrown from the `onDestroy` of a component being destroyed, it
|
|
10
|
+
* escapes Svelte's flush and leaves every `$derived` on the page running again on every read. On
|
|
11
|
+
* the server it is thrown here.
|
|
12
|
+
*/
|
|
13
|
+
export declare const disposeRegistry: (registry: AtomRegistry.AtomRegistry) => void;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { BROWSER } from "esm-env";
|
|
2
|
+
const callbacks = new WeakMap();
|
|
3
|
+
/** Calls `f` once if `registry` is disposed of through `disposeRegistry`; returns the canceler. */
|
|
4
|
+
export const onDispose = (registry, f) => {
|
|
5
|
+
let set = callbacks.get(registry);
|
|
6
|
+
if (set === undefined) {
|
|
7
|
+
set = new Set();
|
|
8
|
+
callbacks.set(registry, set);
|
|
9
|
+
}
|
|
10
|
+
set.add(f);
|
|
11
|
+
return () => {
|
|
12
|
+
set.delete(f);
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Disposes of `registry`, then runs what waited on it, even when a finalizer the disposal runs
|
|
17
|
+
* throws.
|
|
18
|
+
*
|
|
19
|
+
* In the browser that error is rethrown on a microtask, so it is still reported as uncaught but
|
|
20
|
+
* stays out of Svelte's teardown: thrown from the `onDestroy` of a component being destroyed, it
|
|
21
|
+
* escapes Svelte's flush and leaves every `$derived` on the page running again on every read. On
|
|
22
|
+
* the server it is thrown here.
|
|
23
|
+
*/
|
|
24
|
+
export const disposeRegistry = (registry) => {
|
|
25
|
+
try {
|
|
26
|
+
registry.dispose();
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
if (!BROWSER) {
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
32
|
+
queueMicrotask(() => {
|
|
33
|
+
throw error;
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
finally {
|
|
37
|
+
const set = callbacks.get(registry);
|
|
38
|
+
callbacks.delete(registry);
|
|
39
|
+
for (const f of set ?? []) {
|
|
40
|
+
f();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Engine } from "./engine.ts";
|
|
2
|
+
import type { FieldImpl } from "./field.ts";
|
|
3
|
+
import type { GroupImpl } from "./group.ts";
|
|
4
|
+
import type { IndexPath, KeyPath } from "./paths.ts";
|
|
5
|
+
export declare class ArrayImpl {
|
|
6
|
+
#private;
|
|
7
|
+
readonly keyPath: KeyPath;
|
|
8
|
+
constructor(engine: Engine, keyPath: KeyPath);
|
|
9
|
+
get path(): IndexPath;
|
|
10
|
+
get items(): readonly {
|
|
11
|
+
readonly key: number;
|
|
12
|
+
readonly index: number;
|
|
13
|
+
readonly fields: GroupImpl;
|
|
14
|
+
readonly field: FieldImpl;
|
|
15
|
+
}[];
|
|
16
|
+
get error(): string | undefined;
|
|
17
|
+
append(item: unknown): void;
|
|
18
|
+
remove(index: number): void;
|
|
19
|
+
move(from: number, to: number): void;
|
|
20
|
+
replace(index: number, item: unknown): void;
|
|
21
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { pathId } from "./paths.js";
|
|
2
|
+
export class ArrayImpl {
|
|
3
|
+
#engine;
|
|
4
|
+
keyPath;
|
|
5
|
+
constructor(engine, keyPath) {
|
|
6
|
+
this.#engine = engine;
|
|
7
|
+
this.keyPath = keyPath;
|
|
8
|
+
}
|
|
9
|
+
get path() {
|
|
10
|
+
return this.#engine.indexPath(this.keyPath) ?? [];
|
|
11
|
+
}
|
|
12
|
+
get items() {
|
|
13
|
+
const keys = this.#engine.state.keys.get(pathId(this.keyPath)) ?? [];
|
|
14
|
+
return keys.map((key, index) => ({
|
|
15
|
+
field: this.#engine.field([...this.keyPath, key]),
|
|
16
|
+
fields: this.#engine.group([...this.keyPath, key]),
|
|
17
|
+
index,
|
|
18
|
+
key,
|
|
19
|
+
}));
|
|
20
|
+
}
|
|
21
|
+
get error() {
|
|
22
|
+
return this.#engine.errorAt(this.keyPath);
|
|
23
|
+
}
|
|
24
|
+
append(item) {
|
|
25
|
+
this.#engine.updateArray(this.keyPath, (items, keys) => {
|
|
26
|
+
items.push(item);
|
|
27
|
+
keys.push(this.#engine.newKey());
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
remove(index) {
|
|
31
|
+
this.#engine.updateArray(this.keyPath, (items, keys) => {
|
|
32
|
+
if (index >= 0 && index < items.length) {
|
|
33
|
+
items.splice(index, 1);
|
|
34
|
+
keys.splice(index, 1);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
move(from, to) {
|
|
39
|
+
this.#engine.updateArray(this.keyPath, (items, keys) => {
|
|
40
|
+
const inRange = (index) => index >= 0 && index < items.length;
|
|
41
|
+
if (!(inRange(from) && inRange(to)) || from === to) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
items.splice(to, 0, ...items.splice(from, 1));
|
|
45
|
+
keys.splice(to, 0, ...keys.splice(from, 1));
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
replace(index, item) {
|
|
49
|
+
this.#engine.updateArray(this.keyPath, (items, keys) => {
|
|
50
|
+
if (index >= 0 && index < items.length) {
|
|
51
|
+
items[index] = item;
|
|
52
|
+
// A new item: the old one's errors and touched state don't carry over.
|
|
53
|
+
keys[index] = this.#engine.newKey();
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { SchemaAST } from "effect";
|
|
2
|
+
import type { IndexPath } from "./paths.ts";
|
|
3
|
+
/** The attributes `field.constraints` can set. */
|
|
4
|
+
export interface Constraints {
|
|
5
|
+
readonly required?: boolean | undefined;
|
|
6
|
+
readonly minlength?: number | undefined;
|
|
7
|
+
readonly maxlength?: number | undefined;
|
|
8
|
+
readonly min?: number | undefined;
|
|
9
|
+
readonly max?: number | undefined;
|
|
10
|
+
readonly pattern?: string | undefined;
|
|
11
|
+
}
|
|
12
|
+
/** The HTML constraints for the field at `path` in `schema`'s AST. */
|
|
13
|
+
export declare const constraintsAt: (root: SchemaAST.AST, path: IndexPath) => Constraints;
|