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.
Files changed (56) hide show
  1. package/README.md +5 -2
  2. package/dist/Form.d.ts +605 -0
  3. package/dist/Form.js +67 -0
  4. package/dist/Hooks.svelte.d.ts +70 -10
  5. package/dist/Hooks.svelte.js +489 -163
  6. package/dist/HydrationBoundary.svelte +134 -17
  7. package/dist/HydrationBoundary.svelte.d.ts +13 -4
  8. package/dist/Inspector.d.ts +7 -3
  9. package/dist/Inspector.js +113 -29
  10. package/dist/RegistryContext.d.ts +2 -0
  11. package/dist/RegistryContext.js +13 -5
  12. package/dist/ScopedAtom.d.ts +5 -4
  13. package/dist/ScopedAtom.js +20 -11
  14. package/dist/SvelteKit.d.ts +71 -12
  15. package/dist/SvelteKit.js +133 -54
  16. package/dist/index.d.ts +10 -1
  17. package/dist/index.js +10 -1
  18. package/dist/internal/awaitWrite.d.ts +11 -0
  19. package/dist/internal/awaitWrite.js +27 -0
  20. package/dist/internal/boundaries.d.ts +9 -0
  21. package/dist/internal/boundaries.js +10 -0
  22. package/dist/internal/disposal.d.ts +13 -0
  23. package/dist/internal/disposal.js +43 -0
  24. package/dist/internal/form/array.d.ts +21 -0
  25. package/dist/internal/form/array.js +57 -0
  26. package/dist/internal/form/constraints.d.ts +13 -0
  27. package/dist/internal/form/constraints.js +166 -0
  28. package/dist/internal/form/engine.d.ts +56 -0
  29. package/dist/internal/form/engine.js +842 -0
  30. package/dist/internal/form/engineOf.d.ts +26 -0
  31. package/dist/internal/form/engineOf.js +43 -0
  32. package/dist/internal/form/field.d.ts +30 -0
  33. package/dist/internal/form/field.js +68 -0
  34. package/dist/internal/form/group.d.ts +13 -0
  35. package/dist/internal/form/group.js +20 -0
  36. package/dist/internal/form/instance.d.ts +41 -0
  37. package/dist/internal/form/instance.js +135 -0
  38. package/dist/internal/form/issues.d.ts +23 -0
  39. package/dist/internal/form/issues.js +144 -0
  40. package/dist/internal/form/paths.d.ts +47 -0
  41. package/dist/internal/form/paths.js +175 -0
  42. package/dist/internal/form/types.d.ts +80 -0
  43. package/dist/internal/form/types.js +1 -0
  44. package/dist/internal/form/validationError.d.ts +13 -0
  45. package/dist/internal/form/validationError.js +6 -0
  46. package/dist/internal/hydration.d.ts +11 -2
  47. package/dist/internal/hydration.js +26 -4
  48. package/dist/internal/nodeInternals.d.ts +6 -0
  49. package/dist/internal/nodeInternals.js +64 -7
  50. package/dist/internal/renderEnd.d.ts +13 -0
  51. package/dist/internal/renderEnd.js +34 -0
  52. package/dist/internal/teardown.svelte.d.ts +17 -0
  53. package/dist/internal/teardown.svelte.js +44 -0
  54. package/dist/internal/troubleshooting.d.ts +2 -0
  55. package/dist/internal/troubleshooting.js +3 -0
  56. package/package.json +12 -10
