@thomasfosterau/effect-forms 0.0.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.
@@ -0,0 +1,878 @@
1
+ import * as Schema from "effect/Schema";
2
+ import * as SchemaAST from "effect/SchemaAST";
3
+ import * as Effect from "effect/Effect";
4
+ import * as SchemaIssue from "effect/SchemaIssue";
5
+ import * as Duration from "effect/Duration";
6
+ declare namespace Coerce_d_exports {
7
+ export { booleanFromCheckbox, emptyToNull, emptyToUndefined, numberFromField, trimToNull, trimToUndefined };
8
+ }
9
+ /**
10
+ * @copyright Thomas Foster
11
+ *
12
+ * Tri-state coercers for the HTML-form boundary.
13
+ *
14
+ * An HTML form submits every value as a string, and an absent optional field
15
+ * arrives as `undefined` (or an empty string). Mapping that onto a
16
+ * value / `null` / `undefined` update contract — where a value **sets**, `null`
17
+ * **clears** a nullable field, and `undefined` **skips** (carry-forward) — is
18
+ * the same boundary discipline the host app applies, lifted here so any
19
+ * consumer of the core gets it without a framework dependency.
20
+ *
21
+ * These are plain, total functions: they never throw and never reach for a
22
+ * framework's `error()` helper (validation failures belong on the schema, not
23
+ * in a coercer).
24
+ *
25
+ * @module effect-forms/Coerce
26
+ */
27
+ /** Empty string / `undefined` → `undefined`; otherwise the value (skip semantics). */
28
+ declare function emptyToUndefined<T>(value: T | "" | undefined): T | undefined;
29
+ /**
30
+ * `undefined` stays `undefined` (skip); empty string / `null` → `null`
31
+ * (clear a nullable field); otherwise the value (set). The three-state coercer
32
+ * for nullable update fields.
33
+ */
34
+ declare function emptyToNull<T>(value: T | "" | null | undefined): T | null | undefined;
35
+ /** Trim, then {@link emptyToUndefined}: whitespace-only collapses to `undefined` (skip). */
36
+ declare function trimToUndefined(value: string | undefined): string | undefined;
37
+ /** Trim, then {@link emptyToNull}: whitespace-only / blank clears (`null`). */
38
+ declare function trimToNull(value: string | undefined): string | null | undefined;
39
+ /**
40
+ * Map a numeric text field onto the value / `null` / `undefined` contract.
41
+ * Blank clears on the nullable path and skips otherwise; a present value
42
+ * parses with `Number` (`NaN` for non-numeric input — validate downstream).
43
+ */
44
+ declare function numberFromField(raw: string | undefined, options: {
45
+ nullable: false;
46
+ }): number | undefined;
47
+ declare function numberFromField(raw: string | undefined, options: {
48
+ nullable: true;
49
+ }): number | null | undefined;
50
+ /**
51
+ * Interpret a checkbox field. An HTML checkbox submits its `value` (default
52
+ * `"on"`) only when checked and omits the field entirely when unchecked, so a
53
+ * present field is `true` and an absent one is `false`. The truthy strings
54
+ * `"on"` / `"true"` / `"1"` / `"yes"` all read as `true`; an explicit
55
+ * `"false"` / `"0"` / `"off"` / `"no"` reads as `false`.
56
+ */
57
+ declare function booleanFromCheckbox(raw: string | undefined): boolean;
58
+ declare namespace Command_d_exports {
59
+ export { AdaptOverrides, CommandPlan, FieldKind, FieldPlan, FormInputRecord, blankDefaults, createAdapter, formInputFields, jsonRowsFromField, plan, updateAdapter };
60
+ }
61
+ /** How a command field renders and coerces at the form boundary. */
62
+ type FieldKind = "text" | "id" | "number" | "date" | "jsonArray" | "boolean" | "enum" | "structured";
63
+ /** One command field's derived plan, as plain data. */
64
+ interface FieldPlan {
65
+ readonly key: string;
66
+ readonly kind: FieldKind;
67
+ /** Present on the create struct. */
68
+ readonly inCreate: boolean;
69
+ /** Bare (non-optional) on the create struct. */
70
+ readonly createRequired: boolean;
71
+ /** Present on the update struct. */
72
+ readonly inUpdate: boolean;
73
+ /** `optional(NullOr(...))` on the update struct — blank clears (`null`). */
74
+ readonly updateClearable: boolean;
75
+ /**
76
+ * The field's literal option values, read off the AST — present only when
77
+ * `kind` is `"enum"`. In declaration order, stringified (a `<select>`'s
78
+ * option values are always strings at the form boundary, whatever the
79
+ * decoded literal's own type).
80
+ */
81
+ readonly options?: readonly string[];
82
+ }
83
+ /** The derived plan for one entity's create/update command pair. */
84
+ interface CommandPlan {
85
+ /** Every field except `id`, in update-struct declaration order (create-only fields appended). */
86
+ readonly fields: readonly FieldPlan[];
87
+ /** The create struct carries an optional client-generated `id` (idempotent retries). */
88
+ readonly clientId: boolean;
89
+ }
90
+ /** Per-field adapter overrides — the escape hatch for genuinely irregular coercion. */
91
+ type AdaptOverrides = Readonly<Record<string, (raw: string | undefined) => unknown>>;
92
+ /** A validated all-string multipart input, as the Form-Input schemas produce it. */
93
+ type FormInputRecord = Readonly<Record<string, string | undefined>>;
94
+ /** Minimal runtime shape of a command `Schema.Struct`. */
95
+ interface StructLike {
96
+ readonly fields: Record<string, Schema.Top>;
97
+ }
98
+ /**
99
+ * Read a create/update command struct pair into a {@link CommandPlan}.
100
+ * Runs once at module load — probe cost is a handful of tiny decodes per
101
+ * field, never per render or per request.
102
+ */
103
+ declare const plan: (create: StructLike, update: StructLike) => CommandPlan;
104
+ /**
105
+ * Derive the all-string Form-Input schema fields for one mode — every value is
106
+ * a string at the multipart boundary. Create-required fields get a min-length
107
+ * check so blankness surfaces as an inline field error; the update `id` gets
108
+ * the UUID check. Spread into `Schema.Struct({ ...Command.formInputFields(plan,
109
+ * "create") })`, with `overrides` spread last for irregular fields.
110
+ */
111
+ declare const formInputFields: (commandPlan: CommandPlan, mode: "create" | "update", overrides?: Readonly<Record<string, Schema.Top>>) => Record<string, Schema.Top>;
112
+ /**
113
+ * Blank string defaults for one mode's Form-Input shape. Client-id entities
114
+ * override `id` at the call site (`{ ...blankDefaults(plan, "create"), id:
115
+ * crypto.randomUUID() }`) so retries stay idempotent.
116
+ */
117
+ declare const blankDefaults: (commandPlan: CommandPlan, mode: "create" | "update") => Record<string, string>;
118
+ /**
119
+ * Parse a JSON-encoded row list (a contributor list) from a multipart form
120
+ * field. A blank or absent field yields `undefined` (skip — leave the existing
121
+ * list untouched); an empty JSON array (`"[]"`) clears the list. Rows keep
122
+ * `id` / `creditedName` when present; `orderingPosition` falls back to the
123
+ * row's array index. Throws a plain `Error` on a malformed payload — mapping
124
+ * that onto a framework response is the caller's concern.
125
+ */
126
+ declare const jsonRowsFromField: (raw: string | undefined, label: string) => {
127
+ id?: string | undefined;
128
+ agentId: string;
129
+ creditedName?: string | undefined;
130
+ orderingPosition: number;
131
+ }[] | undefined;
132
+ /**
133
+ * Derive the create adapter: validated all-string input → encoded create
134
+ * command payload. `overrides` replace the default per-field coercion for
135
+ * genuinely irregular fields (e.g. a create-time fallback default).
136
+ */
137
+ declare const createAdapter: (commandPlan: CommandPlan, overrides?: AdaptOverrides) => (input: FormInputRecord) => Record<string, unknown>;
138
+ /**
139
+ * Derive the update adapter: validated all-string input → encoded update
140
+ * command payload (value sets, `null` clears a clearable field, `undefined`
141
+ * skips). The required `id` passes through as-is — the command's canonical
142
+ * decode brands it.
143
+ */
144
+ declare const updateAdapter: (commandPlan: CommandPlan, overrides?: AdaptOverrides) => (input: FormInputRecord) => Record<string, unknown>;
145
+ declare namespace Issues_d_exports {
146
+ export { FormIssue, FormPath, LeafFormatter, defaultLeafFormatter, filterOutputToFormIssues, schemaIssuesToFormIssues };
147
+ }
148
+ /**
149
+ * A path into the form's data. String segments address object keys; number
150
+ * segments address array indices. The empty path addresses the form root
151
+ * (a whole-form / cross-field message).
152
+ */
153
+ type FormPath = ReadonlyArray<string | number>;
154
+ /** One validation failure: where it happened ({@link FormPath}) and why. */
155
+ interface FormIssue {
156
+ readonly path: FormPath;
157
+ readonly message: string;
158
+ }
159
+ /**
160
+ * Render one terminal issue leaf into a human-readable message. The default
161
+ * defers to the issue's own `toString`, which Effect Schema renders into a
162
+ * readable sentence (e.g. `Expected string, got 1`). Override it to localise
163
+ * or to map specific leaf `_tag`s to custom copy.
164
+ */
165
+ type LeafFormatter = (issue: SchemaIssue.Issue) => string;
166
+ /** The default leaf formatter — Effect Schema's own rendering of the leaf. */
167
+ declare const defaultLeafFormatter: LeafFormatter;
168
+ /**
169
+ * Walk an Effect Schema issue tree, emitting one {@link FormIssue} per leaf
170
+ * failure. Path segments accumulate through `Pointer` nodes; `Composite` and
171
+ * `AnyOf` fan out to siblings; `Filter` / `Encoding` are transparent
172
+ * wrappers. An `AnyOf` with no recorded member failures (a bare union
173
+ * mismatch) is itself a leaf.
174
+ */
175
+ declare function schemaIssuesToFormIssues(issue: SchemaIssue.Issue, format?: LeafFormatter): FormIssue[];
176
+ /**
177
+ * Normalise a filter predicate's {@link Schema.FilterOutput} into a flat list of
178
+ * {@link FormIssue}s (empty on success). Shared by whole-form refinements
179
+ * ({@link module:effect-forms/FormBuilder}) and per-field validators
180
+ * ({@link module:effect-forms/Validate}) so both fold into the same nested
181
+ * error tree as schema-decode failures.
182
+ */
183
+ declare function filterOutputToFormIssues(output: Schema.FilterOutput): FormIssue[];
184
+ declare namespace Constraints_d_exports {
185
+ export { InputConstraint, InputConstraints, constraintAt, constraintsFromSchema, isLeafConstraint };
186
+ }
187
+ /** Native validation attributes for a single input. */
188
+ interface InputConstraint {
189
+ readonly required?: boolean;
190
+ readonly min?: number | string;
191
+ readonly max?: number | string;
192
+ readonly minlength?: number;
193
+ readonly maxlength?: number;
194
+ readonly pattern?: string;
195
+ readonly step?: number;
196
+ }
197
+ /** A nested tree of {@link InputConstraint}s mirroring the schema's shape. */
198
+ interface InputConstraints {
199
+ readonly [key: string]: InputConstraint | InputConstraints | undefined;
200
+ }
201
+ /** Whether a node in an {@link InputConstraints} tree is a leaf ({@link InputConstraint}) rather than a branch. */
202
+ declare const isLeafConstraint: (value: InputConstraint | InputConstraints) => value is InputConstraint;
203
+ /**
204
+ * Derive the {@link InputConstraints} for a struct schema. Non-struct schemas
205
+ * yield an empty tree. `overrides` (deep-merged over the derived tree, and
206
+ * winning on conflict) is the seam for anything the AST walk does not infer.
207
+ */
208
+ declare function constraintsFromSchema(schema: Schema.Top, overrides?: InputConstraints): InputConstraints;
209
+ /**
210
+ * Read the {@link InputConstraint} leaf at `path` in an {@link InputConstraints}
211
+ * tree, defaulting to `{}` when `path` has no derived constraints. Array
212
+ * indices in `path` are skipped rather than descended into — the tree has no
213
+ * per-item nodes to index into, since {@link constraintsForType} derives one
214
+ * constraint (or branch) per *field*, shared by every item at that field. A
215
+ * bare item path (`tags[0]`) resolves to that shared leaf; a path that
216
+ * continues past the index into a struct item's own field (`items[0].label`)
217
+ * skips the index and resolves straight to `label`'s derived constraint.
218
+ */
219
+ declare function constraintAt(constraints: InputConstraints, path: FormPath): InputConstraint;
220
+ declare namespace Path_d_exports {
221
+ export { FieldPath, IndexMap, PathValue, clearAll, fromString, getAt, insertAt, isUnder, moveAt, remapIndex, remapKeyedRecord, removeAt, replaceAt$1 as replaceAt, setAt, swapAt, toString };
222
+ }
223
+ /**
224
+ * Read the value at `path`. Returns `undefined` if any segment is missing or
225
+ * traverses a non-object. The empty path returns the object itself.
226
+ */
227
+ declare function getAt(obj: unknown, path: FormPath): unknown;
228
+ /**
229
+ * Return a new tree with the value at `path` replaced by `value`. Objects and
230
+ * arrays along the path are cloned (structural sharing elsewhere); missing
231
+ * containers are created as arrays for numeric segments and objects otherwise.
232
+ * The empty path returns `value`. Pure — `obj` is never mutated.
233
+ */
234
+ declare function setAt<T>(obj: T, path: FormPath, value: unknown): T;
235
+ /** Whether `path` is `root` or nested beneath it. */
236
+ declare function isUnder(path: FormPath, root: FormPath): boolean;
237
+ /**
238
+ * Render a {@link FormPath} as flat field-name notation: string segments join
239
+ * with `.` and number segments become `[n]` (`["tags", 0]` → `"tags[0]"`).
240
+ * The inverse of {@link fromString}.
241
+ */
242
+ declare function toString(path: FormPath): string;
243
+ /**
244
+ * Parse flat field-name notation into a {@link FormPath}: integer tokens become
245
+ * number segments (array indices), everything else stays a string. The inverse
246
+ * of {@link toString}.
247
+ */
248
+ declare function fromString(name: string): FormPath;
249
+ /**
250
+ * The deep-key type: every valid {@link toString} path into `T`, as a union of
251
+ * string literals (`"author.name"`, `"tags[0]"`, `"tags[0].label"`, …). This is
252
+ * the type-checked counterpart of a hand-written {@link FormPath} — a typo'd
253
+ * segment or a path into a field that doesn't exist is a compile error. Bounded
254
+ * to a fixed recursion depth ({@link Prev}'s length) so a struct with cycles
255
+ * (there shouldn't be any — form data is JSON-shaped) can't blow up inference.
256
+ *
257
+ * Used by {@link module:effect-forms-svelte/FormStore}'s `field(path)` — see
258
+ * the trade-off recorded in `docs/decisions/` for why this was chosen over the
259
+ * core's (now-removed) opaque `FieldRef`.
260
+ */
261
+ type FieldPath<T, D extends number = 8> = T extends object ? [D] extends [never] ? never : { [K in Extract<keyof T, string>]: `${K}${PathSuffix<T[K], Prev[D]>}`; }[Extract<keyof T, string>] : never;
262
+ /** Recursion-depth countdown used by {@link FieldPath} / {@link PathValue}. */
263
+ type Prev = [never, 0, 1, 2, 3, 4, 5, 6, 7];
264
+ /**
265
+ * The continuation after a key: nothing, an array index (the bracket closes
266
+ * immediately — `[0]`, not `[0.label]` — then {@link PathSuffix} recurses on
267
+ * the element type for whatever comes after), or `.key`.
268
+ */
269
+ type PathSuffix<T, D extends number> = [D] extends [never] ? "" : T extends ReadonlyArray<infer E> ? "" | `[${number}]${PathSuffix<E, Prev[D]>}` : T extends object ? "" | { [K in Extract<keyof T, string>]: `.${K}${PathSuffix<T[K], Prev[D]>}`; }[Extract<keyof T, string>] : "";
270
+ /** The base key of a single dot-segment (`"tags[0]"` → `"tags"`). */
271
+ type SegmentBase<S extends string> = S extends `${infer Base}[${string}` ? Base : S;
272
+ /** The bracket indices of a single dot-segment (`"tags[0][1]"` → `["0", "1"]`), untyped past presence. */
273
+ type SegmentIndices<S extends string> = S extends `${string}[${infer Idx}]${infer Rest}` ? [Idx, ...SegmentIndicesRest<Rest>] : [];
274
+ /** Continuation of {@link SegmentIndices} once the leading (non-bracket) prefix has been consumed. */
275
+ type SegmentIndicesRest<S extends string> = S extends `[${infer Idx}]${infer Rest}` ? [Idx, ...SegmentIndicesRest<Rest>] : [];
276
+ /** Step through an array type once per entry in `Idxs`. */
277
+ type ApplyIndices<T, Idxs extends readonly unknown[]> = Idxs extends [unknown, ...infer Rest] ? T extends ReadonlyArray<infer E> ? ApplyIndices<E, Rest> : never : T;
278
+ /** Resolve a single dot-segment (base key plus any bracket indices) against `T`. */
279
+ type ResolveSegment<T, S extends string> = SegmentBase<S> extends keyof T ? ApplyIndices<T[SegmentBase<S>], SegmentIndices<S>> : never;
280
+ /**
281
+ * The value type at a {@link FieldPath} `P` into `T` — the leaf type `field(path)`
282
+ * resolves to. `PathValue<{ tags: string[] }, "tags[0]">` is `string`.
283
+ */
284
+ type PathValue<T, P extends string> = P extends `${infer Head}.${infer Rest}` ? PathValue<ResolveSegment<T, Head>, Rest> : ResolveSegment<T, P>;
285
+ /**
286
+ * Maps an array index that existed before an index-shifting mutation to where
287
+ * it lives afterwards, or to `null` when the item at that index no longer
288
+ * exists (its per-index state should be dropped). Built by {@link removeAt} /
289
+ * {@link insertAt} / {@link swapAt} / {@link moveAt} / {@link replaceAt}, and
290
+ * consumed by {@link remapIndex} / {@link remapKeyedRecord} and
291
+ * `Errors.remapIndices` to keep per-index state (touched flags, error trees)
292
+ * in sync with the row it was recorded against.
293
+ */
294
+ type IndexMap = (index: number) => number | null;
295
+ /** The {@link IndexMap} for removing the item at `index`: shifts later indices down by one. */
296
+ declare function removeAt(index: number): IndexMap;
297
+ /** The {@link IndexMap} for inserting an item at `index`: shifts `index` and later up by one. */
298
+ declare function insertAt(index: number): IndexMap;
299
+ /** The {@link IndexMap} for swapping the items at `a` and `b`. */
300
+ declare function swapAt(a: number, b: number): IndexMap;
301
+ /** The {@link IndexMap} for moving the item at `from` to `to`, shifting the items between. */
302
+ declare function moveAt(from: number, to: number): IndexMap;
303
+ /** The {@link IndexMap} for replacing the item at `index` in place: drops only that index's state. */
304
+ declare function replaceAt$1(index: number): IndexMap;
305
+ /** The {@link IndexMap} for clearing every item in the array: drops all per-index state. */
306
+ declare const clearAll: IndexMap;
307
+ /**
308
+ * Remap `path`'s index segment immediately under `arrayPath` through `map`.
309
+ * Paths outside `arrayPath`, and `arrayPath` itself (a whole-array message),
310
+ * pass through unchanged; a path whose index segment maps to `null` is
311
+ * dropped (returns `null`) — its item no longer exists. Pure.
312
+ */
313
+ declare function remapIndex(path: FormPath, arrayPath: FormPath, map: IndexMap): FormPath | null;
314
+ /**
315
+ * Remap every entry of a flat, path-keyed record (a {@link toString} key per
316
+ * entry — e.g. touched flags) through {@link remapIndex}. Entries whose path
317
+ * maps to `null` are dropped. Pure.
318
+ */
319
+ declare function remapKeyedRecord<T>(record: Record<string, T>, arrayPath: FormPath, map: IndexMap): Record<string, T>;
320
+ declare namespace Errors_d_exports {
321
+ export { ErrorSource, FormErrors, ROOT_ERROR_KEY, SourcedErrors, TypedFormErrors, clearSourceAt, emptySourced, flattenErrors, getError, hasErrors, issuesToErrors, mergeErrors, mergeSourced, noErrors, remapIndices, replaceAt, setError, setSourceAt };
322
+ }
323
+ /** The root key under which a branch's own (non-field) messages are stored. */
324
+ declare const ROOT_ERROR_KEY = "_errors";
325
+ /**
326
+ * A nested tree of validation messages. Leaves are `string[]`; branches are
327
+ * nested {@link FormErrors}; a branch's own messages live under
328
+ * {@link ROOT_ERROR_KEY}. Always JSON-serialisable.
329
+ */
330
+ interface FormErrors {
331
+ readonly [ROOT_ERROR_KEY]?: readonly string[];
332
+ readonly [key: string]: FormErrors | readonly string[] | undefined;
333
+ }
334
+ /**
335
+ * A {@link FormErrors} tree typed to the shape of `A`, so fields are accessed
336
+ * by name with type-safety: for `A = { name: string; author: { bio: string } }`,
337
+ * `errors.name` is `readonly string[] | undefined` and `errors.author?.bio`
338
+ * likewise. A branch's own (cross-field) messages live under `_errors`. The
339
+ * runtime value is an ordinary {@link FormErrors} — this only refines the type.
340
+ */
341
+ type TypedFormErrors<A> = [NonNullable<A>] extends [ReadonlyArray<infer E>] ? {
342
+ readonly [index: number]: TypedFormErrors<E> | undefined;
343
+ readonly _errors?: readonly string[];
344
+ } : [NonNullable<A>] extends [object] ? { readonly [K in keyof A]?: TypedFormErrors<A[K]>; } & {
345
+ readonly _errors?: readonly string[];
346
+ } : readonly string[];
347
+ /** Build a nested {@link FormErrors} tree from a flat list of {@link FormIssue}s. */
348
+ declare function issuesToErrors(issues: ReadonlyArray<FormIssue>): FormErrors;
349
+ /**
350
+ * Flatten a {@link FormErrors} tree back into a list of {@link FormIssue}s, in
351
+ * depth-first order. The inverse of {@link issuesToErrors} (modulo message
352
+ * ordering within a single field).
353
+ */
354
+ declare function flattenErrors(errors: FormErrors): FormIssue[];
355
+ /** Whether the tree carries any message anywhere. */
356
+ declare function hasErrors(errors: FormErrors): boolean;
357
+ /**
358
+ * Read the messages attached directly to `path` (a leaf's list, or a branch's
359
+ * own {@link ROOT_ERROR_KEY}). Returns `undefined` when the path has no direct
360
+ * messages.
361
+ */
362
+ declare function getError(errors: FormErrors, path: FormPath): readonly string[] | undefined;
363
+ /** Return a new tree with `message` added at `path`. Pure. */
364
+ declare function setError(errors: FormErrors, path: FormPath, message: string): FormErrors;
365
+ /** Combine two error trees into one (left then right). Pure. */
366
+ declare function mergeErrors(a: FormErrors, b: FormErrors): FormErrors;
367
+ /** The empty error tree. */
368
+ declare const noErrors: FormErrors;
369
+ /**
370
+ * Remap the array-index segments of every message under `arrayPath` through
371
+ * `map` (see {@link module:effect-forms/Path.IndexMap}), so an error recorded
372
+ * against a row follows that row through a remove / insert / swap / move.
373
+ * Messages whose index maps to `null` are dropped. Pure.
374
+ */
375
+ declare function remapIndices(errors: FormErrors, arrayPath: FormPath, map: IndexMap): FormErrors;
376
+ /**
377
+ * Return a new tree with the messages *directly at* `path` replaced by
378
+ * `messages` (removing the key when `messages` is empty), leaving every other
379
+ * path — including children of a branch `path` — untouched. Unlike
380
+ * {@link setError} (which appends one message), this is the "this field's
381
+ * validator just ran, here is its complete result" operation a per-field
382
+ * validator needs: it can clear a field back to valid without disturbing
383
+ * sibling or nested messages. Pure.
384
+ */
385
+ declare function replaceAt(errors: FormErrors, path: FormPath, messages: readonly string[]): FormErrors;
386
+ /**
387
+ * Where a message in a {@link FormErrors} tree came from. `"server"` messages
388
+ * come from the last submission response; `"client"` messages come from
389
+ * browser-side validation (schema re-run, per-field validators, client-run
390
+ * `refineEffect` refinements). Kept as parallel trees (see {@link
391
+ * SourcedErrors}) rather than tagging each message, so a UI can style sources
392
+ * differently and client revalidation can clear only its own tree, leaving
393
+ * server-reported failures in place until their field actually changes.
394
+ */
395
+ type ErrorSource = "client" | "server";
396
+ /** Parallel error trees, one per {@link ErrorSource}, plus their {@link mergeSourced} view. */
397
+ interface SourcedErrors {
398
+ readonly client: FormErrors;
399
+ readonly server: FormErrors;
400
+ }
401
+ /** Both source trees empty. */
402
+ declare const emptySourced: SourcedErrors;
403
+ /** Combine both source trees into the single tree a UI reads (server first, then client). */
404
+ declare function mergeSourced(sourced: SourcedErrors): FormErrors;
405
+ /**
406
+ * Return new {@link SourcedErrors} with `source`'s tree updated at `path` via
407
+ * {@link replaceAt} (the other source's tree is untouched). Pure.
408
+ */
409
+ declare function setSourceAt(sourced: SourcedErrors, source: ErrorSource, path: FormPath, messages: readonly string[]): SourcedErrors;
410
+ /** Clear `source`'s messages at `path` (and nowhere else). Shorthand for {@link setSourceAt} with `[]`. */
411
+ declare function clearSourceAt(sourced: SourcedErrors, source: ErrorSource, path: FormPath): SourcedErrors;
412
+ declare namespace Field_d_exports {
413
+ export { AnyFieldDef, ArrayFieldDef, DecodedFromFields, EncodedFromFields, FieldDef, FieldsRecord, defaultEncodedValues, defaultItemAt, defaultValue, isArrayFieldDef, isFieldDef, makeArrayField, makeField, schemaAt };
414
+ }
415
+ /** A scalar field: a key bound to a leaf (or nested struct) schema. */
416
+ interface FieldDef<K extends string, S extends Schema.Top> {
417
+ readonly _tag: "field";
418
+ readonly key: K;
419
+ readonly schema: S;
420
+ }
421
+ /** An array field: a key bound to an item schema; the field is a list of items. */
422
+ interface ArrayFieldDef<K extends string, S extends Schema.Top> {
423
+ readonly _tag: "array";
424
+ readonly key: K;
425
+ readonly itemSchema: S;
426
+ }
427
+ /** Either field shape, with the key / schema types erased. */
428
+ type AnyFieldDef = FieldDef<string, Schema.Top> | ArrayFieldDef<string, Schema.Top>;
429
+ /** A record of fields, keyed by field name — the builder's accumulated shape. */
430
+ type FieldsRecord = Record<string, AnyFieldDef>;
431
+ /** Whether `def` is an {@link ArrayFieldDef}. */
432
+ declare const isArrayFieldDef: (def: AnyFieldDef) => def is ArrayFieldDef<string, Schema.Top>;
433
+ /** Whether `def` is a scalar {@link FieldDef}. */
434
+ declare const isFieldDef: (def: AnyFieldDef) => def is FieldDef<string, Schema.Top>;
435
+ /** Construct a scalar {@link FieldDef}. */
436
+ declare const makeField: <K extends string, S extends Schema.Top>(key: K, schema: S) => FieldDef<K, S>;
437
+ /** Construct an {@link ArrayFieldDef}. */
438
+ declare const makeArrayField: <K extends string, S extends Schema.Top>(key: K, itemSchema: S) => ArrayFieldDef<K, S>;
439
+ /** The encoded (form-input) data type for a record of fields. */
440
+ type EncodedFromFields<T extends FieldsRecord> = { readonly [K in keyof T]: T[K] extends FieldDef<string, infer S> ? S["Encoded"] : T[K] extends ArrayFieldDef<string, infer S> ? ReadonlyArray<S["Encoded"]> : never; };
441
+ /** The decoded (validated) data type for a record of fields. */
442
+ type DecodedFromFields<T extends FieldsRecord> = { readonly [K in keyof T]: T[K] extends FieldDef<string, infer S> ? S["Type"] : T[K] extends ArrayFieldDef<string, infer S> ? ReadonlyArray<S["Type"]> : never; };
443
+ /**
444
+ * Derive a blank *encoded* default from a schema AST: a struct recurses into
445
+ * its properties, an array seeds to `[]`, and any scalar seeds to `""` (an
446
+ * HTML input's empty value). Conservative by design — a value that will not
447
+ * decode cleanly is still a valid *starting* value the user edits.
448
+ */
449
+ declare function defaultValue(schema: Schema.Top): unknown;
450
+ /**
451
+ * Walk `schema`'s AST along `path` — through struct properties and array
452
+ * elements alike — and return the AST found there, or `undefined` if `path`
453
+ * doesn't resolve (a missing property, or a non-array indexed). Unlike a
454
+ * {@link FieldsRecord} lookup, this works for any nesting depth and for
455
+ * schemas built directly with `Form.make` (no builder involved).
456
+ */
457
+ declare function schemaAt(schema: Schema.Top, path: FormPath): SchemaAST.AST | undefined;
458
+ /**
459
+ * Derive a blank encoded item for the array field at `path` (any nesting
460
+ * depth) by walking `schema`'s AST there — the nested-array counterpart to
461
+ * {@link defaultValue}. Returns `""` when `path` doesn't resolve to an array.
462
+ */
463
+ declare function defaultItemAt(schema: Schema.Top, path: FormPath): unknown;
464
+ /** Seed a blank encoded value for every field: `[]` for arrays, `""` otherwise. */
465
+ declare function defaultEncodedValues(fields: FieldsRecord): Record<string, unknown>;
466
+ declare namespace FormBuilder_d_exports {
467
+ export { FormBuilder, TypeId, asyncRefinements, buildSchema, empty, filterOutputToFormIssues, isFormBuilder, make$2 as make, runAsyncRefinements };
468
+ }
469
+ /** Brand for a {@link FormBuilder}. */
470
+ declare const TypeId: unique symbol;
471
+ type TypeId = typeof TypeId;
472
+ /** A synchronous cross-field refinement — becomes a struct-level schema check. */
473
+ interface SyncRefinement {
474
+ readonly _tag: "sync";
475
+ readonly fn: (values: unknown) => Schema.FilterOutput;
476
+ }
477
+ /** An asynchronous cross-field refinement — run by the Effect validate path. */
478
+ interface AsyncRefinement {
479
+ readonly _tag: "async";
480
+ readonly fn: (values: unknown) => Effect.Effect<Schema.FilterOutput, never, unknown>;
481
+ }
482
+ type Refinement = SyncRefinement | AsyncRefinement;
483
+ /**
484
+ * A form assembled field by field. `TFields` accumulates the field record;
485
+ * `R` accumulates the schemas' decoding-service requirements. Immutable — every
486
+ * method returns a new builder.
487
+ */
488
+ interface FormBuilder<TFields extends FieldsRecord, R> {
489
+ readonly [TypeId]: TypeId;
490
+ readonly fields: TFields;
491
+ readonly refinements: ReadonlyArray<Refinement>;
492
+ readonly _R?: R;
493
+ /** Add a scalar field from a {@link FieldDef}. */
494
+ addField<K extends string, S extends Schema.Top>(field: FieldDef<K, S>): FormBuilder<TFields & { readonly [key in K]: FieldDef<K, S>; }, R | S["DecodingServices"]>;
495
+ /** Add an array field from an {@link ArrayFieldDef}. */
496
+ addField<K extends string, S extends Schema.Top>(field: ArrayFieldDef<K, S>): FormBuilder<TFields & { readonly [key in K]: ArrayFieldDef<K, S>; }, R | S["DecodingServices"]>;
497
+ /** Add a scalar field from a key and schema. */
498
+ addField<K extends string, S extends Schema.Top>(key: K, schema: S): FormBuilder<TFields & { readonly [key in K]: FieldDef<K, S>; }, R | S["DecodingServices"]>;
499
+ /** Fold another builder's fields and refinements into this one. */
500
+ merge<TFields2 extends FieldsRecord, R2>(other: FormBuilder<TFields2, R2>): FormBuilder<TFields & TFields2, R | R2>;
501
+ /**
502
+ * Attach a synchronous whole-form validation. The predicate sees the decoded
503
+ * values and returns a {@link Schema.FilterOutput} (`undefined` / `true` to
504
+ * pass, a message / `{ path, issue }` / list to fail).
505
+ */
506
+ refine(predicate: (values: DecodedFromFields<TFields>) => Schema.FilterOutput): FormBuilder<TFields, R>;
507
+ /** Attach an asynchronous whole-form validation (run by the validate path). */
508
+ refineEffect<RD>(predicate: (values: DecodedFromFields<TFields>) => Effect.Effect<Schema.FilterOutput, never, RD>): FormBuilder<TFields, R | RD>;
509
+ }
510
+ /** Whether `u` is a {@link FormBuilder}. */
511
+ declare const isFormBuilder: (u: unknown) => u is FormBuilder<FieldsRecord, unknown>;
512
+ /** The empty builder — the starting point for {@link FormBuilder.addField}. */
513
+ declare const make$2: FormBuilder<Record<never, never>, never>;
514
+ /** Alias of {@link make} — the empty builder. */
515
+ declare const empty: FormBuilder<Record<never, never>, never>;
516
+ /**
517
+ * Collapse a builder into an Effect Schema: a `Schema.Struct` of the fields
518
+ * (array fields as `Schema.Array(item)`) with every synchronous refinement
519
+ * applied as a struct-level check. Ready for {@link module:effect-forms/Form}.
520
+ */
521
+ declare function buildSchema<TFields extends FieldsRecord, R>(self: FormBuilder<TFields, R>): Schema.Codec<DecodedFromFields<TFields>, EncodedFromFields<TFields>, R>;
522
+ /** The asynchronous refinements carried on a builder, as bare predicates. */
523
+ declare function asyncRefinements<TFields extends FieldsRecord, R>(self: FormBuilder<TFields, R>): ReadonlyArray<(values: DecodedFromFields<TFields>) => Effect.Effect<Schema.FilterOutput, never, R>>;
524
+ /**
525
+ * Run every asynchronous refinement on `self` against `values` and collect
526
+ * their failures as {@link FormIssue}s (empty when all pass). Runs them
527
+ * concurrently; requirements propagate on the `R` channel.
528
+ */
529
+ declare function runAsyncRefinements<TFields extends FieldsRecord, R>(self: FormBuilder<TFields, R>, values: DecodedFromFields<TFields>): Effect.Effect<ReadonlyArray<FormIssue>, never, R>;
530
+ declare namespace State_d_exports {
531
+ export { FormState, FormStateSchema, MakeFormStateOptions, isValid, make$1 as make, withError, withMessage };
532
+ }
533
+ /**
534
+ * A form snapshot.
535
+ *
536
+ * - `id` distinguishes multiple forms on one page.
537
+ * - `valid` is whether the last validation succeeded.
538
+ * - `posted` is whether this snapshot came from a submission (vs. a seeded
539
+ * empty form) — drives "show errors only after submit" UIs.
540
+ * - `data` is the current values (typed output on success; posted values
541
+ * merged over defaults on failure, so the form re-renders what the user
542
+ * typed).
543
+ * - `errors` is the nested {@link FormErrors} tree.
544
+ * - `message` is an optional status payload (a free-form status payload).
545
+ * - `constraints` is the optional derived {@link InputConstraints} tree.
546
+ */
547
+ interface FormState<A, Message = unknown> {
548
+ readonly id: string;
549
+ readonly valid: boolean;
550
+ readonly posted: boolean;
551
+ readonly data: A;
552
+ readonly errors: FormErrors;
553
+ readonly message?: Message | undefined;
554
+ readonly constraints?: InputConstraints | undefined;
555
+ }
556
+ /** Options for {@link make}. */
557
+ interface MakeFormStateOptions<Message> {
558
+ readonly id?: string;
559
+ readonly valid?: boolean;
560
+ readonly posted?: boolean;
561
+ readonly errors?: FormErrors;
562
+ readonly message?: Message;
563
+ readonly constraints?: InputConstraints;
564
+ }
565
+ /** Build a {@link FormState} from data plus overrides. `valid` defaults to "no errors". */
566
+ declare function make$1<A, Message = unknown>(data: A, options?: MakeFormStateOptions<Message>): FormState<A, Message>;
567
+ /** Return a new state carrying `message` (and not otherwise changed). */
568
+ declare function withMessage<A, Message>(state: FormState<A, Message>, message: Message): FormState<A, Message>;
569
+ /**
570
+ * Return a new state with `message` added at `path` and `valid` forced to
571
+ * `false` — for attaching a
572
+ * server-side validation failure to a specific field after the schema passed.
573
+ */
574
+ declare function withError<A, Message>(state: FormState<A, Message>, path: FormPath, message: string): FormState<A, Message>;
575
+ /** Whether the state is valid. */
576
+ declare const isValid: <A, Message>(state: FormState<A, Message>) => boolean;
577
+ /** Effect Schema describing the JSON shape of {@link FormState} for transport. */
578
+ declare function FormStateSchema<DataSchema extends Schema.Top>(data: DataSchema): Schema.Struct<{
579
+ readonly id: Schema.String;
580
+ readonly valid: Schema.Boolean;
581
+ readonly posted: Schema.Boolean;
582
+ readonly data: DataSchema;
583
+ readonly errors: Schema.$Record<Schema.String, Schema.Unknown>;
584
+ readonly message: Schema.optional<Schema.Unknown>;
585
+ readonly constraints: Schema.optional<Schema.$Record<Schema.String, Schema.Unknown>>;
586
+ }>;
587
+ declare namespace Form_d_exports {
588
+ export { Form, FormInput, FromBuilderOptions, MakeFormOptions, fromBuilder, make };
589
+ }
590
+ /** Everything {@link Form.validate} accepts. `null` / `undefined` mean "no submission". */
591
+ type FormInput = FormData | URLSearchParams | Request | Record<string, unknown> | null | undefined;
592
+ /** Options for {@link make}. */
593
+ interface MakeFormOptions<S extends Schema.Top> {
594
+ /** Distinguishes multiple forms on a page. Defaults to `"default"`. */
595
+ readonly id?: string;
596
+ /** The blank-form values — what {@link Form.empty} seeds and the merge base on failure. */
597
+ readonly defaults: S["Type"];
598
+ /** Override the per-leaf message rendering. */
599
+ readonly leafFormatter?: LeafFormatter;
600
+ /** Constraint overrides deep-merged over the AST-derived tree. */
601
+ readonly constraints?: InputConstraints;
602
+ /** Derive {@link InputConstraints} from the schema AST (default `true`). */
603
+ readonly deriveConstraints?: boolean;
604
+ }
605
+ /** A schema bound to a form: a validator plus an empty-state factory. */
606
+ interface Form<S extends Schema.Top, Message = unknown> {
607
+ readonly schema: S;
608
+ readonly id: string;
609
+ readonly constraints: InputConstraints;
610
+ /** Seed a blank, unposted form from the defaults. */
611
+ empty(overrides?: MakeFormStateOptions<Message>): FormState<S["Type"], Message>;
612
+ /** Parse a form body (no decode) into a structured object. */
613
+ parse(input: FormData | URLSearchParams | Record<string, unknown>): Record<string, unknown>;
614
+ /**
615
+ * Validate a submission into a {@link FormState}. Never fails on the error
616
+ * channel — schema failures become `errors` on the returned (invalid) state.
617
+ */
618
+ validate(input: FormInput): Effect.Effect<FormState<S["Type"], Message>, never, S["DecodingServices"]>;
619
+ }
620
+ /**
621
+ * Bind `schema` to a form. See the module docs for the contract. The returned
622
+ * {@link Form} is stateless and reusable across requests.
623
+ */
624
+ declare function make<S extends Schema.Top, Message = unknown>(schema: S, options: MakeFormOptions<S>): Form<S, Message>;
625
+ /** Options for {@link fromBuilder}. Like {@link MakeFormOptions} but `defaults` is optional. */
626
+ interface FromBuilderOptions<TFields extends FieldsRecord> {
627
+ /** Distinguishes multiple forms on a page. Defaults to `"default"`. */
628
+ readonly id?: string;
629
+ /** Blank-form values. Defaults to a schema-derived encoded blank per field. */
630
+ readonly defaults?: EncodedFromFields<TFields>;
631
+ /** Override the per-leaf message rendering. */
632
+ readonly leafFormatter?: LeafFormatter;
633
+ /** Constraint overrides deep-merged over the AST-derived tree. */
634
+ readonly constraints?: InputConstraints;
635
+ /** Derive {@link InputConstraints} from the schema AST (default `true`). */
636
+ readonly deriveConstraints?: boolean;
637
+ }
638
+ /**
639
+ * Bind a {@link FormBuilder} to a {@link Form}. Collapses the builder to a
640
+ * schema (see {@link buildSchema}), then behaves like {@link make} — except its
641
+ * `validate` also runs the builder's *asynchronous* refinements after a
642
+ * successful decode, folding any failures into the same nested error tree. When
643
+ * `defaults` is omitted it is derived from the fields (blank encoded values).
644
+ */
645
+ declare function fromBuilder<TFields extends FieldsRecord, R, Message = unknown>(builder: FormBuilder<TFields, R>, options?: FromBuilderOptions<TFields>): Form<Schema.Codec<DecodedFromFields<TFields>, EncodedFromFields<TFields>, R>, Message>;
646
+ declare namespace FormData_d_exports {
647
+ export { FormBodySource, formDataToObject, parseFieldPath, requestToObject };
648
+ }
649
+ /**
650
+ * @copyright Thomas Foster
651
+ *
652
+ * Structured parsing of an HTML form body into a plain object the schema can
653
+ * decode.
654
+ *
655
+ * Browsers flatten structured data into `name=value` pairs using bracket /
656
+ * dot path notation (`author.name`, `author[name]`, `tags[0]`, `tags[]`).
657
+ * {@link formDataToObject} reverses that into a nested object / array tree —
658
+ * the framework-neutral step that turns a form body into structured data, before handing it
659
+ * to a validator. Coercion (strings → numbers / booleans / dates) is then the
660
+ * schema's job: decode the result with a schema built from coercing leaves
661
+ * (`Schema.FiniteFromString`, `Schema.BooleanFromString`, the {@link
662
+ * module:effect-forms/Coerce} helpers, …).
663
+ *
664
+ * `File` values pass through untouched; repeated bare keys collapse into an
665
+ * array. The parser is total and never throws.
666
+ *
667
+ * @module effect-forms/FormData
668
+ */
669
+ /** Anything that can be parsed into a structured form object. */
670
+ type FormBodySource = FormData | URLSearchParams | Iterable<readonly [string, FormDataEntryValue | string]>;
671
+ /**
672
+ * Split a flat field name into its path tokens. Supports dot notation
673
+ * (`a.b`), bracket notation (`a[b]`, `a[0]`), and the trailing append form
674
+ * (`a[]`, represented by an empty-string token). `"a[b][0]"` → `["a","b","0"]`;
675
+ * `"a.b[]"` → `["a","b",""]`.
676
+ */
677
+ declare function parseFieldPath(name: string): string[];
678
+ /**
679
+ * Parse a form body into a nested object / array tree. Field names use dot or
680
+ * bracket path notation (see {@link parseFieldPath}); repeated bare keys
681
+ * become arrays; `File` values pass through. The result is ready to hand to
682
+ * `Schema.decodeUnknown` with a schema whose leaves coerce the string inputs.
683
+ */
684
+ declare function formDataToObject(source: FormBodySource): Record<string, unknown>;
685
+ /** Read and parse a `Request`'s form body. Errors (e.g. wrong content-type) reject. */
686
+ declare function requestToObject(request: Request): Promise<Record<string, unknown>>;
687
+ declare namespace Mode_d_exports {
688
+ export { FormMode, ParsedMode, RevalidateMode, parse };
689
+ }
690
+ /** What re-validates a posted form as the user keeps editing. See {@link ParsedMode.revalidate}. */
691
+ type RevalidateMode = "onChange" | "onBlur" | "never";
692
+ /**
693
+ * How a form decides when to validate, and whether it auto-submits.
694
+ *
695
+ * - `onSubmit` (the default) cannot auto-submit or debounce.
696
+ * - `onBlur` may auto-submit; debounce is not meaningful.
697
+ * - `onChange` may auto-submit and may debounce.
698
+ *
699
+ * `revalidate` is independent of `validation` and applies to every mode —
700
+ * see the module docs.
701
+ */
702
+ type FormMode = ({
703
+ readonly validation?: "onSubmit";
704
+ readonly autoSubmit?: false;
705
+ readonly debounce?: never;
706
+ } | {
707
+ readonly validation: "onBlur";
708
+ readonly autoSubmit?: boolean;
709
+ readonly debounce?: never;
710
+ } | {
711
+ readonly validation: "onChange";
712
+ readonly debounce?: Duration.Input;
713
+ readonly autoSubmit?: boolean;
714
+ }) & {
715
+ readonly revalidate?: RevalidateMode;
716
+ };
717
+ /** The fully-resolved form of a {@link FormMode} — every field decided. */
718
+ interface ParsedMode {
719
+ readonly validation: "onSubmit" | "onBlur" | "onChange";
720
+ /** Debounce in milliseconds, or `null` for no debounce. */
721
+ readonly debounce: number | null;
722
+ readonly autoSubmit: boolean;
723
+ /** What re-validates the form once it has been posted at least once. Defaults to `"onChange"`. */
724
+ readonly revalidate: RevalidateMode;
725
+ }
726
+ /**
727
+ * Normalise a {@link FormMode} into a {@link ParsedMode}. An absent mode is
728
+ * `onSubmit`. `debounce` is resolved to milliseconds and only applies in
729
+ * `onChange`; `autoSubmit` is only honoured for `onBlur` / `onChange`;
730
+ * `revalidate` defaults to `"onChange"` (TanStack's / RHF's default
731
+ * post-submit behaviour) regardless of the primary `validation` mode.
732
+ */
733
+ declare function parse(mode?: FormMode): ParsedMode;
734
+ declare namespace Result_d_exports {
735
+ export { FormActionData, FormActionResult, FormErrorResult, FormFailure, FormRedirect, FormSuccess, errorResult, failure, redirect, success };
736
+ }
737
+ /** The submission validated and the command ran; carries the resulting state. */
738
+ interface FormSuccess<A, Message> {
739
+ readonly _tag: "Success";
740
+ readonly status: number;
741
+ readonly state: FormState<A, Message>;
742
+ }
743
+ /** The submission was rejected (validation or a user-fixable command failure). */
744
+ interface FormFailure<A, Message> {
745
+ readonly _tag: "Failure";
746
+ readonly status: number;
747
+ readonly state: FormState<A, Message>;
748
+ }
749
+ /** The handler asks the client to navigate elsewhere (post-success redirect). */
750
+ interface FormRedirect {
751
+ readonly _tag: "Redirect";
752
+ readonly status: number;
753
+ readonly location: string;
754
+ }
755
+ /** An unexpected (non-user-fixable) error: surface it, don't bind it to fields. */
756
+ interface FormErrorResult {
757
+ readonly _tag: "Error";
758
+ readonly status: number;
759
+ readonly message: string;
760
+ }
761
+ /** The outcome of a form submission. */
762
+ type FormActionResult<A, Message = unknown> = FormSuccess<A, Message> | FormFailure<A, Message> | FormRedirect | FormErrorResult;
763
+ /**
764
+ * The data a server form action returns to the client on a non-redirect
765
+ * outcome — the wire contract a client adapter applies back to its store. A
766
+ * validation failure carries the {@link FormState} (`form`); an unexpected
767
+ * error carries a root `message`.
768
+ */
769
+ interface FormActionData<A = unknown, Message = unknown> {
770
+ /** The form state (nested errors + posted values) for the client to apply. */
771
+ readonly form?: FormState<A, Message>;
772
+ /** A root-level message for an unexpected (non-field) failure. */
773
+ readonly message?: string;
774
+ }
775
+ /** Construct a {@link FormSuccess} (default status 200). */
776
+ declare const success: <A, Message>(state: FormState<A, Message>, status?: number) => FormActionResult<A, Message>;
777
+ /** Construct a {@link FormFailure} (default status 400). */
778
+ declare const failure: <A, Message>(state: FormState<A, Message>, status?: number) => FormActionResult<A, Message>;
779
+ /** Construct a {@link FormRedirect} (default status 303). */
780
+ declare const redirect: <A, Message>(location: string, status?: number) => FormActionResult<A, Message>;
781
+ /** Construct a {@link FormErrorResult} (default status 500). */
782
+ declare const errorResult: <A, Message>(message: string, status?: number) => FormActionResult<A, Message>;
783
+ declare namespace Validate_d_exports {
784
+ export { AsyncFieldValidator, FieldTrigger, FieldValidators, ResolvedFieldTrigger, SyncFieldValidator, ValidationResult, resolveFieldTrigger, runAsyncFieldValidator, runSyncFieldValidator, validateValue, validateValueSync };
785
+ }
786
+ /** The outcome of validating a value against a schema. */
787
+ type ValidationResult<A> = {
788
+ readonly valid: true;
789
+ readonly data: A;
790
+ } | {
791
+ readonly valid: false;
792
+ readonly errors: FormErrors;
793
+ readonly issues: ReadonlyArray<FormIssue>;
794
+ };
795
+ /**
796
+ * Decode `input` through `schema`, returning a {@link ValidationResult} on the
797
+ * success channel (decode failures become `valid: false`, not an Effect
798
+ * failure). Decoding-service requirements propagate on the `R` channel.
799
+ */
800
+ declare function validateValue<S extends Schema.Top>(schema: S, input: unknown, format?: LeafFormatter): Effect.Effect<ValidationResult<S["Type"]>, never, S["DecodingServices"]>;
801
+ /**
802
+ * Synchronous {@link validateValue} for schemas with no decoding-service
803
+ * requirements (`R = never`) and no async decoding — the common case for plain
804
+ * form schemas. {@link validateValue} never fails on the error channel, so
805
+ * running it with {@link Effect.runSync} is total.
806
+ */
807
+ declare function validateValueSync<S extends Schema.Top>(schema: S, input: unknown, format?: LeafFormatter): ValidationResult<S["Type"]>;
808
+ /**
809
+ * A synchronous per-field validator — TanStack Form's `validators.onChange` /
810
+ * `onBlur` shape. Sees the field's decoded value and returns a {@link
811
+ * Schema.FilterOutput} (`undefined` / `true` to pass, a message / issue /
812
+ * list to fail), the same vocabulary a schema `check` or a {@link
813
+ * module:effect-forms/FormBuilder} `refine` predicate uses.
814
+ */
815
+ type SyncFieldValidator<A> = (value: A) => Schema.FilterOutput;
816
+ /**
817
+ * An asynchronous per-field validator — TanStack Form's `onChangeAsync` /
818
+ * `onBlurAsync`. An Effect (not a `Promise`) so its `R` channel can demand
819
+ * services (an API client, a cache) the way a {@link
820
+ * module:effect-forms/FormBuilder} `refineEffect` refinement does; a caller
821
+ * running this client-side supplies `R` via a `Layer` / runtime, or keeps it
822
+ * `never` for a plain `fetch`-based check.
823
+ */
824
+ type AsyncFieldValidator<A, R = never> = (value: A) => Effect.Effect<Schema.FilterOutput, never, R>;
825
+ /**
826
+ * The validators one field declares, mirroring TanStack Form's per-field
827
+ * `validators` option. `asyncDebounceMs` (default `0`, meaning no debounce)
828
+ * governs both async variants; see {@link runAsyncFieldValidator} for how the
829
+ * debounce and cancellation actually work.
830
+ */
831
+ interface FieldValidators<A, R = never> {
832
+ readonly onChange?: SyncFieldValidator<A>;
833
+ readonly onBlur?: SyncFieldValidator<A>;
834
+ readonly onChangeAsync?: AsyncFieldValidator<A, R>;
835
+ readonly onBlurAsync?: AsyncFieldValidator<A, R>;
836
+ readonly asyncDebounceMs?: number;
837
+ }
838
+ /**
839
+ * Run a {@link SyncFieldValidator}, folding its {@link Schema.FilterOutput}
840
+ * into {@link FormIssue}s (empty on success). Total — a sync field validator
841
+ * cannot fail the Effect error channel.
842
+ */
843
+ declare function runSyncFieldValidator<A>(validator: SyncFieldValidator<A>, value: A): ReadonlyArray<FormIssue>;
844
+ /**
845
+ * Build the Effect an {@link AsyncFieldValidator} run is: sleep `debounceMs`
846
+ * (a no-op at `0`, the default), then run `validator` and fold its output
847
+ * into {@link FormIssue}s.
848
+ *
849
+ * The debounce is a plain {@link Effect.sleep}, so it rides Effect's `Clock`
850
+ * — deterministic and instantly-advanceable under `TestClock` in tests, real
851
+ * time otherwise (no `setTimeout` bookkeeping for a caller to manage).
852
+ * "Latest wins" falls out of ordinary fiber interruption: a caller re-running
853
+ * this on every keystroke forks a fresh fiber per call and interrupts the
854
+ * previous one before forking the next — interrupting mid-sleep skips the
855
+ * validator entirely (a true debounce, not just a discarded stale result),
856
+ * and interrupting mid-validator aborts whatever the validator was doing
857
+ * (e.g. an in-flight `fetch` wrapped as an Effect).
858
+ */
859
+ declare function runAsyncFieldValidator<A, R>(validator: AsyncFieldValidator<A, R>, value: A, debounceMs?: number): Effect.Effect<ReadonlyArray<FormIssue>, never, R>;
860
+ /** Which trigger a {@link FieldValidators} sync/async pair reacts to. */
861
+ type FieldTrigger = "change" | "blur";
862
+ /** The validator(s) (if any) that apply for one field trigger — see {@link resolveFieldTrigger}. */
863
+ interface ResolvedFieldTrigger<A, R = never> {
864
+ readonly sync: SyncFieldValidator<A> | undefined;
865
+ readonly async: AsyncFieldValidator<A, R> | undefined;
866
+ readonly asyncDebounceMs: number;
867
+ }
868
+ /**
869
+ * Pick `validators`' sync/async pair for `trigger` (TanStack's `onChange` /
870
+ * `onBlur` vs `onChangeAsync` / `onBlurAsync`). A reactive store calls this
871
+ * once per trigger instead of repeating the trigger ternary at each call
872
+ * site — the pure decision half of what {@link module:effect-forms-svelte/FormStore}'s
873
+ * field-trigger dispatch runs.
874
+ */
875
+ declare function resolveFieldTrigger<A, R = never>(validators: FieldValidators<A, R> | undefined, trigger: FieldTrigger): ResolvedFieldTrigger<A, R>;
876
+ //#endregion
877
+ export { Coerce_d_exports as Coerce, Command_d_exports as Command, Constraints_d_exports as Constraints, Errors_d_exports as Errors, Field_d_exports as Field, Form_d_exports as Form, FormBuilder_d_exports as FormBuilder, FormData_d_exports as FormData, Issues_d_exports as Issues, Mode_d_exports as Mode, Path_d_exports as Path, Result_d_exports as Result, State_d_exports as State, Validate_d_exports as Validate };
878
+ //# sourceMappingURL=index.d.ts.map