@@ -0,0 +1,26 @@
1
+ import { Atom } from "effect/reactivity";
2
+ import type { AtomRegistry } from "effect/reactivity";
3
+ import { Engine } from "./engine.ts";
4
+ import type { Definition } from "./types.ts";
5
+ /** An engine for a form definition, from the options `useForm` and `Form.instance` take. */
6
+ export declare const engineOf: (form: {
7
+ readonly definition: Definition;
8
+ }, options: {
9
+ readonly registry: AtomRegistry.AtomRegistry;
10
+ readonly initialValues: unknown;
11
+ readonly validates: boolean;
12
+ readonly id: string;
13
+ readonly values?: Atom.Writable<unknown> | undefined;
14
+ readonly submit?: {
15
+ readonly atom: unknown;
16
+ readonly payload?: unknown;
17
+ } | undefined;
18
+ readonly mapError?: unknown;
19
+ readonly autoSubmit?: boolean | undefined;
20
+ readonly resetOnSuccess?: boolean | undefined;
21
+ }) => Engine;
22
+ /**
23
+ * The function that computes a form's initial values, when `initialValues` is one rather than the
24
+ * values themselves.
25
+ */
26
+ export declare const initialGetter: (initial: unknown) => (() => unknown) | undefined;
@@ -0,0 +1,43 @@
1
+ // Builds a form engine from the public options. Internal: not exported from the package.
2
+ import { Atom } from "effect/reactivity";
3
+ import { Engine } from "./engine.js";
4
+ /** An engine for a form definition, from the options `useForm` and `Form.instance` take. */
5
+ export const engineOf = (form, options) => {
6
+ const { submit } = options;
7
+ const atom = submit?.atom;
8
+ const getAtom = typeof atom === "function" && !Atom.isAtom(atom) ? atom : () => atom;
9
+ return new Engine(form.definition, {
10
+ autoSubmit: options.autoSubmit,
11
+ id: options.id,
12
+ initialValues: options.initialValues,
13
+ // SAFETY: the overloads of useForm and Form.instance type mapError as taking the submit atom's
14
+ // error and returning errors for the form's values.
15
+ // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- the implementation signatures can't carry the overloads' types
16
+ mapError: options.mapError,
17
+ registry: options.registry,
18
+ resetOnSuccess: options.resetOnSuccess,
19
+ submit: submit === undefined
20
+ ? undefined
21
+ : {
22
+ // SAFETY: the overloads type the submit atom as a writable AsyncResult atom, or a
23
+ // function returning one; anything else that is a function is the second.
24
+ // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- a typeof check can't recover the function's signature
25
+ atom: getAtom,
26
+ // SAFETY: the overloads type payload as a function of the decoded values and the
27
+ // submit's arguments.
28
+ // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- the implementation signatures can't carry the overloads' types
29
+ payload: submit.payload,
30
+ },
31
+ validates: options.validates,
32
+ values: options.values,
33
+ });
34
+ };
35
+ /**
36
+ * The function that computes a form's initial values, when `initialValues` is one rather than the
37
+ * values themselves.
38
+ */
39
+ export const initialGetter = (initial) =>
40
+ // SAFETY: the overloads type initialValues as the values or a function of no arguments that
41
+ // returns them, so a function here is the second.
42
+ // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- a typeof check can't recover the function's signature
43
+ typeof initial === "function" ? initial : undefined;
@@ -0,0 +1,30 @@
1
+ import type { Constraints } from "./constraints.ts";
2
+ import type { Engine } from "./engine.ts";
3
+ import type { IndexPath, KeyPath } from "./paths.ts";
4
+ export declare class FieldImpl {
5
+ #private;
6
+ readonly keyPath: KeyPath;
7
+ constructor(engine: Engine, keyPath: KeyPath);
8
+ get path(): IndexPath;
9
+ get value(): unknown;
10
+ set value(value: unknown);
11
+ get error(): string | undefined;
12
+ get issue(): string | undefined;
13
+ get touched(): boolean;
14
+ set touched(touched: boolean);
15
+ get dirty(): boolean;
16
+ get validating(): boolean;
17
+ get id(): string;
18
+ get errorId(): string;
19
+ get name(): string;
20
+ get attrs(): {
21
+ readonly "aria-describedby": string | undefined;
22
+ readonly "aria-invalid": true | undefined;
23
+ readonly id: string;
24
+ readonly name: string;
25
+ readonly onblur: () => void;
26
+ };
27
+ get constraints(): Constraints;
28
+ /** What leaving the field does, for a custom control with no blur event of its own. */
29
+ blur(): void;
30
+ }
@@ -0,0 +1,68 @@
1
+ import { equals, getIn, nameOf, pathId } from "./paths.js";
2
+ export class FieldImpl {
3
+ #engine;
4
+ keyPath;
5
+ #onblur = () => {
6
+ this.#engine.leave(this.keyPath);
7
+ };
8
+ constructor(engine, keyPath) {
9
+ this.#engine = engine;
10
+ this.keyPath = keyPath;
11
+ }
12
+ get path() {
13
+ return this.#engine.indexPath(this.keyPath) ?? [];
14
+ }
15
+ get value() {
16
+ const index = this.#engine.indexPath(this.keyPath);
17
+ return index === undefined ? undefined : getIn(this.#engine.values, index);
18
+ }
19
+ set value(value) {
20
+ this.#engine.setField(this.keyPath, value);
21
+ }
22
+ get error() {
23
+ return this.#engine.errorAt(this.keyPath);
24
+ }
25
+ get issue() {
26
+ return this.#engine.state.issues.get(pathId(this.keyPath));
27
+ }
28
+ get touched() {
29
+ return this.#engine.state.touched.has(pathId(this.keyPath));
30
+ }
31
+ set touched(touched) {
32
+ this.#engine.setTouched(this.keyPath, touched);
33
+ }
34
+ get dirty() {
35
+ const index = this.#engine.indexPath(this.keyPath);
36
+ return (index !== undefined &&
37
+ !equals(getIn(this.#engine.values, index), getIn(this.#engine.state.initial, index)));
38
+ }
39
+ get validating() {
40
+ return this.#engine.state.validating;
41
+ }
42
+ get id() {
43
+ return this.#engine.elementId(this.keyPath);
44
+ }
45
+ get errorId() {
46
+ return `${this.id}-error`;
47
+ }
48
+ get name() {
49
+ return nameOf(this.path);
50
+ }
51
+ get attrs() {
52
+ const invalid = this.error !== undefined;
53
+ return {
54
+ "aria-describedby": invalid ? this.errorId : undefined,
55
+ "aria-invalid": invalid ? true : undefined,
56
+ id: this.id,
57
+ name: this.name,
58
+ onblur: this.#onblur,
59
+ };
60
+ }
61
+ get constraints() {
62
+ return this.#engine.constraints(this.path);
63
+ }
64
+ /** What leaving the field does, for a custom control with no blur event of its own. */
65
+ blur() {
66
+ this.#onblur();
67
+ }
68
+ }
@@ -0,0 +1,13 @@
1
+ import type { ArrayImpl } from "./array.ts";
2
+ import type { Engine } from "./engine.ts";
3
+ import type { FieldImpl } from "./field.ts";
4
+ import type { IndexPath, KeyPath } from "./paths.ts";
5
+ export declare class GroupImpl {
6
+ readonly engine: Engine;
7
+ readonly keyPath: KeyPath;
8
+ constructor(engine: Engine, keyPath: KeyPath);
9
+ get path(): IndexPath;
10
+ field(path: string | IndexPath): FieldImpl;
11
+ array(path: string | IndexPath): ArrayImpl;
12
+ at(path: string | IndexPath): GroupImpl;
13
+ }
@@ -0,0 +1,20 @@
1
+ export class GroupImpl {
2
+ engine;
3
+ keyPath;
4
+ constructor(engine, keyPath) {
5
+ this.engine = engine;
6
+ this.keyPath = keyPath;
7
+ }
8
+ get path() {
9
+ return this.engine.indexPath(this.keyPath) ?? [];
10
+ }
11
+ field(path) {
12
+ return this.engine.field(this.engine.resolve(this.keyPath, path));
13
+ }
14
+ array(path) {
15
+ return this.engine.array(this.engine.resolve(this.keyPath, path));
16
+ }
17
+ at(path) {
18
+ return this.engine.group(this.engine.resolve(this.keyPath, path));
19
+ }
20
+ }
@@ -0,0 +1,41 @@
1
+ import { Exit, Option } from "effect";
2
+ import type { AsyncResult } from "effect/reactivity";
3
+ import type { Engine } from "./engine.ts";
4
+ import { GroupImpl } from "./group.ts";
5
+ import type { IndexPath } from "./paths.ts";
6
+ import type { ErrorsInput } from "./types.ts";
7
+ export declare class InstanceImpl extends GroupImpl {
8
+ #private;
9
+ constructor(engine: Engine);
10
+ get values(): unknown;
11
+ get initialValues(): unknown;
12
+ get dirty(): boolean;
13
+ get valid(): boolean;
14
+ get validating(): boolean;
15
+ get submitting(): boolean;
16
+ get submitResult(): AsyncResult.AsyncResult<unknown, unknown>;
17
+ get rootError(): string | undefined;
18
+ get errors(): readonly {
19
+ readonly path: IndexPath;
20
+ readonly message: string;
21
+ }[];
22
+ get submitCount(): number;
23
+ get validationCount(): number;
24
+ get lastSubmitted(): Option.Option<{
25
+ readonly encoded: unknown;
26
+ readonly decoded: unknown;
27
+ }>;
28
+ get dirtySinceSubmit(): boolean;
29
+ get attrs(): {
30
+ readonly novalidate: true;
31
+ readonly onsubmit: (event: Event) => void;
32
+ };
33
+ submit(args?: unknown): Promise<Exit.Exit<unknown, unknown>>;
34
+ validate(paths?: readonly (string | IndexPath)[]): Promise<boolean>;
35
+ setValues(values: unknown): void;
36
+ reset(values?: unknown): void;
37
+ revertToLastSubmit(): void;
38
+ setErrors(errors: ErrorsInput): void;
39
+ focusFirstError(): void;
40
+ dispose(): void;
41
+ }
@@ -0,0 +1,135 @@
1
+ // A form instance: the root group, with the form's state and actions. Internal: `Form.ts` types it.
2
+ import { Cause, Exit, Option } from "effect";
3
+ import { GroupImpl } from "./group.js";
4
+ import { equals, fromPathId, pathId, toKeyPath } from "./paths.js";
5
+ import { FormValidationError } from "./validationError.js";
6
+ export class InstanceImpl extends GroupImpl {
7
+ #onsubmit = (event) => {
8
+ event.preventDefault();
9
+ void this.#submitAndFocus();
10
+ };
11
+ // oxlint-disable-next-line effecttsgo/async-function -- the form's submit handler, which awaits the Promise-facing submit, outside any Effect program
12
+ async #submitAndFocus() {
13
+ const exit = await this.submit();
14
+ if (Exit.isFailure(exit)) {
15
+ const error = Cause.findErrorOption(exit.cause);
16
+ if (Option.isSome(error) && error.value instanceof FormValidationError) {
17
+ this.focusFirstError();
18
+ }
19
+ }
20
+ }
21
+ constructor(engine) {
22
+ super(engine, []);
23
+ }
24
+ get values() {
25
+ return this.engine.values;
26
+ }
27
+ get initialValues() {
28
+ return this.engine.state.initial;
29
+ }
30
+ get dirty() {
31
+ return !equals(this.engine.values, this.engine.state.initial);
32
+ }
33
+ get valid() {
34
+ const { state } = this.engine;
35
+ const current = state.checked === this.engine.values && !state.validating;
36
+ return (current &&
37
+ state.failure === undefined &&
38
+ state.issues.size === 0 &&
39
+ state.external.size === 0);
40
+ }
41
+ get validating() {
42
+ return this.engine.state.validating;
43
+ }
44
+ get submitting() {
45
+ return this.engine.state.submitting;
46
+ }
47
+ get submitResult() {
48
+ return this.engine.state.submitResult;
49
+ }
50
+ get rootError() {
51
+ return this.engine.errorAt([]);
52
+ }
53
+ get errors() {
54
+ const { state } = this.engine;
55
+ const out = [];
56
+ const seen = new Set();
57
+ const add = (id) => {
58
+ if (seen.has(id)) {
59
+ return;
60
+ }
61
+ seen.add(id);
62
+ const keyPath = fromPathId(id);
63
+ const message = this.engine.errorAt(keyPath);
64
+ const path = this.engine.indexPath(keyPath);
65
+ if (message !== undefined && path !== undefined) {
66
+ out.push({ message, path });
67
+ }
68
+ };
69
+ add(pathId([]));
70
+ for (const id of state.issues.keys()) {
71
+ add(id);
72
+ }
73
+ for (const id of state.external.keys()) {
74
+ add(id);
75
+ }
76
+ return out;
77
+ }
78
+ get submitCount() {
79
+ return this.engine.state.submitCount;
80
+ }
81
+ get validationCount() {
82
+ return this.engine.state.validationCount;
83
+ }
84
+ get lastSubmitted() {
85
+ return this.engine.state.lastSubmitted;
86
+ }
87
+ get dirtySinceSubmit() {
88
+ const last = this.engine.state.lastSubmitted;
89
+ return (Option.isSome(last) && !equals(this.engine.values, last.value.encoded));
90
+ }
91
+ get attrs() {
92
+ return { novalidate: true, onsubmit: this.#onsubmit };
93
+ }
94
+ submit(args) {
95
+ return this.engine.submit(args);
96
+ }
97
+ validate(paths) {
98
+ return this.engine.validate(paths);
99
+ }
100
+ setValues(values) {
101
+ this.engine.setValues(values);
102
+ }
103
+ reset(values) {
104
+ this.engine.reset(values);
105
+ }
106
+ revertToLastSubmit() {
107
+ this.engine.revertToLastSubmit();
108
+ }
109
+ setErrors(errors) {
110
+ this.engine.setErrors(errors, "manual");
111
+ }
112
+ focusFirstError() {
113
+ if (typeof document === "undefined") {
114
+ return;
115
+ }
116
+ // The ids of the fields showing an error, as in `field.attrs`.
117
+ const selectors = [];
118
+ for (const { path } of this.errors) {
119
+ const keyPath = path.length === 0 ? undefined : toKeyPath(this.engine.state.keys, path);
120
+ if (keyPath !== undefined) {
121
+ selectors.push(`#${CSS.escape(this.engine.elementId(keyPath))}`);
122
+ }
123
+ }
124
+ if (selectors.length === 0) {
125
+ return;
126
+ }
127
+ // querySelector finds the first match in document order: the first on the page, which may not
128
+ // be the Schema's first, as for a `{ city, street }` laid out street first. A field whose
129
+ // element isn't on the page is skipped.
130
+ document.querySelector(selectors.join(","))?.focus();
131
+ }
132
+ dispose() {
133
+ this.engine.dispose();
134
+ }
135
+ }
@@ -0,0 +1,23 @@
1
+ import { SchemaIssue } from "effect";
2
+ import type { IndexPath } from "./paths.ts";
3
+ /** A message for a leaf issue, or `undefined` to fall back on Effect's. */
4
+ export type LeafHook = (issue: SchemaIssue.Leaf) => string | undefined;
5
+ /** A message for a failed check, or `undefined` to fall back on Effect's. */
6
+ export type CheckHook = (issue: SchemaIssue.Filter) => string | undefined;
7
+ /** One error from a decode: where it is, and what to show. */
8
+ export interface PathIssue {
9
+ readonly path: IndexPath;
10
+ readonly message: string;
11
+ }
12
+ interface Hooks {
13
+ readonly leafHook?: LeafHook | undefined;
14
+ readonly checkHook?: CheckHook | undefined;
15
+ }
16
+ /**
17
+ * Every issue in a decode failure, as Effect's Standard Schema formatter walks them, but with the
18
+ * message chosen in this order: the Schema's annotation, then the caller's hook, then Effect's
19
+ * default. A check that carries a `message` annotation and points at a sub-path keeps that path;
20
+ * Effect's formatter would report it at the check's own path.
21
+ */
22
+ export declare const toPathIssues: (issue: SchemaIssue.Issue, hooks: Hooks) => readonly PathIssue[];
23
+ export {};
@@ -0,0 +1,144 @@
1
+ import { SchemaIssue } from "effect";
2
+ // The lookup Effect's default hooks do (`findMessage` in SchemaIssue.ts, which is private): a
3
+ // `message` annotation, or `messageMissingKey`/`messageUnexpectedKey` for those issues. Done here
4
+ // so an annotated message wins over the caller's hooks, which would otherwise replace Effect's
5
+ // default hooks and with them the annotation lookup.
6
+ const messageKeys = {
7
+ MissingKey: "messageMissingKey",
8
+ UnexpectedKey: "messageUnexpectedKey",
9
+ };
10
+ const findMessage = (issue) => {
11
+ if (issue._tag === "Pointer") {
12
+ return undefined;
13
+ }
14
+ if (issue._tag === "Encoding") {
15
+ return findMessage(issue.issue);
16
+ }
17
+ let source;
18
+ if (issue._tag === "Filter") {
19
+ source = issue.filter;
20
+ }
21
+ else if ("annotations" in issue) {
22
+ source = issue;
23
+ }
24
+ else {
25
+ source = issue.ast;
26
+ }
27
+ const { annotations } = source;
28
+ const message = annotations?.[messageKeys[issue._tag] ?? "message"];
29
+ return typeof message === "string" ? message : undefined;
30
+ };
31
+ const effectFormatter = SchemaIssue.makeFormatterStandardSchemaV1();
32
+ // Paths from Effect are PropertyKeys, or `{ key }` in Standard Schema's type; a form's paths are
33
+ // object keys and array indexes.
34
+ const normalize = (path) => path.map((segment) => {
35
+ const key = typeof segment === "object" ? segment.key : segment;
36
+ return typeof key === "number" ? key : String(key);
37
+ });
38
+ // The paths a check's issue points at, below `path`: a check on a Struct that returns
39
+ // `{ path, issue }` wraps its issue in a Pointer.
40
+ const pointedPaths = (issue, path) => {
41
+ switch (issue._tag) {
42
+ case "Pointer": {
43
+ return [[...path, ...issue.path]];
44
+ }
45
+ case "Composite":
46
+ case "AnyOf": {
47
+ return issue.issues.flatMap((inner) => pointedPaths(inner, path));
48
+ }
49
+ case "Encoding": {
50
+ return pointedPaths(issue.issue, path);
51
+ }
52
+ case "Filter":
53
+ case "Forbidden":
54
+ case "InvalidType":
55
+ case "InvalidValue":
56
+ case "MissingKey":
57
+ case "OneOf":
58
+ case "UnexpectedKey": {
59
+ return [];
60
+ }
61
+ }
62
+ };
63
+ // Effect's own message for an issue it formats with no hook, at `path`.
64
+ const emitDefault = (issue, path, emit) => {
65
+ for (const formatted of effectFormatter(issue).issues) {
66
+ emit([...path, ...(formatted.path ?? [])], formatted.message);
67
+ }
68
+ };
69
+ const walkFilter = (issue, path, hooks, emit, walk) => {
70
+ const pointed = pointedPaths(issue.issue, path);
71
+ const annotated = findMessage(issue.issue) ?? findMessage(issue);
72
+ if (annotated !== undefined) {
73
+ for (const target of pointed.length > 0 ? pointed : [path]) {
74
+ emit(target, annotated);
75
+ }
76
+ return;
77
+ }
78
+ // A check that points at fields carries its own messages there ({ path, issue }), so the
79
+ // caller's hook doesn't replace them.
80
+ if (pointed.length > 0) {
81
+ walk(issue.issue, path);
82
+ return;
83
+ }
84
+ const hooked = hooks.checkHook?.(issue);
85
+ if (hooked !== undefined) {
86
+ emit(path, hooked);
87
+ }
88
+ else if (issue.issue._tag === "InvalidValue") {
89
+ emitDefault(issue, path, emit);
90
+ }
91
+ else {
92
+ walk(issue.issue, path);
93
+ }
94
+ };
95
+ /**
96
+ * Every issue in a decode failure, as Effect's Standard Schema formatter walks them, but with the
97
+ * message chosen in this order: the Schema's annotation, then the caller's hook, then Effect's
98
+ * default. A check that carries a `message` annotation and points at a sub-path keeps that path;
99
+ * Effect's formatter would report it at the check's own path.
100
+ */
101
+ export const toPathIssues = (issue, hooks) => {
102
+ const out = [];
103
+ const emit = (path, message) => {
104
+ out.push({ message, path: normalize(path) });
105
+ };
106
+ const walk = (current, path) => {
107
+ switch (current._tag) {
108
+ case "Filter": {
109
+ walkFilter(current, path, hooks, emit, walk);
110
+ return;
111
+ }
112
+ case "Encoding": {
113
+ walk(current.issue, path);
114
+ return;
115
+ }
116
+ case "Pointer": {
117
+ walk(current.issue, [...path, ...current.path]);
118
+ return;
119
+ }
120
+ case "Composite":
121
+ case "AnyOf": {
122
+ if (current.issues.length === 0) {
123
+ emitDefault(current, path, emit);
124
+ }
125
+ for (const inner of current.issues) {
126
+ walk(inner, path);
127
+ }
128
+ return;
129
+ }
130
+ case "Forbidden":
131
+ case "InvalidType":
132
+ case "InvalidValue":
133
+ case "MissingKey":
134
+ case "OneOf":
135
+ case "UnexpectedKey": {
136
+ emit(path, findMessage(current) ??
137
+ hooks.leafHook?.(current) ??
138
+ SchemaIssue.defaultLeafHook(current));
139
+ }
140
+ }
141
+ };
142
+ walk(issue, []);
143
+ return out;
144
+ };
@@ -0,0 +1,47 @@
1
+ /** A path into a form's values: object keys, and numbers for array indexes. */
2
+ export type IndexPath = readonly (string | number)[];
3
+ /** A path with each array index replaced by that item's key. */
4
+ export type KeyPath = readonly (string | number)[];
5
+ /** Item keys per array, by the array's key path (as `pathId`). */
6
+ export type Keys = ReadonlyMap<string, readonly number[]>;
7
+ /** A string for a key path, to use as a map key. */
8
+ export declare const pathId: (path: KeyPath) => string;
9
+ /** Parses a `pathId` back into its path. */
10
+ export declare const fromPathId: (id: string) => KeyPath;
11
+ /** The value at `path`, or `undefined` when a step is missing. */
12
+ export declare const getIn: (root: unknown, path: IndexPath) => unknown;
13
+ /** A copy of `root` with `value` at `path`, sharing everything off the path. */
14
+ export declare const setIn: (root: unknown, path: IndexPath, value: unknown) => unknown;
15
+ /** Whether `path` starts with `prefix`. */
16
+ export declare const startsWith: (path: KeyPath, prefix: KeyPath) => boolean;
17
+ /**
18
+ * The key path for an index path, or `undefined` when an index has no item. A number segment is an
19
+ * index only where the value so far is an array; a record's numeric-looking keys are strings.
20
+ */
21
+ export declare const toKeyPath: (keys: Keys, path: IndexPath) => KeyPath | undefined;
22
+ /** The index path for a key path, or `undefined` when one of its items is gone. */
23
+ export declare const toIndexPath: (keys: Keys, path: KeyPath) => IndexPath | undefined;
24
+ /**
25
+ * Item keys for every array in `values`. Arrays that already have keys keep them, by position, and
26
+ * new positions take fresh keys from `next`; keys for arrays that are gone are dropped.
27
+ */
28
+ export declare const reconcileKeys: (values: unknown, previous: Keys, next: () => number) => Map<string, readonly number[]>;
29
+ /** Whether every item key in `path` still belongs to its array. */
30
+ export declare const isLive: (keys: Keys, path: KeyPath) => boolean;
31
+ /**
32
+ * Equality for form values. Arrays and plain objects compare by content, with a key set to
33
+ * `undefined` the same as a missing one; dates by time; values that implement Effect's `Equal`
34
+ * (`Option`, `HashMap`) by `Equal.equals`. Any other class instance compares by identity: a
35
+ * `File`, a `Map`, and also a `Data.Class` or `Schema.Class` value, which in Effect 4 doesn't
36
+ * implement `Equal`. So a fresh instance with the same content counts as a change.
37
+ */
38
+ export declare const equals: (a: unknown, b: unknown) => boolean;
39
+ /**
40
+ * A string for a key path that is safe in an element id and different for every path:
41
+ * `items-_7-qty`. Segments are joined with `-`, an item key is `_` and the number, and any other
42
+ * character of a key than a letter or a digit is escaped (`a-b` is `a_2d_b`), so a key can't
43
+ * produce a `-` or an item key's form.
44
+ */
45
+ export declare const idSuffix: (path: KeyPath) => string;
46
+ /** The `name` attribute for an index path: `items.0.qty`. */
47
+ export declare const nameOf: (path: IndexPath) => string;