react-f0rm 1.1.0 → 1.2.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 (61) hide show
  1. package/README.md +152 -32
  2. package/dist/devtools/index.cjs.js +1 -1
  3. package/dist/devtools/index.cjs.js.map +1 -1
  4. package/dist/devtools/index.d.ts +2 -2
  5. package/dist/devtools/index.mjs +1 -1
  6. package/dist/devtools/index.mjs.map +1 -1
  7. package/dist/errors-CxSjrWJO.cjs.js +2 -0
  8. package/dist/errors-CxSjrWJO.cjs.js.map +1 -0
  9. package/dist/errors-CzWtwjO0.mjs +2 -0
  10. package/dist/errors-CzWtwjO0.mjs.map +1 -0
  11. package/dist/form-CvmWHUrd.d.ts +423 -0
  12. package/dist/index.cjs.js +1 -1
  13. package/dist/index.cjs.js.map +1 -1
  14. package/dist/index.d.ts +786 -102
  15. package/dist/index.mjs +1 -1
  16. package/dist/index.mjs.map +1 -1
  17. package/dist/index.umd.js +801 -354
  18. package/dist/index.umd.js.map +1 -1
  19. package/dist/index.umd.min.js +2 -2
  20. package/dist/index.umd.min.js.map +1 -1
  21. package/dist/persist.cjs.js +2 -0
  22. package/dist/persist.cjs.js.map +1 -0
  23. package/dist/persist.d.ts +49 -0
  24. package/dist/persist.mjs +2 -0
  25. package/dist/persist.mjs.map +1 -0
  26. package/dist/resolvers/standard-schema.cjs.js +1 -1
  27. package/dist/resolvers/standard-schema.cjs.js.map +1 -1
  28. package/dist/resolvers/standard-schema.d.ts +7 -5
  29. package/dist/resolvers/standard-schema.mjs +1 -1
  30. package/dist/resolvers/standard-schema.mjs.map +1 -1
  31. package/dist/resolvers/yup.cjs.js +1 -1
  32. package/dist/resolvers/yup.cjs.js.map +1 -1
  33. package/dist/resolvers/yup.d.ts +1 -1
  34. package/dist/resolvers/yup.mjs +1 -1
  35. package/dist/resolvers/yup.mjs.map +1 -1
  36. package/dist/resolvers/zod.cjs.js +1 -1
  37. package/dist/resolvers/zod.cjs.js.map +1 -1
  38. package/dist/resolvers/zod.d.ts +1 -1
  39. package/dist/resolvers/zod.mjs +1 -1
  40. package/dist/resolvers/zod.mjs.map +1 -1
  41. package/dist/server/index.cjs.js +2 -0
  42. package/dist/server/index.cjs.js.map +1 -0
  43. package/dist/server/index.d.ts +77 -0
  44. package/dist/server/index.mjs +2 -0
  45. package/dist/server/index.mjs.map +1 -0
  46. package/dist/validate-B1Gdjeaq.mjs +2 -0
  47. package/dist/validate-B1Gdjeaq.mjs.map +1 -0
  48. package/dist/validate-CUmNZqg6.d.ts +238 -0
  49. package/dist/validate-DAfz8Nbb.cjs.js +2 -0
  50. package/dist/validate-DAfz8Nbb.cjs.js.map +1 -0
  51. package/dist/values-B1IV-6V4.mjs +2 -0
  52. package/dist/values-B1IV-6V4.mjs.map +1 -0
  53. package/dist/values-CDNAYEOB.cjs.js +2 -0
  54. package/dist/values-CDNAYEOB.cjs.js.map +1 -0
  55. package/package.json +71 -24
  56. package/dist/form-BGWPwts2.mjs +0 -2
  57. package/dist/form-BGWPwts2.mjs.map +0 -1
  58. package/dist/form-BiDaJLjD.d.ts +0 -826
  59. package/dist/form-DwuY91QB.cjs.js +0 -2
  60. package/dist/form-DwuY91QB.cjs.js.map +0 -1
  61. package/dist/validate-2XUilILy.d.ts +0 -22
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","sources":["../../src/server.ts","../../src/form.ts"],"sourcesContent":["/**\n * Server-side validation entry — `react-f0rm/server`.\n *\n * The core (`./form`) is pure TypeScript with zero React imports, so it\n * already runs anywhere Node does; what the server lacked is an entry\n * that never pulls React into the module graph at all — the main entry\n * re-exports the hooks/components, and a Server Action or RSC that only\n * wants to check a payload should not have to depend on them. This module\n * is that entry: values in, one whole-form validation round, structured\n * result out.\n *\n * It plays the role TanStack Form gives `createServerValidate`, minus the\n * action prop: their API wraps validation inside a generated server\n * action, while react-f0rm keeps the values store the single source of\n * truth — {@link validateValues} is a plain function over values (the\n * same contract the client-side `trigger` has over a form instance), so\n * it composes into any server framework's handler instead of owning it.\n *\n * Like the resolvers and devtools it is intentionally NOT re-exported\n * from the main entry: importing `react-f0rm/server` is the only way this\n * code reaches a bundle, so client builds that never validate on the\n * server stay at baseline size.\n */\nimport createForm, {getErrors, getValues, trigger} from './form';\nimport type {FieldErrorEntry, Options, ValidationOutcome} from './form';\n\n// Building a branded ValidationOutcome server-side (schema adapters, or a\n// hand-written validate that returns parsed values) needs the brand symbol\n// itself; importing it from the package root would drag the React graph\n// back in, so it is re-exported here together with its result type.\nexport {VALIDATION_OUTCOME} from './form';\nexport type {ValidationOutcome};\n\n/** The outcome of {@link validateValues}: the error-free flag, the values\n * once the round has landed (schema-coerced where the validator produced\n * parsed values), and every error the round wrote as flat entries. */\nexport type ValidateValuesResult<T extends Record<string, any> = any> = {\n /** Whether the round landed no errors — `trigger`'s boolean. An invalid\n * payload is a normal outcome, never a rejection: unlike\n * `ensureValidate`, server callers learn validity from data instead of\n * catching, because both branches are interesting on the server\n * (persist vs. bounce back to the client). */\n valid: boolean;\n /** The values after the round. When the validator returned a branded\n * {@link ValidationOutcome} with `values`, those parsed values are the\n * baseline `getValues` layers over the input — schema coerce/transform\n * output included — so this is the tree to persist or feed onward, not\n * necessarily the object passed in. Deep-equals the input otherwise. */\n values: T;\n /** Every error the round landed, flattened to `{path, type, message}`\n * entries — the same list {@link getErrors} hands out on the client.\n * Feed it to `setServerErrors` to land a failed round back on the\n * client form (the Server Actions bridge; see the docs' Server Actions\n * guide). */\n errors: FieldErrorEntry[];\n};\n\n/** Append one value under `key` into `fd`. Arrays and FileLists flatten\n * to one entry per item (FormData's multi-entry convention); Files keep\n * their name; Dates become ISO strings; other objects JSON.stringify;\n * booleans/numbers/strings String() as a native form submit would. */\nfunction appendFormDataValue(fd: FormData, key: string, value: any): void {\n if (value == null) return;\n if (Array.isArray(value)) {\n for (const item of value) appendFormDataValue(fd, key, item);\n return;\n }\n if (typeof FileList !== 'undefined' && value instanceof FileList) {\n for (let i = 0; i < value.length; i++) fd.append(key, value.item(i)!);\n return;\n }\n if (typeof File !== 'undefined' && value instanceof File) {\n fd.append(key, value, value.name);\n return;\n }\n if (typeof Blob !== 'undefined' && value instanceof Blob) {\n fd.append(key, value);\n return;\n }\n if (value instanceof Date) {\n fd.append(key, value.toISOString());\n return;\n }\n fd.append(\n key,\n typeof value === 'object' ? JSON.stringify(value) : String(value)\n );\n}\n\n/**\n * Convert a values object into FormData — the transport shape React 19\n * Server Actions and multipart handlers expect. Built to pair with the\n * `<Form action>` prop and `validateValues`: the validated (schema-coerced)\n * values tree lands in the server action as FormData, files included.\n *\n * Array values become multiple entries under the same key (FormData's\n * native multi-value convention); File values keep their name; Dates\n * become ISO strings; plain objects JSON.stringify; null/undefined are\n * skipped.\n */\nexport function formDataFromValues(values: Record<string, any>): FormData {\n const fd = new FormData();\n for (const key of Object.keys(values)) {\n appendFormDataValue(fd, key, values[key]);\n }\n return fd;\n}\n\n/**\n * Validate a payload of values on the server — no form instance, no\n * React.\n *\n * Spins up a throwaway form from `options` (with `initialValues` forced\n * to `values`), runs one whole-form `trigger`, and reads the outcome\n * back. `trigger` never rejects and waits out async validators and any\n * `validateDebounce` window, so a single `await` drains the whole round —\n * the returned `valid`/`errors` are final, not a snapshot mid-flight.\n *\n * Validation comes from `options.validate` — the form-level validator.\n * Field validators register through mounted fields (`useField`), and\n * nothing is mounted on the server, so they cannot participate by\n * construction; pass a schema-backed form validator\n * (`standardSchemaFormValidator(schema)` from\n * `react-f0rm/resolvers/standard-schema`) or a hand-written `validate`\n * instead. `mode`/`reValidateMode` are equally inert here — there are no\n * field events to gate — and may be omitted.\n *\n * When the validator returns a branded {@link ValidationOutcome} whose\n * `values` carry the schema's output, those parsed values become the\n * form's parsedValues baseline, so `result.values` flows coercion and\n * transforms forward (`z.coerce.number()` turning `'42'` into `42`, and\n * friends). Persist that tree; on the client the same schema round runs\n * again on submit, keeping one validation source across the boundary.\n *\n * Safe to call from Node, Server Actions and RSC — the module graph is\n * this file plus the pure core, zero React.\n *\n * @param values the payload to validate; becomes the form's initialValues\n * @param options form options; `validate` is where the rules come from\n * @return the settled round: `valid`, the (possibly parsed) values, and\n * the flat error entries\n */\nexport async function validateValues<T extends Record<string, any> = any>(\n values: T,\n options?: Options<T>\n): Promise<ValidateValuesResult<T>> {\n const form = createForm({...options, initialValues: values});\n const valid = await trigger(form);\n return {valid, values: getValues(form), errors: getErrors(form)};\n}\n","import {\n create as createEmitter,\n emit,\n setMaxListeners\n} from '@for-fun/event-emitter';\nimport type {EventEmitter} from '@for-fun/event-emitter';\nimport createPath from './path';\nimport type {Name, Path} from './path';\nimport type {FieldPath} from './types';\nimport {isPromise} from './util';\nimport {setInitialValues} from './core/values';\nimport type {SetFocusOptions} from './core/focus';\nimport type {VALIDATION_OUTCOME} from './core/errors';\n\n// The implementation is split by concern under ./core (values, errors,\n// touched, dirty, validate, change, submit, focus; module-private shared\n// state lives in ./core/internals, which is deliberately not re-exported).\n// This file keeps the public types and the create factory, and re-exports\n// every public function — the single import surface the rest of the\n// package (hooks, components, server, persist, resolvers) consumes.\nexport type {Name};\nexport type {FieldPath, PathValue} from './types';\n\n/** Dev-only flag, replaced at build time (rollup.config.js `replace`);\n * defined for the test environment in vitest.config.ts. */\ndeclare const __DEV__: boolean;\n\n/** A field error: `type` identifies the error kind ('custom' for plain\n * string errors), `message` is the display text. */\nexport type FieldError = {type: string; message: string};\n\n/** A flattened entry from {@link getErrors}. */\nexport type FieldErrorEntry = {path: string; type: string; message: string};\n\n/** When a field is validated:\n * - `'onSubmit'` (default): only on submit\n * - `'onBlur'`: when the field loses focus\n * - `'onChange'`: on every change\n * - `'onTouched'`: on first blur, then on every change\n * - `'all'`: on both change and blur\n */\nexport type ValidationMode =\n 'onSubmit' | 'onBlur' | 'onChange' | 'onTouched' | 'all';\n\n/** When a field is re-validated after it already has an error:\n * - `'onChange'` (default): on every change\n * - `'onBlur'`: when the field loses focus\n * - `'onSubmit'`: only on submit (no live re-validation)\n */\nexport type ReValidateMode = 'onChange' | 'onBlur' | 'onSubmit';\n\n/** Structured form-level validate result: `errors` uses the same nested\n * shape a plain error record uses, `values` is the schema's parsed output\n * (coerce/transform results included). Either side may be omitted.\n *\n * The brand constant itself lives in the errors module (the leaf module of\n * the core dependency graph — every consumer imports it from there) and is\n * re-exported below with `export *`. */\nexport type ValidationOutcome<T> = {\n [VALIDATION_OUTCOME]: true;\n errors?: Record<string, any>;\n values?: T;\n};\n\n/** What a form-level validate function may return: a plain nested error\n * record (flattened into field errors — the long-standing shape), or a\n * branded {@link ValidationOutcome} whose `values` become the form's\n * parsedValues baseline. */\nexport type ValidateResult<T> =\n | Record<string, any>\n | ValidationOutcome<T>\n | Promise<Record<string, any> | ValidationOutcome<T>>;\n\n/** Context passed to a form-level `validate` function's second argument.\n * `signal` aborts as soon as the round is superseded — a newer round\n * started (which only happens under a positive `validateDebounce`, where\n * kicks merge into windows) — so async validators can cancel their\n * underlying work instead of racing a stale result home. Stale results\n * are dropped independently by the round gate, so validators that ignore\n * the signal stay correct too; the same contract field-level validators\n * get through their own `meta`. */\nexport type FormValidateMeta<T extends Record<string, any> = any> = {\n form: Form<T>;\n signal: AbortSignal;\n};\n\n/** Form-level validator: receives all values (plus {@link\n * FormValidateMeta} as an optional second argument) and returns a\n * {@link ValidateResult} — sync or async — or `undefined`/nothing when\n * valid (the runtime skips falsy results, so implicit-return callbacks\n * type-check). */\nexport type FormValidateFn<T extends Record<string, any> = any> = (\n values: T,\n meta: FormValidateMeta<T>\n) => ValidateResult<T> | undefined;\n\n/**\n * The emitter event table for {@link Form.emitter}: each event's payload\n * tuple. Path-carrying events declare an optional single `Path` payload —\n * emit sites send it for single-field mutations and omit it for bulk\n * payload-less broadcasts (reset, setInitialValues, clear-all), both of\n * which subscribers handle. `focusError` carries the target's path key\n * plus optional {@link SetFocusOptions}.\n */\nexport type FormEvents =\n | ['change', [path?: Path]]\n | ['errors', [path?: Path]]\n | ['touched', [path?: Path]]\n | ['validating', [path?: Path]]\n | ['submitting', []]\n | ['submitCount', []]\n | ['submitSuccessful', []]\n | ['reset', []]\n | ['disabled', []]\n | ['loading', []]\n | ['focusError', [key: string, options?: SetFocusOptions]];\n\nexport type Form<T extends Record<string, any> = any> = {\n emitter: EventEmitter<FormEvents>;\n mode: ValidationMode;\n reValidateMode: ReValidateMode;\n initialValues: T;\n values: Map<string, any>;\n /** Tombstones of unregistered field paths (JSON path keys): reading or\n * merging values must not fall back to initialValues for these paths. */\n deleted: Set<string>;\n /** Every error registered for a field, as a non-empty array (the\n * write-side {@link setErrorByPath} normalizes to this invariant, so\n * readers never need to guard against an empty list). Readers wanting\n * the display error take the first entry ({@link getError}); readers\n * wanting all of them use {@link getFieldErrors}. */\n errors: Map<string, FieldError[]>;\n touched: Set<string>;\n /** Per-field validation kicks, registered by {@link\n * registerValidatorByPath} (`useValidate` is the React-side\n * registration): each is the field's debounce/lock-aware kick —\n * invoking it validates the field's current value. `trigger` /\n * `ensureValidate` run every entry; the user-change gate ({@link\n * userChangeByPath}) runs the entry at the changed path. */\n validators: Map<string, () => void>;\n validating: Set<string>;\n /** Parsed values from the last successful schema validation: the\n * schema's complete output tree (coerced/transformed values included).\n * Sits between initialValues and the values Map in {@link getValues}\n * until `reset`/`setInitialValues` clears it. Never affects dirty\n * state — that compares live edits against initialValues only. */\n parsedValues: T | undefined;\n /** Form-level validator, seeded from {@link Options.validate}. May\n * receive a second {@link FormValidateMeta} argument. */\n validate?: FormValidateFn<T>;\n /** Delay in milliseconds before the form-level `validate` runs; seeded\n * from {@link Options.validateDebounce} and fixed at create time. */\n validateDebounce?: number;\n /** Path keys (JSON-stringified segments) of the fields whose user\n * changes re-run the form-level `validate`; normalized from {@link\n * Options.validateDeps} at create time and fixed thereafter. */\n validateDeps?: ReadonlySet<string>;\n isSubmitting: boolean;\n submitCount: number;\n isSubmitSuccessful: boolean | undefined;\n /** True while an async {@link Options.initialValues} source (a Promise,\n * or a thunk returning one) is still pending — the form starts empty\n * and the resolved values become the baseline via setInitialValues when\n * it lands. Flips through the payload-less 'loading' event\n * (`useIsLoading` / `useFormState().isLoading`). */\n isLoading: boolean;\n /** Form-level default for a bound field's unmount behavior, seeded from\n * {@link Options.shouldUnregister}: `true` (the default) tombstones an\n * unmounted field, `false` keeps its value (react-hook-form's\n * `shouldUnregister` semantics). A field's own `shouldUnregister` option\n * overrides this. */\n shouldUnregister?: boolean;\n /** Form-level disabled flag, OR-ed into every bound field's `disabled`\n * (form flag || the field's own option). Seeded from\n * {@link Options}.disabled at create time and toggled at runtime with\n * {@link setDisabled}, which emits a payload-less 'disabled' event so\n * subscribed fields re-render. */\n disabled: boolean;\n};\n\nexport type Options<T extends Record<string, any> = any> = {\n /**\n * The values baseline. Sync objects seed immediately (SSR renders\n * them). Async sources — a Promise, or a thunk returning a value or\n * Promise (react-hook-form's async `defaultValues` shape) — start the\n * form empty with `isLoading: true` and land the resolved values as\n * the baseline via setInitialValues once they resolve: value\n * subscribers re-sync, dirty/touched state starts clean, and a later\n * `reset()` returns to the resolved baseline. A rejected source flips\n * isLoading back to false, keeps the form empty, and logs the error in\n * DEV — attach a `.catch` on the source itself to handle it. The thunk\n * runs at create time: keep its identity stable (module scope or\n * useMemo) when passing it inline, and note StrictMode double-invokes\n * it in development, like every render-phase call.\n */\n initialValues?: T | Promise<T> | (() => T | Promise<T>);\n /** When fields are validated. Defaults to `'onSubmit'`. See\n * {@link ValidationMode}. */\n mode?: ValidationMode;\n /** When a field is re-validated after it already has an error — it only\n * takes effect once the field has an error. Defaults to `'onChange'`. See\n * {@link ReValidateMode}. */\n reValidateMode?: ReValidateMode;\n /**\n * Form-level validator. Returns a record of errors keyed by field path;\n * nested objects are flattened ('a.b' style) and array values contribute\n * every non-empty string they hold as separate errors (zod `flatten()`\n * formErrors style). Schema adapters instead return a branded\n * {@link ValidationOutcome}: `errors` flattens the same way, `values`\n * (the schema's parsed output) becomes the form's parsedValues baseline\n * that {@link getValues} layers over initialValues.\n */\n validate?: FormValidateFn<T>;\n /**\n * Milliseconds to debounce the form-level `validate`: kicks from\n * `trigger`/`ensureValidate`/submit inside the window merge into one\n * run, and while the timer is pending the form counts as validating,\n * so `trigger` and submit wait the window out — the same contract the\n * per-field `validateDebounce` gives field validators. The merged run\n * reads the values current when its timer fires. Defaults to `0`\n * (validate runs immediately, exactly as before this option existed).\n */\n validateDebounce?: number;\n /** Fields whose user changes re-run the form-level `validate` — the\n * cross-field dependency list (password-confirm mismatch and friends).\n * Each entry is a field path ('password', 'user.email', 'items.0.qty');\n * a user change to a listed field re-runs the form-level `validate`\n * under the same mode/`reValidateMode` gating the field's own\n * validator gets. Omit it and the form-level `validate` only runs on\n * `trigger`/submit, exactly as before this option existed.\n *\n * Opting in also changes what a re-run may clear: each round first\n * drops the errors the previous round wrote (paths it flattened onto),\n * so a dep change that fixes the cross-field error makes it disappear.\n * Errors the round never wrote — field validators', `setServerErrors`,\n * manual `setError` — are never touched. TanStack Form's counterpart is\n * `onChangeListenTo` (v1) / validator `triggers` (v2 alpha). */\n validateDeps?: FieldPath<T>[];\n /**\n * Form-level default for a bound field's unmount behavior. `true` (the\n * default) tombstones an unmounted field — it drops out of\n * `getValues()` instead of reviving its initial value (this library's\n * historical default); `false` keeps the value, matching\n * react-hook-form's `shouldUnregister`. A field's own\n * `useField({shouldUnregister})` option overrides the form-level flag\n * in either direction.\n */\n shouldUnregister?: boolean;\n /** Start the form with every bound field disabled — the flag bound\n * fields OR with their own `disabled` option (a field cannot opt out\n * of a disabled form). Toggle later with {@link setDisabled}.\n * Defaults to `false`. */\n disabled?: boolean;\n};\n\n/**\n * Create form instance\n * @param options\n * @return form instance\n */\nexport default function create<T extends Record<string, any> = any>(\n options?: Options<T>\n): Form<T> {\n const emitter = createEmitter<FormEvents>();\n // A form legitimately accumulates one listener per mounted field per\n // event (useField subscribes change/errors/disabled/focusError…), so\n // the emitter's default max-listener warning would fire in DEV for any\n // form over ~10 fields. Field subscriptions are removed on unmount —\n // there is nothing to leak — so the warning would only be noise: raise\n // the cap to unlimited for form emitters.\n setMaxListeners(emitter, 0);\n // Async initialValues: a thunk is invoked here (create-time, like every\n // other option resolution); a promise-typed result starts the loading\n // cycle below instead of seeding.\n let source: any = options?.initialValues ?? {};\n if (typeof source === 'function') source = (source as () => unknown)();\n const form: Form<T> = {\n emitter,\n ...options,\n mode: options?.mode ?? 'onSubmit',\n reValidateMode: options?.reValidateMode ?? 'onChange',\n disabled: options?.disabled ?? false,\n validateDeps: options?.validateDeps\n ? new Set(options.validateDeps.map(dep => createPath(dep).key))\n : undefined,\n initialValues: {} as T,\n values: new Map(),\n deleted: new Set(),\n errors: new Map(),\n touched: new Set(),\n validators: new Map(),\n validating: new Set(),\n parsedValues: undefined,\n isSubmitting: false,\n submitCount: 0,\n isSubmitSuccessful: undefined,\n isLoading: false\n };\n if (isPromise(source)) {\n // The form starts empty; when the source resolves, its values become\n // the baseline through setInitialValues (payload-less 'change', so\n // every value subscriber re-syncs). The loading flag flips through\n // the 'loading' event before and after — no subscriber exists during\n // the first render, so the synchronous first emit is a safe no-op.\n form.isLoading = true;\n emit(emitter, 'loading');\n Promise.resolve(source).then(\n resolved => {\n form.isLoading = false;\n emit(emitter, 'loading');\n setInitialValues(form, resolved ?? {});\n },\n error => {\n form.isLoading = false;\n emit(emitter, 'loading');\n // The caller's own catch on the source sees the rejection;\n // rethrowing here would only duplicate it as an unhandled\n // promise rejection. Surface it in DEV instead.\n if (__DEV__) {\n // eslint-disable-next-line no-console -- dev-only diagnostics\n console.error('react-f0rm: async initialValues rejected', error);\n }\n }\n );\n } else {\n form.initialValues = source as T;\n }\n return form;\n}\n\nexport * from './core/values';\nexport * from './core/errors';\nexport * from './core/touched';\nexport * from './core/dirty';\nexport * from './core/validate';\nexport * from './core/change';\nexport * from './core/submit';\nexport * from './core/focus';\n"],"names":["appendFormDataValue","fd","key","value","Array","isArray","item","FileList","i","length","append","File","name","Blob","Date","toISOString","JSON","stringify","String","formDataFromValues","values","FormData","Object","keys","async","validateValues","options","form","emitter","createEmitter","setMaxListeners","source","initialValues","mode","reValidateMode","disabled","validateDeps","Set","map","dep","createPath","Map","deleted","errors","touched","validators","validating","parsedValues","isSubmitting","submitCount","isSubmitSuccessful","isLoading","isPromise","emit","Promise","resolve","then","resolved","setInitialValues","error","createForm","valid","trigger","getValues","getErrors"],"mappings":"8RA6DA,SAASA,EAAoBC,EAAcC,EAAaC,GACtD,GAAa,MAATA,EACJ,GAAIC,MAAMC,QAAQF,GAChB,IAAA,MAAWG,KAAQH,EAAOH,EAAoBC,EAAIC,EAAKI,QAGzD,GAAwB,oBAAbC,UAA4BJ,aAAiBI,SACtD,IAAA,IAASC,EAAI,EAAGA,EAAIL,EAAMM,OAAQD,IAAKP,EAAGS,OAAOR,EAAKC,EAAMG,KAAKE,QAG/C,oBAATG,MAAwBR,aAAiBQ,KAClDV,EAAGS,OAAOR,EAAKC,EAAOA,EAAMS,MAGV,oBAATC,MAAwBV,aAAiBU,KAClDZ,EAAGS,OAAOR,EAAKC,GAGbA,aAAiBW,KACnBb,EAAGS,OAAOR,EAAKC,EAAMY,eAGvBd,EAAGS,OACDR,EACiB,iBAAVC,EAAqBa,KAAKC,UAAUd,GAASe,OAAOf,GAE/D,CAaO,SAASgB,EAAmBC,GACjC,MAAMnB,EAAK,IAAIoB,SACf,IAAA,MAAWnB,KAAOoB,OAAOC,KAAKH,GAC5BpB,EAAoBC,EAAIC,EAAKkB,EAAOlB,IAEtC,OAAOD,CACT,CAoCAuB,eAAsBC,EACpBL,EACAM,GAEA,MAAMC,ECkHR,SACED,GAEA,MAAME,EAAUC,IAOhBC,EAAgBF,EAAS,GAIzB,IAAIG,EAAcL,GAASM,eAAiB,CAAA,EACtB,mBAAXD,IAAuBA,EAAUA,KAC5C,MAAMJ,EAAgB,CACpBC,aACGF,EACHO,KAAMP,GAASO,MAAQ,WACvBC,eAAgBR,GAASQ,gBAAkB,WAC3CC,SAAUT,GAASS,WAAY,EAC/BC,aAAcV,GAASU,aACnB,IAAIC,IAAIX,EAAQU,aAAaE,IAAIC,GAAOC,EAAWD,GAAKrC,WACxD,EACJ8B,cAAe,CAAA,EACfZ,WAAYqB,IACZC,YAAaL,IACbM,WAAYF,IACZG,YAAaP,IACbQ,eAAgBJ,IAChBK,eAAgBT,IAChBU,kBAAc,EACdC,cAAc,EACdC,YAAa,EACbC,wBAAoB,EACpBC,WAAW,GA+Bb,OA7BIC,EAAUrB,IAMZJ,EAAKwB,WAAY,EACjBE,EAAKzB,EAAS,WACd0B,QAAQC,QAAQxB,GAAQyB,KACtBC,IACE9B,EAAKwB,WAAY,EACjBE,EAAKzB,EAAS,WACd8B,EAAiB/B,EAAM8B,GAAY,KAErCE,IACEhC,EAAKwB,WAAY,EACjBE,EAAKzB,EAAS,cAWlBD,EAAKK,cAAgBD,EAEhBJ,CACT,CDtLeiC,CAAW,IAAIlC,EAASM,cAAeZ,IAEpD,MAAO,CAACyC,YADYC,EAAQnC,GACbP,OAAQ2C,EAAUpC,GAAOgB,OAAQqB,EAAUrC,GAC5D"}
@@ -0,0 +1,2 @@
1
+ import{emit as e}from"@for-fun/event-emitter";import{h as r,w as t,V as o,s as n,c as i,b as s,n as a,e as l,f as c}from"./errors-CzWtwjO0.mjs";import{g as u}from"./values-B1IV-6V4.mjs";async function f(o,n,i){const s=e=>t(o.emitter,"validating",()=>function(e){for(const r of e.validating)if(r!==w)return!1;return!0}(o),()=>!1);return o.validators.forEach(e=>e()),await s(),o.validate&&await function(r){const t=r.validate;if(!t)return Promise.resolve();const o=r.validateDebounce??0;if(o<=0){const e=new AbortController;return Promise.resolve(t(u(r),{form:r,signal:e.signal})).then(e=>{v(r,e)})}const n=function(e){let r=y.get(e);r||(r={timer:null,controller:null,round:null,marked:!1,waiters:[]},y.set(e,r));return r}(r);null!==n.timer?clearTimeout(n.timer):(n.marked=!0,r.validating.add(w),e(r.emitter,"validating"));return n.timer=setTimeout(()=>{n.timer=null;const e=n.round={};(function(e,r,t){const o=e.validate;if(!o)return Promise.resolve();r.controller?.abort();const n=r.controller=new AbortController;let i;try{i=Promise.resolve(o(u(e),{form:e,signal:n.signal}))}catch(e){i=Promise.reject(e)}return i.then(o=>{r.round===t&&v(e,o)},e=>{if(r.round===t)throw e})})(r,n,e).then(()=>b(r,n,e,p),t=>b(r,n,e,t))},o),new Promise((e,r)=>{n.waiters.push({resolve:e,reject:r})})}(o),!r(o)}function m(e,r,t=[],o){Object.entries(r).forEach(([r,n])=>{const i=[...t,...s(r)?[r]:a(r)];"string"==typeof n?n&&(l(e,i,n),d(e,i,o)):Array.isArray(n)||c(n)?(l(e,i,n),d(e,i,o)):n&&"object"==typeof n&&m(e,n,i,o)})}function d(e,r,t){if(!t)return;const o=i(r),n=e.errors.get(o.key);n&&t.set(o.key,n)}function v(r,t){const s=r.validateDeps?function(e){let r=g.get(e);r||(r=new Map,g.set(e,r));return r}(r):void 0;if(s&&(!function(r,t){for(const[o,n]of t){r.errors.get(o)===n&&(r.errors.delete(o),e(r.emitter,"errors",i(JSON.parse(o))))}}(r,s),s.clear()),t){if("object"==typeof t&&o in t){const e=t;return e.errors&&m(r,e.errors,[],s),void n(r,e.values)}m(r,t,[],s)}}const g=new WeakMap;const w="__form_validate__";const p=Symbol("form-validate-settled"),y=new WeakMap;function b(r,t,o,n){if(t.round!==o)return;if(t.round=null,null!==t.timer)return;t.marked&&(t.marked=!1,r.validating.delete(w),e(r.emitter,"validating"));const i=t.waiters;t.waiters=[];for(const e of i)n===p?e.resolve():e.reject(n)}export{f as t};
2
+ //# sourceMappingURL=validate-B1Gdjeaq.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validate-B1Gdjeaq.mjs","sources":["../src/core/validate.ts"],"sourcesContent":["import {emit} from '@for-fun/event-emitter';\nimport createPath from '../path';\nimport type {Name, Path, PathSegments} from '../path';\nimport {isIndex, isPromise, normalizePath, waitUntil} from '../util';\nimport type {\n FieldError,\n FieldErrorEntry,\n Form,\n ValidationMode,\n ValidateResult,\n ValidationOutcome\n} from '../form';\nimport {\n VALIDATION_OUTCOME,\n getErrors,\n getFirstError,\n hasErrors,\n setError,\n setErrorByPath\n} from './errors';\nimport {hasTouchedByPath, setTouchedByPath} from './touched';\nimport {getValueByPath, getValues} from './values';\nimport {isFieldError, isSegmentsPath, setParsedValues} from './internals';\n\nexport function unsetValidatingByPath(\n {emitter, validating}: Form,\n path: Path\n): void {\n validating.delete(path.key);\n // Path payload lets key-scoped subscribers (onKeyEvent) skip unrelated\n // fields; payload-less listeners ignore it.\n emit(emitter, 'validating', path);\n}\n\nexport function setValidatingByPath(\n {emitter, validating}: Form,\n path: Path\n): void {\n validating.add(path.key);\n emit(emitter, 'validating', path);\n}\n\n/**\n * Field validator. Returns an error (a string, a FieldError, or an array\n * mixing both) or undefined when valid; may return a Promise for async\n * validation.\n *\n * The second argument carries the validation context. `meta.signal` is\n * aborted as soon as the round is superseded — a newer round started, or\n * the field unregistered — so async validators can cancel their underlying\n * work (fetch, timers) instead of racing a stale result home. Stale\n * results are dropped independently by the registration's lock\n * ({@link registerValidatorByPath}), so validators that ignore the signal\n * stay correct too. Validators written against the older two-argument\n * signature keep working.\n */\nexport type Validator = (\n value: any,\n meta: {form: Form; path: Path; signal: AbortSignal}\n) =>\n | string\n | FieldError\n | (string | FieldError)[]\n | undefined\n | Promise<string | FieldError | (string | FieldError)[] | undefined>;\n\n/**\n * Synchronous pre-validator for {@link registerValidatorByPath}'s `sync`\n * accessor — declarative `required` rules compiled by `rulesToValidator`\n * in practice, but any sync-only check works. Runs on every kick, never\n * debounced: its errors land immediately and, while present,\n * short-circuit the debounced validator for that kick (the expensive\n * check never sees a value the gate already rejects). Must be synchronous\n * — unlike a {@link Validator} it may not return a Promise — and its meta\n * carries no `signal`: there is nothing to abort in a synchronous check.\n */\nexport type SyncValidator = (\n value: any,\n meta: {form: Form; path: Path}\n) => string | FieldError | (string | FieldError)[] | undefined;\n\n/** Live options for {@link registerValidatorByPath}: read at every kick\n * through accessors, so callers (React's `useValidate`) can swap the\n * validator/debounce/sync-gate per render without re-subscribing the\n * registration mid-flight. */\nexport type ValidatorRegistration = {\n /** Current debounced validator (or undefined — a sync-only\n * registration). */\n validate: () => Validator | undefined;\n /** Debounce delay in milliseconds; 0 (default) runs immediately. */\n debounce: () => number;\n /** Synchronous pre-validator, run on every kick (never debounced). */\n sync: () => SyncValidator | undefined;\n};\n\n/**\n * Register a field validator's kick at `path` in {@link Form.validators}\n * — the framework-free machinery behind `useValidate`. Returns a\n * disposer that drops the registration and cancels any pending debounce\n * window or in-flight round (its signal aborts and its validating mark\n * is released).\n *\n * Contract of the registered kick (the same contract `trigger` /\n * `ensureValidate` rely on when they run every entry, and the\n * user-change gate relies on when it runs the changed path's entry):\n * - the `sync` gate runs immediately on every kick — never debounced —\n * and while it returns errors, the debounced validator is skipped for\n * that kick and any pending window or in-flight round is superseded;\n * - a positive `debounce` merges kicks inside the window: only the last\n * one runs the validator, and while the timer is pending the field\n * counts as validating so `trigger`/`ensureValidate` wait it out;\n * - async results land under a lock: a superseded round's outcome —\n * rejection included — is dropped, and only the owning round releases\n * the validating mark;\n * - a synchronous throw inside the validator propagates to the caller\n * (the validating mark is not left stuck behind it).\n *\n * Registering at a path already registered by another mount replaces it\n * (last-wins, the historical `useValidate` behavior); the disposer drops\n * its own registration unconditionally.\n *\n * @param form\n * @param path\n * @param registration live validator/debounce/sync accessors\n * @return disposer: unregister and cancel pending work\n */\nexport function registerValidatorByPath(\n form: Form,\n path: Path,\n registration: ValidatorRegistration\n): () => void {\n // The pending debounce timer and the current round's controller live in\n // this closure so the disposer below can cancel them.\n let timer: ReturnType<typeof setTimeout> | null = null;\n let controller: AbortController | null = null;\n // Whether this registration currently holds the path's slot in\n // form.validating. The mark is taken when a debounce window opens or an\n // async round starts, and released by whichever round settles last —\n // including a later sync round that supersedes an in-flight async one\n // (its own .finally is lock-gated out by then).\n let marked = false;\n // The async-round lock: only the latest round may land its result or\n // release the mark; a superseded round's outcome is dropped wholesale.\n let lock: object | null = null;\n // Which source wrote the error currently on display — the sync gate or\n // the debounced validator. Tracked so a passing sync check can clear\n // its own stale error immediately instead of leaving it on screen until\n // the debounced round lands. External writers (setError, form-level\n // validate) are invisible here; a passing round clearing them matches\n // the long-standing \"a field validator owns its whole key\" contract.\n let errorSource: 'sync' | 'validator' | null = null;\n /** Does a validator result land errors? `[]` normalizes away exactly\n * like undefined in setErrorByPath. */\n const hasErrors = (errors: any): boolean =>\n errors !== undefined && !(Array.isArray(errors) && errors.length === 0);\n const mark = () => {\n if (marked) return;\n marked = true;\n setValidatingByPath(form, path);\n };\n const unmark = () => {\n if (!marked) return;\n marked = false;\n unsetValidatingByPath(form, path);\n };\n\n /** Run the synchronous gate on the field's current value. Its errors\n * land immediately — the gate is never debounced. Returns true when\n * errors landed (the kick's whole outcome for the debounced validator).\n * A passing gate clears the field's errors when they were its own from\n * an earlier kick, or when no debounced validator exists to own the\n * round. */\n const runSync = (): boolean => {\n const sync = registration.sync();\n if (!sync) return false;\n const errors = sync(getValueByPath(form, path), {form, path});\n if (!hasErrors(errors)) {\n // A stale error the gate itself wrote is answered by the gate\n // alone; a rules-only registration's passing check is the whole\n // round. With a debounced validator registered, its upcoming round\n // owns the outcome and lands it later.\n if (!registration.validate() || errorSource === 'sync') {\n setErrorByPath(form, path, undefined);\n errorSource = null;\n }\n return false;\n }\n setErrorByPath(form, path, errors);\n errorSource = 'sync';\n return true;\n };\n\n /** Drop any pending window or in-flight round without landing it: the\n * sync gate now owns the outcome, so the debounced validator must not\n * run for this value. */\n const supersede = () => {\n if (timer !== null) {\n clearTimeout(timer);\n timer = null;\n }\n controller?.abort();\n lock = {};\n };\n\n /** Run the debounced validator on the field's current value and land\n * its result — the sync gate has already passed. */\n const runValidator = () => {\n const fn = registration.validate();\n if (!fn) {\n unmark();\n return;\n }\n // Abort the superseded round's signal: a listening validator should\n // stop its underlying work. The lock refresh below independently\n // drops any result that still arrives, signal or not.\n controller?.abort();\n controller = new AbortController();\n const round = (lock = {});\n let result;\n try {\n result = fn(getValueByPath(form, path), {\n form,\n path,\n signal: controller.signal\n });\n } catch (e) {\n // A throwing sync validator propagates to the caller as it always\n // has; just don't leave the validating mark stuck behind it.\n unmark();\n throw e;\n }\n if (!isPromise(result)) {\n setErrorByPath(form, path, result);\n errorSource = hasErrors(result) ? 'validator' : null;\n // Error first, then release the mark: 'validating' subscribers\n // (trigger) re-read state on wake and must see the landed error.\n unmark();\n return;\n }\n mark();\n result\n .then(\n (error: string | FieldError | (string | FieldError)[] | undefined) => {\n if (lock === round) {\n setErrorByPath(form, path, error);\n errorSource = hasErrors(error) ? 'validator' : null;\n }\n }\n )\n // A rejected round is the normal way a signal-listening validator\n // gives up (fetch throws AbortError once aborted); swallow it and\n // let the owning round write the outcome.\n .catch(() => {})\n .finally(() => {\n if (lock === round) {\n unmark();\n lock = null;\n }\n });\n };\n\n /** A debounce window fired: the value may have drifted since the last\n * kick (programmatic writes do not kick validators), so re-run the\n * sync gate before spending the debounced validator on a value the\n * gate already rejects. */\n const run = () => {\n timer = null;\n if (runSync()) {\n supersede();\n unmark();\n return;\n }\n runValidator();\n };\n\n const kick = () => {\n if (runSync()) {\n supersede();\n unmark();\n return;\n }\n if (!registration.validate()) return;\n const debounce = registration.debounce();\n if (debounce > 0) {\n // Only the last kick inside the window runs: restart the timer on\n // every kick. The mark keeps trigger/ensureValidate's\n // validating-set wait covering the pending timer, not just\n // in-flight promises.\n if (timer !== null) clearTimeout(timer);\n else mark();\n timer = setTimeout(run, debounce);\n return;\n }\n runValidator();\n };\n\n form.validators.set(path.key, kick);\n return () => {\n form.validators.delete(path.key);\n if (timer !== null) {\n clearTimeout(timer);\n timer = null;\n }\n unmark();\n controller?.abort();\n };\n}\n\n/**\n * Set field error\n * @param form\n * @param name\n * @param error string is normalized to {type: 'custom', message}; a\n * FieldError object is stored as-is; an array holds several errors\n * (falsy items dropped, strings normalized); undefined clears\n */\n/** Options accepted by {@link trigger}. `shouldTouch` defaults to `false`;\n * omitting the options object entirely keeps the plain validate-only\n * behavior, so the historical two-argument calls are untouched. */\nexport type TriggerOptions = {\n /** Mark every path in the triggered scope as touched — even when\n * validation fails — once the round settles. Mirrors react-hook-form's\n * trigger `shouldTouch`. Defaults to `false`. */\n shouldTouch?: boolean;\n /**\n * Focus the first errored field in the triggered scope once the round\n * settles (and only when the round left errors) — react-hook-form's\n * trigger `shouldFocus` counterpart. Rides the 'focusError' event\n * channel like a failed submit's auto-focus: only mounted bound fields\n * react, unmounted ones are silent no-ops. Without `name` the first key\n * of the errors Map wins (the same rule handleSubmit applies); with\n * `name` the first errored triggered key does. Defaults to `false`.\n */\n shouldFocus?: boolean;\n};\n\n/**\n * Trigger field validation.\n *\n * Without `name` every registered field validator runs. A single `name` —\n * dotted string or segments array — runs only that field's validator, and\n * an array of names runs each one in order. An empty array is a no-op, as\n * is any name with no registered validator. An array argument counts as\n * one segments path only when it mixes in numbers (`['items', 0]`); pure\n * string arrays are name lists, so `['a', 'b']` triggers fields `a` and\n * `b`, not the nested path `a.b`.\n *\n * `options.shouldTouch` marks the triggered scope — the given names, or\n * every registered field when `name` is omitted — as touched after the\n * round settles, whether validation passed or failed. The wait/settle\n * logic is untouched: the marking rides on top of the settled round, so\n * subscribers observe errors and touched together rather than mid-flight.\n *\n * The returned promise waits for the triggered validation to settle —\n * async validators included — so their errors have already landed in\n * `form.errors` when it resolves. It never rejects: landing errors is the\n * expected outcome here, not a failure. Resolves `true` when the triggered\n * scope is error-free, `false` otherwise. Without `name` the scope is all\n * fields plus the form-level `validate` result (which runs after field\n * validators settle, same pipeline as {@link ensureValidate}); with `name`\n * only those fields' own errors count and form-level `validate` is\n * skipped (RHF semantics).\n *\n * Fire-and-forget callers may ignore the promise: the validator kicks\n * still happen synchronously, matching the pre-promise behavior.\n *\n * @param form\n * @param name field name(s) to trigger, or all fields when omitted\n * @param options extra behavior toggles ({@link TriggerOptions}); omitted,\n * validation alone runs — no touched marking\n * @return whether the triggered scope is error-free once validation settles\n */\nexport async function trigger(\n form: Form,\n name?: Name | Name[],\n options?: TriggerOptions\n): Promise<boolean> {\n // Never reject (an error landing is a normal outcome, not a failure), so\n // waitUntil's isReject is permanently false. Without a name the wait is\n // deliberately conservative — every FIELD validator, unrelated in-flight\n // ones included, because the round covers the whole form (and the\n // form-level validate's own window is excluded via fieldsSettled —\n // callers wait that out through the kick's promise instead, so a pending\n // window never gates the next kick). With a name the wait narrows to the\n // triggered keys only: a slow async validator on field B must not hold\n // trigger('a') hostage when the round never reads B.\n const settle = (keys?: string[]) =>\n waitUntil(\n form.emitter,\n 'validating',\n () =>\n keys === undefined\n ? fieldsSettled(form)\n : keys.every(key => !form.validating.has(key)),\n () => false\n );\n\n if (name === undefined) {\n form.validators.forEach(validator => validator());\n await settle();\n if (form.validate) await runFormValidate(form);\n // shouldTouch marks the whole registered scope — every key the round\n // could have validated — pass or fail alike.\n if (options?.shouldTouch) touchKeys(form, [...form.validators.keys()]);\n // First error across the errors Map — the same rule a failed submit's\n // auto-focus applies (a form-level error may land first; it has no\n // element, so it is a silent no-op like every unbound path).\n if (options?.shouldFocus) {\n const firstKey = form.errors.keys().next().value;\n if (firstKey !== undefined) emit(form.emitter, 'focusError', firstKey);\n }\n return !hasErrors(form);\n }\n\n const keys: string[] =\n typeof name === 'string' || isSegmentsPath(name)\n ? [createPath(name).key]\n : name.map(one => createPath(one).key);\n keys.forEach(key => form.validators.get(key)?.());\n await settle(keys);\n if (options?.shouldTouch) touchKeys(form, keys);\n // Focus the first errored key among the triggered scope — trigger('a')\n // never focuses B's pre-existing error.\n if (options?.shouldFocus) {\n const firstKey = keys.find(key => form.errors.has(key));\n if (firstKey !== undefined) emit(form.emitter, 'focusError', firstKey);\n }\n return keys.every(key => !form.errors.has(key));\n}\n\n/** trigger's `shouldTouch` marking: touch every key in the triggered scope\n * through {@link setTouchedByPath}, which no-ops on already-touched keys\n * and emits the path-carrying 'touched' event per newly touched one. Keys\n * are the stored JSON-stringified segments shape, so parse them back into\n * Path — normalizePath passes segment arrays through untouched, making the\n * key round-trip exact. */\nfunction touchKeys(form: Form, keys: string[]): void {\n keys.forEach(key => setTouchedByPath(form, createPath(JSON.parse(key))));\n}\n\n/**\n * Flatten a form-level validate result and write each leaf error through\n * setError. Nested objects descend into deeper paths ({a: {b: 'msg'}} sets\n * the 'a.b' error), array values contribute every non-empty string they\n * hold as separate errors (zod flatten() formErrors style), and\n * FieldError-shaped objects are stored as-is. Falsy values are skipped.\n *\n * When `footprint` is passed (validateDeps forms only), every leaf this\n * round actually stored is recorded into it — the exact stored array —\n * so the next round can drop exactly what this one wrote.\n */\nfunction setFormErrors(\n form: Form,\n result: Record<string, any>,\n segments: PathSegments = [],\n footprint?: Map<string, FieldError[]>\n): void {\n Object.entries(result).forEach(([key, value]) => {\n // Error-tree keys are explicit object keys, not path expressions:\n // a numeric key ('0' — Standard Schema issue paths stringify array\n // indices) stays a literal string segment instead of feeding the\n // path parser, whose dotted-numeric rule governs path strings only.\n const path: PathSegments = [\n ...segments,\n ...(isIndex(key) ? [key] : normalizePath(key))\n ];\n if (typeof value === 'string') {\n if (value) {\n setError(form, path, value);\n recordFootprint(form, path, footprint);\n }\n } else if (Array.isArray(value)) {\n setError(form, path, value);\n recordFootprint(form, path, footprint);\n } else if (isFieldError(value)) {\n setError(form, path, value);\n recordFootprint(form, path, footprint);\n } else if (value && typeof value === 'object') {\n setFormErrors(form, value, path, footprint);\n }\n });\n}\n\n/** Record one leaf write of a form-level validate round: the path key and\n * the exact array now stored there. Nothing is recorded when the write\n * normalized away (all-empty arrays) — there is no error to own. The\n * stored array is read back from the errors Map because setErrorByPath\n * owns normalization. */\nfunction recordFootprint(\n form: Form,\n segments: PathSegments,\n footprint: Map<string, FieldError[]> | undefined\n): void {\n if (!footprint) return;\n const path = createPath(segments);\n const stored = form.errors.get(path.key);\n if (stored) footprint.set(path.key, stored);\n}\n\n/**\n * Land a form-level validate result. A plain record keeps the\n * long-standing behavior — flattened into field errors by\n * {@link setFormErrors}. A branded {@link ValidationOutcome} splits\n * instead: `errors` flattens exactly like a plain record, and `values`\n * (the schema's parsed output — coerced/transformed values included)\n * becomes the form's parsedValues baseline. Falsy results are skipped,\n * branded or not.\n *\n * Forms that opted into `validateDeps` additionally get round-scoped\n * error ownership: before the new result lands, the errors the previous\n * round wrote are dropped ({@link clearFormValidateErrors}), so a re-run\n * that passes makes the cross-field error disappear — and the new\n * round's own writes become the tracked footprint. Forms without the\n * option keep the historical write-only behavior untouched.\n */\nfunction applyValidateResult(\n form: Form,\n result: ValidateResult<any> | undefined\n): void {\n const footprint = form.validateDeps ? getFormErrorFootprint(form) : undefined;\n if (footprint) {\n clearFormValidateErrors(form, footprint);\n footprint.clear();\n }\n if (!result) return;\n if (typeof result === 'object' && VALIDATION_OUTCOME in result) {\n const outcome = result as ValidationOutcome<any>;\n if (outcome.errors) setFormErrors(form, outcome.errors, [], footprint);\n setParsedValues(form, outcome.values);\n return;\n }\n setFormErrors(form, result as Record<string, any>, [], footprint);\n}\n\n/** Per-form error footprint of the last form-level validate round: every\n * path key it flattened onto, with the exact array instance it stored.\n * Tracked only for forms that opted into `validateDeps` — held in a\n * WeakMap so the Form shape and the non-opted pipeline stay untouched. */\nconst formErrorFootprints = new WeakMap<Form, Map<string, FieldError[]>>();\n\nfunction getFormErrorFootprint(form: Form): Map<string, FieldError[]> {\n let footprint = formErrorFootprints.get(form);\n if (!footprint) {\n footprint = new Map();\n formErrorFootprints.set(form, footprint);\n }\n return footprint;\n}\n\n/** Does the form still show an error the last form-level round wrote?\n * Compared by identity, not key membership: once a field validator,\n * `setServerErrors`, a manual `setError` or `clearErrors` replaces the\n * stored array, that error is no longer the round's to own — neither the\n * dep-change gate nor the next round's clearing may touch it. */\nfunction hasFormValidateErrors(form: Form): boolean {\n const footprint = formErrorFootprints.get(form);\n if (!footprint) return false;\n for (const [key, written] of footprint) {\n if (form.errors.get(key) === written) return true;\n }\n return false;\n}\n\n/** Drop the last form-level round's errors before the next round lands.\n * Per key the stored array is identity-checked — an error overwritten or\n * cleared by anyone else in between survives — and each drop emits the\n * same path-payload 'errors' event {@link setErrorByPath} would, so\n * subscribed fields re-render exactly like on any error write. */\nfunction clearFormValidateErrors(\n form: Form,\n footprint: Map<string, FieldError[]>\n): void {\n for (const [key, written] of footprint) {\n const stored = form.errors.get(key);\n if (stored !== written) continue;\n form.errors.delete(key);\n emit(form.emitter, 'errors', createPath(JSON.parse(key)));\n }\n}\n\n/** Key the form-level validate round reserves in `form.validating` while\n * its debounce window is pending or its async round is in flight. Real\n * path keys are JSON-stringified segments (always bracketed), so a bare\n * word can never collide. */\nconst FORM_VALIDATING_KEY = '__form_validate__';\n\n/** Are all FIELD validation rounds drained? trigger/ensureValidate wait on\n * this before kicking the form-level validate (its errors gate whether the\n * form-level round may run at all). The form validate's own reserved key\n * is deliberately excluded: its window is waited out through the kick's\n * returned promise instead, so a pending window or in-flight form round\n * never gates the next kick — a kick during an in-flight round opens a\n * new window and the newer round supersedes, mirroring the per-field\n * `validateDebounce` contract. */\nfunction fieldsSettled(form: Form): boolean {\n for (const key of form.validating) {\n if (key !== FORM_VALIDATING_KEY) return false;\n }\n return true;\n}\n\n/** Sentinel telling {@link settleFormValidate} the round landed cleanly —\n * distinct from every rejection payload, including `undefined`. */\nconst SETTLED = Symbol('form-validate-settled');\n\n/** Per-form bookkeeping for the debounced form-level validate: the\n * pending window timer, the in-flight round, and the waiters merged into\n * the current window group. Held in a WeakMap so the Form instance shape\n * is untouched for forms that never set `validateDebounce`. */\ntype FormValidateState = {\n timer: ReturnType<typeof setTimeout> | null;\n controller: AbortController | null;\n /** Identity of the in-flight round; a superseded round's outcome\n * (rejection included) is dropped by comparing against it. */\n round: object | null;\n /** Whether this state currently holds FORM_VALIDATING_KEY in\n * form.validating. */\n marked: boolean;\n waiters: Array<{resolve: () => void; reject: (error: unknown) => void}>;\n};\n\nconst formValidateStates = new WeakMap<Form, FormValidateState>();\n\nfunction getFormValidateState(form: Form): FormValidateState {\n let state = formValidateStates.get(form);\n if (!state) {\n state = {\n timer: null,\n controller: null,\n round: null,\n marked: false,\n waiters: []\n };\n formValidateStates.set(form, state);\n }\n return state;\n}\n\n/**\n * Run the form-level `validate` and land its result, honoring the form's\n * `validateDebounce` option.\n *\n * Undebounced (`0`/undefined) the caller's await *is* the validate call —\n * the long-standing pipeline, unchanged: no validating mark, no round\n * gating, immediate values snapshot, rejection propagating to the caller.\n *\n * Debounced, the kick opens (or restarts — kicks inside the window merge)\n * a window during which the form counts as validating, so `trigger` /\n * `ensureValidate` / submit wait the window out exactly like a field's\n * `validateDebounce` window. When the timer fires, the round reads the\n * then-current values, supersedes (aborts) any in-flight round, and lands\n * its result. The returned promise settles once the window group's final\n * round has landed — rejecting when that round's validate callback threw\n * or its promise rejected, mirroring the undebounced propagation — so\n * merged callers all observe the same outcome.\n *\n * Only called under `if (form.validate)`.\n */\nfunction runFormValidate(form: Form): Promise<void> {\n const validate = form.validate;\n if (!validate) return Promise.resolve();\n const debounce = form.validateDebounce ?? 0;\n if (debounce <= 0) {\n // Standalone controller: nothing supersedes an undebounced call, so\n // its signal never fires — it exists for argument-shape parity with\n // the debounced rounds (and with field-level meta.signal).\n const controller = new AbortController();\n return Promise.resolve(\n validate(getValues(form), {form, signal: controller.signal})\n ).then(result => {\n applyValidateResult(form, result);\n });\n }\n const state = getFormValidateState(form);\n // (Re)open the window: a kick while the timer is pending restarts it\n // (only the last kick's values run); one while a round is in flight\n // keeps the validating mark held and defers to the new window's round.\n if (state.timer !== null) clearTimeout(state.timer);\n else {\n state.marked = true;\n form.validating.add(FORM_VALIDATING_KEY);\n emit(form.emitter, 'validating');\n }\n state.timer = setTimeout(() => {\n state.timer = null;\n const round = (state.round = {});\n runFormValidateRound(form, state, round).then(\n () => settleFormValidate(form, state, round, SETTLED),\n error => settleFormValidate(form, state, round, error)\n );\n }, debounce);\n return new Promise<void>((resolve, reject) => {\n state.waiters.push({resolve, reject});\n });\n}\n\n/** Run one form-level validate round with the form's current values.\n * Aborts the previous in-flight round's signal; a superseded round's\n * outcome — rejection included — is dropped by the round gate, exactly\n * like the field-level lock. */\nfunction runFormValidateRound(\n form: Form,\n state: FormValidateState,\n round: object\n): Promise<void> {\n const validate = form.validate;\n if (!validate) return Promise.resolve();\n state.controller?.abort();\n const controller = (state.controller = new AbortController());\n let outcome: Promise<any>;\n try {\n outcome = Promise.resolve(\n validate(getValues(form), {form, signal: controller.signal})\n );\n } catch (error) {\n outcome = Promise.reject(error);\n }\n return outcome.then(\n result => {\n if (state.round === round) applyValidateResult(form, result);\n },\n error => {\n if (state.round === round) throw error;\n }\n );\n}\n\n/** Land the window group's outcome: release the validating mark — after\n * the round's errors/values have already landed, because 'validating'\n * subscribers (trigger, ensureValidate) re-read state on wake — and\n * settle every merged waiter. A superseded round never lands here (the\n * newer round owns the release), and a window that re-opened while the\n * round was in flight defers: the mark and the waiters carry over to the\n * pending timer's round. */\nfunction settleFormValidate(\n form: Form,\n state: FormValidateState,\n round: object,\n outcome: unknown\n): void {\n if (state.round !== round) return;\n state.round = null;\n if (state.timer !== null) return;\n if (state.marked) {\n state.marked = false;\n form.validating.delete(FORM_VALIDATING_KEY);\n emit(form.emitter, 'validating');\n }\n const waiters = state.waiters;\n state.waiters = [];\n for (const waiter of waiters) {\n if (outcome === SETTLED) waiter.resolve();\n else waiter.reject(outcome);\n }\n}\n\n/**\n * Form-level twin of the gated validator kick in `useField`'s onChange:\n * re-run the form-level `validate` after a user change to a field listed\n * in `validateDeps`. Called from the field's own change pipeline (typing\n * and `changeValue` alike — both route through the mounted field's\n * onChange), so programmatic `setValue` writes do not re-run it, exactly\n * like they do not re-run field validators.\n *\n * The gate mirrors the per-field matrix with the *changed field's*\n * effective `mode` (a per-field override governs when its changes may\n * fire validation) and the form-level `reValidateMode` against the last\n * round's error footprint ({@link hasFormValidateErrors} — field\n * validators' errors never arm this kick):\n * - `mode` `'onChange'`/`'all'` — every dep change re-runs;\n * - `mode` `'onTouched'` — dep changes re-run once the field was touched;\n * - otherwise the re-run waits for `reValidateMode: 'onChange'` (the\n * default) while the last round's error is still live — the\n * submit-then-fix flow: the mismatch lands on submit, editing the\n * dependency re-runs the validate and clears it.\n * `reValidateMode: 'onBlur'`/`'onSubmit'` never re-run on a change (a\n * change is not a blur; submit re-runs are the submit pipeline's job).\n *\n * The kick is fire-and-forget: async round rejections are swallowed\n * (nothing in an event handler can await them), while a synchronous\n * throw inside the validate callback propagates to the caller exactly\n * like a field validator's does.\n *\n * A no-op unless the form set `validateDeps` listing `path` — forms\n * without the option pay one property check here.\n */\nexport function revalidateFormOnChange(\n form: Form,\n path: Path,\n mode: ValidationMode\n): void {\n if (!form.validateDeps?.has(path.key) || !form.validate) return;\n if (\n mode === 'onChange' ||\n mode === 'all' ||\n (mode === 'onTouched' && hasTouchedByPath(form, path)) ||\n (form.reValidateMode === 'onChange' && hasFormValidateErrors(form))\n ) {\n runFormValidate(form).catch(() => {});\n }\n}\n\n/** Per-form registry of field-level `validateDeps` declarations ({@link\n * revalidateDependentsOnChange}): dep path key -> every dependent field key\n * that listed it. Held in a WeakMap so the Form shape is untouched for\n * forms whose fields never declare deps. */\nconst fieldValidateDeps = new WeakMap<Form, Map<string, Set<string>>>();\n\n/** Register one field's validateDeps declaration: `key` re-validates when\n * any path in `depKeys` takes a user change. Idempotent per (key, dep)\n * pair, so StrictMode's double effect is harmless. */\nexport function registerFieldValidateDeps(\n form: Form,\n key: string,\n depKeys: string[]\n): void {\n let deps = fieldValidateDeps.get(form);\n if (!deps) {\n deps = new Map();\n fieldValidateDeps.set(form, deps);\n }\n for (const depKey of depKeys) {\n let dependents = deps.get(depKey);\n if (!dependents) {\n dependents = new Set();\n deps.set(depKey, dependents);\n }\n dependents.add(key);\n }\n}\n\n/** Drop one field's validateDeps registration ({@link\n * registerFieldValidateDeps}). Entries nobody lists anymore are removed so\n * the registry never outlives its fields. */\nexport function unregisterFieldValidateDeps(\n form: Form,\n key: string,\n depKeys: string[]\n): void {\n const deps = fieldValidateDeps.get(form);\n if (!deps) return;\n for (const depKey of depKeys) {\n const dependents = deps.get(depKey);\n if (!dependents?.delete(key)) continue;\n if (!dependents.size) deps.delete(depKey);\n }\n}\n\n/**\n * Field-level twin of {@link revalidateFormOnChange}: after a user change\n * to `path`, re-run every field validator that declared `path` in its\n * `validateDeps` (useField option). Same channel, same gate: the kick\n * rides the changed field's own onChange pipeline (typing and\n * `changeValue` alike), so programmatic `setValue` writes never fire it —\n * exactly like field validators and the form-level `validateDeps`.\n *\n * The gate mirrors the form-level matrix with the *changed field's*\n * effective `mode` and the form-level `reValidateMode` against each\n * dependent's live error:\n * - `mode` `'onChange'`/`'all'` — every dep change re-runs the dependent;\n * - `mode` `'onTouched'` — once the changed field was touched;\n * - otherwise the re-run waits for `reValidateMode: 'onChange'` (the\n * default) while the dependent still shows an error — the\n * submit-then-fix flow: the mismatch lands on submit, editing the\n * dependency re-validates the dependent and a passing round clears it\n * (a field validator owns its whole key, so the re-run's result\n * replaces whatever the previous round wrote — the field-level shape\n * of the form-level footprint reclaim).\n *\n * The kick is an ordinary validator kick: the dependent's own\n * `validateDebounce` window applies, and a synchronous throw inside its\n * validate propagates to the caller like any field validator's would.\n *\n * A no-op unless some field declared `path` as a dep — forms without any\n * field-level `validateDeps` pay one property check here.\n */\nexport function revalidateDependentsOnChange(\n form: Form,\n path: Path,\n mode: ValidationMode\n): void {\n const dependents = fieldValidateDeps.get(form)?.get(path.key);\n if (!dependents?.size) return;\n for (const dependent of dependents) {\n // A self-dep changes nothing: the field's own onChange above already\n // validated it under the same gate.\n if (dependent === path.key) continue;\n if (\n mode === 'onChange' ||\n mode === 'all' ||\n (mode === 'onTouched' && hasTouchedByPath(form, path)) ||\n (form.reValidateMode === 'onChange' && form.errors.has(dependent))\n ) {\n form.validators.get(dependent)?.();\n }\n }\n}\n\n/** The Error {@link ensureValidate} rejects with: `message` is the first\n * error's display text ({@link getFirstError}) — the long-standing shape\n * — and `.errors` carries the complete flattened error list ({@link\n * getErrors}: `{path, type, message}` entries, dotted display paths) so\n * catchers can branch on types and locate fields without re-reading the\n * form. */\nexport type FormValidationError = Error & {errors: FieldErrorEntry[]};\n\n/** Build {@link ensureValidate}'s rejection: first error's message, every\n * error attached. */\nfunction validationError(form: Form): FormValidationError {\n const error = new Error(getFirstError(form)) as FormValidationError;\n error.errors = getErrors(form);\n return error;\n}\n\n/**\n * Validate and throw if any field error.\n * @param form\n * @return resolve if no error; reject and stop validate if has an error\n */\nexport async function ensureValidate(form: Form): Promise<void> {\n form.validators.forEach(validator => validator());\n\n await waitUntil(\n form.emitter,\n 'validating',\n () => fieldsSettled(form),\n () => hasErrors(form)\n ).catch(() => {\n throw validationError(form);\n });\n\n if (form.validate) {\n await runFormValidate(form);\n if (hasErrors(form)) throw validationError(form);\n }\n}\n\n/**\n * Validate and return if any field error.\n * @param form\n * @return error message string or void\n */\nexport async function validate(form: Form): Promise<void | string> {\n return ensureValidate(form).catch(e => e.message);\n}\n"],"names":["async","trigger","form","name","options","settle","keys","waitUntil","emitter","key","validating","FORM_VALIDATING_KEY","fieldsSettled","validators","forEach","validator","validate","Promise","resolve","debounce","validateDebounce","controller","AbortController","getValues","signal","then","result","applyValidateResult","state","formValidateStates","get","timer","round","marked","waiters","set","getFormValidateState","clearTimeout","add","emit","setTimeout","abort","outcome","error","reject","runFormValidateRound","settleFormValidate","SETTLED","push","runFormValidate","hasErrors","setFormErrors","segments","footprint","Object","entries","value","path","isIndex","normalizePath","setError","recordFootprint","Array","isArray","isFieldError","createPath","stored","errors","validateDeps","formErrorFootprints","Map","getFormErrorFootprint","written","delete","JSON","parse","clearFormValidateErrors","clear","VALIDATION_OUTCOME","setParsedValues","values","WeakMap","waiter"],"mappings":"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"}
@@ -0,0 +1,238 @@
1
+ import { F as Form, P as Path, a as FieldError, e as FieldErrorEntry, V as ValidationMode, N as Name } from './form-CvmWHUrd.js';
2
+
3
+ declare function unsetValidatingByPath({ emitter, validating }: Form, path: Path): void;
4
+ declare function setValidatingByPath({ emitter, validating }: Form, path: Path): void;
5
+ /**
6
+ * Field validator. Returns an error (a string, a FieldError, or an array
7
+ * mixing both) or undefined when valid; may return a Promise for async
8
+ * validation.
9
+ *
10
+ * The second argument carries the validation context. `meta.signal` is
11
+ * aborted as soon as the round is superseded — a newer round started, or
12
+ * the field unregistered — so async validators can cancel their underlying
13
+ * work (fetch, timers) instead of racing a stale result home. Stale
14
+ * results are dropped independently by the registration's lock
15
+ * ({@link registerValidatorByPath}), so validators that ignore the signal
16
+ * stay correct too. Validators written against the older two-argument
17
+ * signature keep working.
18
+ */
19
+ type Validator = (value: any, meta: {
20
+ form: Form;
21
+ path: Path;
22
+ signal: AbortSignal;
23
+ }) => string | FieldError | (string | FieldError)[] | undefined | Promise<string | FieldError | (string | FieldError)[] | undefined>;
24
+ /**
25
+ * Synchronous pre-validator for {@link registerValidatorByPath}'s `sync`
26
+ * accessor — declarative `required` rules compiled by `rulesToValidator`
27
+ * in practice, but any sync-only check works. Runs on every kick, never
28
+ * debounced: its errors land immediately and, while present,
29
+ * short-circuit the debounced validator for that kick (the expensive
30
+ * check never sees a value the gate already rejects). Must be synchronous
31
+ * — unlike a {@link Validator} it may not return a Promise — and its meta
32
+ * carries no `signal`: there is nothing to abort in a synchronous check.
33
+ */
34
+ type SyncValidator = (value: any, meta: {
35
+ form: Form;
36
+ path: Path;
37
+ }) => string | FieldError | (string | FieldError)[] | undefined;
38
+ /** Live options for {@link registerValidatorByPath}: read at every kick
39
+ * through accessors, so callers (React's `useValidate`) can swap the
40
+ * validator/debounce/sync-gate per render without re-subscribing the
41
+ * registration mid-flight. */
42
+ type ValidatorRegistration = {
43
+ /** Current debounced validator (or undefined — a sync-only
44
+ * registration). */
45
+ validate: () => Validator | undefined;
46
+ /** Debounce delay in milliseconds; 0 (default) runs immediately. */
47
+ debounce: () => number;
48
+ /** Synchronous pre-validator, run on every kick (never debounced). */
49
+ sync: () => SyncValidator | undefined;
50
+ };
51
+ /**
52
+ * Register a field validator's kick at `path` in {@link Form.validators}
53
+ * — the framework-free machinery behind `useValidate`. Returns a
54
+ * disposer that drops the registration and cancels any pending debounce
55
+ * window or in-flight round (its signal aborts and its validating mark
56
+ * is released).
57
+ *
58
+ * Contract of the registered kick (the same contract `trigger` /
59
+ * `ensureValidate` rely on when they run every entry, and the
60
+ * user-change gate relies on when it runs the changed path's entry):
61
+ * - the `sync` gate runs immediately on every kick — never debounced —
62
+ * and while it returns errors, the debounced validator is skipped for
63
+ * that kick and any pending window or in-flight round is superseded;
64
+ * - a positive `debounce` merges kicks inside the window: only the last
65
+ * one runs the validator, and while the timer is pending the field
66
+ * counts as validating so `trigger`/`ensureValidate` wait it out;
67
+ * - async results land under a lock: a superseded round's outcome —
68
+ * rejection included — is dropped, and only the owning round releases
69
+ * the validating mark;
70
+ * - a synchronous throw inside the validator propagates to the caller
71
+ * (the validating mark is not left stuck behind it).
72
+ *
73
+ * Registering at a path already registered by another mount replaces it
74
+ * (last-wins, the historical `useValidate` behavior); the disposer drops
75
+ * its own registration unconditionally.
76
+ *
77
+ * @param form
78
+ * @param path
79
+ * @param registration live validator/debounce/sync accessors
80
+ * @return disposer: unregister and cancel pending work
81
+ */
82
+ declare function registerValidatorByPath(form: Form, path: Path, registration: ValidatorRegistration): () => void;
83
+ /**
84
+ * Set field error
85
+ * @param form
86
+ * @param name
87
+ * @param error string is normalized to {type: 'custom', message}; a
88
+ * FieldError object is stored as-is; an array holds several errors
89
+ * (falsy items dropped, strings normalized); undefined clears
90
+ */
91
+ /** Options accepted by {@link trigger}. `shouldTouch` defaults to `false`;
92
+ * omitting the options object entirely keeps the plain validate-only
93
+ * behavior, so the historical two-argument calls are untouched. */
94
+ type TriggerOptions = {
95
+ /** Mark every path in the triggered scope as touched — even when
96
+ * validation fails — once the round settles. Mirrors react-hook-form's
97
+ * trigger `shouldTouch`. Defaults to `false`. */
98
+ shouldTouch?: boolean;
99
+ /**
100
+ * Focus the first errored field in the triggered scope once the round
101
+ * settles (and only when the round left errors) — react-hook-form's
102
+ * trigger `shouldFocus` counterpart. Rides the 'focusError' event
103
+ * channel like a failed submit's auto-focus: only mounted bound fields
104
+ * react, unmounted ones are silent no-ops. Without `name` the first key
105
+ * of the errors Map wins (the same rule handleSubmit applies); with
106
+ * `name` the first errored triggered key does. Defaults to `false`.
107
+ */
108
+ shouldFocus?: boolean;
109
+ };
110
+ /**
111
+ * Trigger field validation.
112
+ *
113
+ * Without `name` every registered field validator runs. A single `name` —
114
+ * dotted string or segments array — runs only that field's validator, and
115
+ * an array of names runs each one in order. An empty array is a no-op, as
116
+ * is any name with no registered validator. An array argument counts as
117
+ * one segments path only when it mixes in numbers (`['items', 0]`); pure
118
+ * string arrays are name lists, so `['a', 'b']` triggers fields `a` and
119
+ * `b`, not the nested path `a.b`.
120
+ *
121
+ * `options.shouldTouch` marks the triggered scope — the given names, or
122
+ * every registered field when `name` is omitted — as touched after the
123
+ * round settles, whether validation passed or failed. The wait/settle
124
+ * logic is untouched: the marking rides on top of the settled round, so
125
+ * subscribers observe errors and touched together rather than mid-flight.
126
+ *
127
+ * The returned promise waits for the triggered validation to settle —
128
+ * async validators included — so their errors have already landed in
129
+ * `form.errors` when it resolves. It never rejects: landing errors is the
130
+ * expected outcome here, not a failure. Resolves `true` when the triggered
131
+ * scope is error-free, `false` otherwise. Without `name` the scope is all
132
+ * fields plus the form-level `validate` result (which runs after field
133
+ * validators settle, same pipeline as {@link ensureValidate}); with `name`
134
+ * only those fields' own errors count and form-level `validate` is
135
+ * skipped (RHF semantics).
136
+ *
137
+ * Fire-and-forget callers may ignore the promise: the validator kicks
138
+ * still happen synchronously, matching the pre-promise behavior.
139
+ *
140
+ * @param form
141
+ * @param name field name(s) to trigger, or all fields when omitted
142
+ * @param options extra behavior toggles ({@link TriggerOptions}); omitted,
143
+ * validation alone runs — no touched marking
144
+ * @return whether the triggered scope is error-free once validation settles
145
+ */
146
+ declare function trigger(form: Form, name?: Name | Name[], options?: TriggerOptions): Promise<boolean>;
147
+ /**
148
+ * Form-level twin of the gated validator kick in `useField`'s onChange:
149
+ * re-run the form-level `validate` after a user change to a field listed
150
+ * in `validateDeps`. Called from the field's own change pipeline (typing
151
+ * and `changeValue` alike — both route through the mounted field's
152
+ * onChange), so programmatic `setValue` writes do not re-run it, exactly
153
+ * like they do not re-run field validators.
154
+ *
155
+ * The gate mirrors the per-field matrix with the *changed field's*
156
+ * effective `mode` (a per-field override governs when its changes may
157
+ * fire validation) and the form-level `reValidateMode` against the last
158
+ * round's error footprint ({@link hasFormValidateErrors} — field
159
+ * validators' errors never arm this kick):
160
+ * - `mode` `'onChange'`/`'all'` — every dep change re-runs;
161
+ * - `mode` `'onTouched'` — dep changes re-run once the field was touched;
162
+ * - otherwise the re-run waits for `reValidateMode: 'onChange'` (the
163
+ * default) while the last round's error is still live — the
164
+ * submit-then-fix flow: the mismatch lands on submit, editing the
165
+ * dependency re-runs the validate and clears it.
166
+ * `reValidateMode: 'onBlur'`/`'onSubmit'` never re-run on a change (a
167
+ * change is not a blur; submit re-runs are the submit pipeline's job).
168
+ *
169
+ * The kick is fire-and-forget: async round rejections are swallowed
170
+ * (nothing in an event handler can await them), while a synchronous
171
+ * throw inside the validate callback propagates to the caller exactly
172
+ * like a field validator's does.
173
+ *
174
+ * A no-op unless the form set `validateDeps` listing `path` — forms
175
+ * without the option pay one property check here.
176
+ */
177
+ declare function revalidateFormOnChange(form: Form, path: Path, mode: ValidationMode): void;
178
+ /** Register one field's validateDeps declaration: `key` re-validates when
179
+ * any path in `depKeys` takes a user change. Idempotent per (key, dep)
180
+ * pair, so StrictMode's double effect is harmless. */
181
+ declare function registerFieldValidateDeps(form: Form, key: string, depKeys: string[]): void;
182
+ /** Drop one field's validateDeps registration ({@link
183
+ * registerFieldValidateDeps}). Entries nobody lists anymore are removed so
184
+ * the registry never outlives its fields. */
185
+ declare function unregisterFieldValidateDeps(form: Form, key: string, depKeys: string[]): void;
186
+ /**
187
+ * Field-level twin of {@link revalidateFormOnChange}: after a user change
188
+ * to `path`, re-run every field validator that declared `path` in its
189
+ * `validateDeps` (useField option). Same channel, same gate: the kick
190
+ * rides the changed field's own onChange pipeline (typing and
191
+ * `changeValue` alike), so programmatic `setValue` writes never fire it —
192
+ * exactly like field validators and the form-level `validateDeps`.
193
+ *
194
+ * The gate mirrors the form-level matrix with the *changed field's*
195
+ * effective `mode` and the form-level `reValidateMode` against each
196
+ * dependent's live error:
197
+ * - `mode` `'onChange'`/`'all'` — every dep change re-runs the dependent;
198
+ * - `mode` `'onTouched'` — once the changed field was touched;
199
+ * - otherwise the re-run waits for `reValidateMode: 'onChange'` (the
200
+ * default) while the dependent still shows an error — the
201
+ * submit-then-fix flow: the mismatch lands on submit, editing the
202
+ * dependency re-validates the dependent and a passing round clears it
203
+ * (a field validator owns its whole key, so the re-run's result
204
+ * replaces whatever the previous round wrote — the field-level shape
205
+ * of the form-level footprint reclaim).
206
+ *
207
+ * The kick is an ordinary validator kick: the dependent's own
208
+ * `validateDebounce` window applies, and a synchronous throw inside its
209
+ * validate propagates to the caller like any field validator's would.
210
+ *
211
+ * A no-op unless some field declared `path` as a dep — forms without any
212
+ * field-level `validateDeps` pay one property check here.
213
+ */
214
+ declare function revalidateDependentsOnChange(form: Form, path: Path, mode: ValidationMode): void;
215
+ /** The Error {@link ensureValidate} rejects with: `message` is the first
216
+ * error's display text ({@link getFirstError}) — the long-standing shape
217
+ * — and `.errors` carries the complete flattened error list ({@link
218
+ * getErrors}: `{path, type, message}` entries, dotted display paths) so
219
+ * catchers can branch on types and locate fields without re-reading the
220
+ * form. */
221
+ type FormValidationError = Error & {
222
+ errors: FieldErrorEntry[];
223
+ };
224
+ /**
225
+ * Validate and throw if any field error.
226
+ * @param form
227
+ * @return resolve if no error; reject and stop validate if has an error
228
+ */
229
+ declare function ensureValidate(form: Form): Promise<void>;
230
+ /**
231
+ * Validate and return if any field error.
232
+ * @param form
233
+ * @return error message string or void
234
+ */
235
+ declare function validate(form: Form): Promise<void | string>;
236
+
237
+ export { registerValidatorByPath as b, revalidateDependentsOnChange as c, revalidateFormOnChange as d, ensureValidate as e, unsetValidatingByPath as f, registerFieldValidateDeps as r, setValidatingByPath as s, trigger as t, unregisterFieldValidateDeps as u, validate as v };
238
+ export type { FormValidationError as F, SyncValidator as S, TriggerOptions as T, Validator as V, ValidatorRegistration as a };
@@ -0,0 +1,2 @@
1
+ "use strict";var e=require("@for-fun/event-emitter"),r=require("./errors-CxSjrWJO.cjs.js"),t=require("./values-CDNAYEOB.cjs.js");function n(e,t,i=[],s){Object.entries(t).forEach(([t,a])=>{const l=[...i,...r.isIndex(t)?[t]:r.normalizePath(t)];"string"==typeof a?a&&(r.setError(e,l,a),o(e,l,s)):Array.isArray(a)||r.isFieldError(a)?(r.setError(e,l,a),o(e,l,s)):a&&"object"==typeof a&&n(e,a,l,s)})}function o(e,t,n){if(!n)return;const o=r.create(t),i=e.errors.get(o.key);i&&n.set(o.key,i)}function i(t,o){const i=t.validateDeps?function(e){let r=s.get(e);r||(r=new Map,s.set(e,r));return r}(t):void 0;if(i&&(!function(t,n){for(const[o,i]of n){t.errors.get(o)===i&&(t.errors.delete(o),e.emit(t.emitter,"errors",r.create(JSON.parse(o))))}}(t,i),i.clear()),o){if("object"==typeof o&&r.VALIDATION_OUTCOME in o){const e=o;return e.errors&&n(t,e.errors,[],i),void r.setParsedValues(t,e.values)}n(t,o,[],i)}}const s=new WeakMap;const a="__form_validate__";const l=Symbol("form-validate-settled"),c=new WeakMap;function u(r,t,n,o){if(t.round!==n)return;if(t.round=null,null!==t.timer)return;t.marked&&(t.marked=!1,r.validating.delete(a),e.emit(r.emitter,"validating"));const i=t.waiters;t.waiters=[];for(const e of i)o===l?e.resolve():e.reject(o)}exports.trigger=async function(n,o,s){const f=e=>r.waitUntil(n.emitter,"validating",()=>function(e){for(const r of e.validating)if(r!==a)return!1;return!0}(n),()=>!1);return n.validators.forEach(e=>e()),await f(),n.validate&&await function(r){const n=r.validate;if(!n)return Promise.resolve();const o=r.validateDebounce??0;if(o<=0){const e=new AbortController;return Promise.resolve(n(t.getValues(r),{form:r,signal:e.signal})).then(e=>{i(r,e)})}const s=function(e){let r=c.get(e);r||(r={timer:null,controller:null,round:null,marked:!1,waiters:[]},c.set(e,r));return r}(r);null!==s.timer?clearTimeout(s.timer):(s.marked=!0,r.validating.add(a),e.emit(r.emitter,"validating"));return s.timer=setTimeout(()=>{s.timer=null;const e=s.round={};(function(e,r,n){const o=e.validate;if(!o)return Promise.resolve();r.controller?.abort();const s=r.controller=new AbortController;let a;try{a=Promise.resolve(o(t.getValues(e),{form:e,signal:s.signal}))}catch(e){a=Promise.reject(e)}return a.then(t=>{r.round===n&&i(e,t)},e=>{if(r.round===n)throw e})})(r,s,e).then(()=>u(r,s,e,l),t=>u(r,s,e,t))},o),new Promise((e,r)=>{s.waiters.push({resolve:e,reject:r})})}(n),!r.hasErrors(n)};
2
+ //# sourceMappingURL=validate-DAfz8Nbb.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validate-DAfz8Nbb.cjs.js","sources":["../src/core/validate.ts"],"sourcesContent":["import {emit} from '@for-fun/event-emitter';\nimport createPath from '../path';\nimport type {Name, Path, PathSegments} from '../path';\nimport {isIndex, isPromise, normalizePath, waitUntil} from '../util';\nimport type {\n FieldError,\n FieldErrorEntry,\n Form,\n ValidationMode,\n ValidateResult,\n ValidationOutcome\n} from '../form';\nimport {\n VALIDATION_OUTCOME,\n getErrors,\n getFirstError,\n hasErrors,\n setError,\n setErrorByPath\n} from './errors';\nimport {hasTouchedByPath, setTouchedByPath} from './touched';\nimport {getValueByPath, getValues} from './values';\nimport {isFieldError, isSegmentsPath, setParsedValues} from './internals';\n\nexport function unsetValidatingByPath(\n {emitter, validating}: Form,\n path: Path\n): void {\n validating.delete(path.key);\n // Path payload lets key-scoped subscribers (onKeyEvent) skip unrelated\n // fields; payload-less listeners ignore it.\n emit(emitter, 'validating', path);\n}\n\nexport function setValidatingByPath(\n {emitter, validating}: Form,\n path: Path\n): void {\n validating.add(path.key);\n emit(emitter, 'validating', path);\n}\n\n/**\n * Field validator. Returns an error (a string, a FieldError, or an array\n * mixing both) or undefined when valid; may return a Promise for async\n * validation.\n *\n * The second argument carries the validation context. `meta.signal` is\n * aborted as soon as the round is superseded — a newer round started, or\n * the field unregistered — so async validators can cancel their underlying\n * work (fetch, timers) instead of racing a stale result home. Stale\n * results are dropped independently by the registration's lock\n * ({@link registerValidatorByPath}), so validators that ignore the signal\n * stay correct too. Validators written against the older two-argument\n * signature keep working.\n */\nexport type Validator = (\n value: any,\n meta: {form: Form; path: Path; signal: AbortSignal}\n) =>\n | string\n | FieldError\n | (string | FieldError)[]\n | undefined\n | Promise<string | FieldError | (string | FieldError)[] | undefined>;\n\n/**\n * Synchronous pre-validator for {@link registerValidatorByPath}'s `sync`\n * accessor — declarative `required` rules compiled by `rulesToValidator`\n * in practice, but any sync-only check works. Runs on every kick, never\n * debounced: its errors land immediately and, while present,\n * short-circuit the debounced validator for that kick (the expensive\n * check never sees a value the gate already rejects). Must be synchronous\n * — unlike a {@link Validator} it may not return a Promise — and its meta\n * carries no `signal`: there is nothing to abort in a synchronous check.\n */\nexport type SyncValidator = (\n value: any,\n meta: {form: Form; path: Path}\n) => string | FieldError | (string | FieldError)[] | undefined;\n\n/** Live options for {@link registerValidatorByPath}: read at every kick\n * through accessors, so callers (React's `useValidate`) can swap the\n * validator/debounce/sync-gate per render without re-subscribing the\n * registration mid-flight. */\nexport type ValidatorRegistration = {\n /** Current debounced validator (or undefined — a sync-only\n * registration). */\n validate: () => Validator | undefined;\n /** Debounce delay in milliseconds; 0 (default) runs immediately. */\n debounce: () => number;\n /** Synchronous pre-validator, run on every kick (never debounced). */\n sync: () => SyncValidator | undefined;\n};\n\n/**\n * Register a field validator's kick at `path` in {@link Form.validators}\n * — the framework-free machinery behind `useValidate`. Returns a\n * disposer that drops the registration and cancels any pending debounce\n * window or in-flight round (its signal aborts and its validating mark\n * is released).\n *\n * Contract of the registered kick (the same contract `trigger` /\n * `ensureValidate` rely on when they run every entry, and the\n * user-change gate relies on when it runs the changed path's entry):\n * - the `sync` gate runs immediately on every kick — never debounced —\n * and while it returns errors, the debounced validator is skipped for\n * that kick and any pending window or in-flight round is superseded;\n * - a positive `debounce` merges kicks inside the window: only the last\n * one runs the validator, and while the timer is pending the field\n * counts as validating so `trigger`/`ensureValidate` wait it out;\n * - async results land under a lock: a superseded round's outcome —\n * rejection included — is dropped, and only the owning round releases\n * the validating mark;\n * - a synchronous throw inside the validator propagates to the caller\n * (the validating mark is not left stuck behind it).\n *\n * Registering at a path already registered by another mount replaces it\n * (last-wins, the historical `useValidate` behavior); the disposer drops\n * its own registration unconditionally.\n *\n * @param form\n * @param path\n * @param registration live validator/debounce/sync accessors\n * @return disposer: unregister and cancel pending work\n */\nexport function registerValidatorByPath(\n form: Form,\n path: Path,\n registration: ValidatorRegistration\n): () => void {\n // The pending debounce timer and the current round's controller live in\n // this closure so the disposer below can cancel them.\n let timer: ReturnType<typeof setTimeout> | null = null;\n let controller: AbortController | null = null;\n // Whether this registration currently holds the path's slot in\n // form.validating. The mark is taken when a debounce window opens or an\n // async round starts, and released by whichever round settles last —\n // including a later sync round that supersedes an in-flight async one\n // (its own .finally is lock-gated out by then).\n let marked = false;\n // The async-round lock: only the latest round may land its result or\n // release the mark; a superseded round's outcome is dropped wholesale.\n let lock: object | null = null;\n // Which source wrote the error currently on display — the sync gate or\n // the debounced validator. Tracked so a passing sync check can clear\n // its own stale error immediately instead of leaving it on screen until\n // the debounced round lands. External writers (setError, form-level\n // validate) are invisible here; a passing round clearing them matches\n // the long-standing \"a field validator owns its whole key\" contract.\n let errorSource: 'sync' | 'validator' | null = null;\n /** Does a validator result land errors? `[]` normalizes away exactly\n * like undefined in setErrorByPath. */\n const hasErrors = (errors: any): boolean =>\n errors !== undefined && !(Array.isArray(errors) && errors.length === 0);\n const mark = () => {\n if (marked) return;\n marked = true;\n setValidatingByPath(form, path);\n };\n const unmark = () => {\n if (!marked) return;\n marked = false;\n unsetValidatingByPath(form, path);\n };\n\n /** Run the synchronous gate on the field's current value. Its errors\n * land immediately — the gate is never debounced. Returns true when\n * errors landed (the kick's whole outcome for the debounced validator).\n * A passing gate clears the field's errors when they were its own from\n * an earlier kick, or when no debounced validator exists to own the\n * round. */\n const runSync = (): boolean => {\n const sync = registration.sync();\n if (!sync) return false;\n const errors = sync(getValueByPath(form, path), {form, path});\n if (!hasErrors(errors)) {\n // A stale error the gate itself wrote is answered by the gate\n // alone; a rules-only registration's passing check is the whole\n // round. With a debounced validator registered, its upcoming round\n // owns the outcome and lands it later.\n if (!registration.validate() || errorSource === 'sync') {\n setErrorByPath(form, path, undefined);\n errorSource = null;\n }\n return false;\n }\n setErrorByPath(form, path, errors);\n errorSource = 'sync';\n return true;\n };\n\n /** Drop any pending window or in-flight round without landing it: the\n * sync gate now owns the outcome, so the debounced validator must not\n * run for this value. */\n const supersede = () => {\n if (timer !== null) {\n clearTimeout(timer);\n timer = null;\n }\n controller?.abort();\n lock = {};\n };\n\n /** Run the debounced validator on the field's current value and land\n * its result — the sync gate has already passed. */\n const runValidator = () => {\n const fn = registration.validate();\n if (!fn) {\n unmark();\n return;\n }\n // Abort the superseded round's signal: a listening validator should\n // stop its underlying work. The lock refresh below independently\n // drops any result that still arrives, signal or not.\n controller?.abort();\n controller = new AbortController();\n const round = (lock = {});\n let result;\n try {\n result = fn(getValueByPath(form, path), {\n form,\n path,\n signal: controller.signal\n });\n } catch (e) {\n // A throwing sync validator propagates to the caller as it always\n // has; just don't leave the validating mark stuck behind it.\n unmark();\n throw e;\n }\n if (!isPromise(result)) {\n setErrorByPath(form, path, result);\n errorSource = hasErrors(result) ? 'validator' : null;\n // Error first, then release the mark: 'validating' subscribers\n // (trigger) re-read state on wake and must see the landed error.\n unmark();\n return;\n }\n mark();\n result\n .then(\n (error: string | FieldError | (string | FieldError)[] | undefined) => {\n if (lock === round) {\n setErrorByPath(form, path, error);\n errorSource = hasErrors(error) ? 'validator' : null;\n }\n }\n )\n // A rejected round is the normal way a signal-listening validator\n // gives up (fetch throws AbortError once aborted); swallow it and\n // let the owning round write the outcome.\n .catch(() => {})\n .finally(() => {\n if (lock === round) {\n unmark();\n lock = null;\n }\n });\n };\n\n /** A debounce window fired: the value may have drifted since the last\n * kick (programmatic writes do not kick validators), so re-run the\n * sync gate before spending the debounced validator on a value the\n * gate already rejects. */\n const run = () => {\n timer = null;\n if (runSync()) {\n supersede();\n unmark();\n return;\n }\n runValidator();\n };\n\n const kick = () => {\n if (runSync()) {\n supersede();\n unmark();\n return;\n }\n if (!registration.validate()) return;\n const debounce = registration.debounce();\n if (debounce > 0) {\n // Only the last kick inside the window runs: restart the timer on\n // every kick. The mark keeps trigger/ensureValidate's\n // validating-set wait covering the pending timer, not just\n // in-flight promises.\n if (timer !== null) clearTimeout(timer);\n else mark();\n timer = setTimeout(run, debounce);\n return;\n }\n runValidator();\n };\n\n form.validators.set(path.key, kick);\n return () => {\n form.validators.delete(path.key);\n if (timer !== null) {\n clearTimeout(timer);\n timer = null;\n }\n unmark();\n controller?.abort();\n };\n}\n\n/**\n * Set field error\n * @param form\n * @param name\n * @param error string is normalized to {type: 'custom', message}; a\n * FieldError object is stored as-is; an array holds several errors\n * (falsy items dropped, strings normalized); undefined clears\n */\n/** Options accepted by {@link trigger}. `shouldTouch` defaults to `false`;\n * omitting the options object entirely keeps the plain validate-only\n * behavior, so the historical two-argument calls are untouched. */\nexport type TriggerOptions = {\n /** Mark every path in the triggered scope as touched — even when\n * validation fails — once the round settles. Mirrors react-hook-form's\n * trigger `shouldTouch`. Defaults to `false`. */\n shouldTouch?: boolean;\n /**\n * Focus the first errored field in the triggered scope once the round\n * settles (and only when the round left errors) — react-hook-form's\n * trigger `shouldFocus` counterpart. Rides the 'focusError' event\n * channel like a failed submit's auto-focus: only mounted bound fields\n * react, unmounted ones are silent no-ops. Without `name` the first key\n * of the errors Map wins (the same rule handleSubmit applies); with\n * `name` the first errored triggered key does. Defaults to `false`.\n */\n shouldFocus?: boolean;\n};\n\n/**\n * Trigger field validation.\n *\n * Without `name` every registered field validator runs. A single `name` —\n * dotted string or segments array — runs only that field's validator, and\n * an array of names runs each one in order. An empty array is a no-op, as\n * is any name with no registered validator. An array argument counts as\n * one segments path only when it mixes in numbers (`['items', 0]`); pure\n * string arrays are name lists, so `['a', 'b']` triggers fields `a` and\n * `b`, not the nested path `a.b`.\n *\n * `options.shouldTouch` marks the triggered scope — the given names, or\n * every registered field when `name` is omitted — as touched after the\n * round settles, whether validation passed or failed. The wait/settle\n * logic is untouched: the marking rides on top of the settled round, so\n * subscribers observe errors and touched together rather than mid-flight.\n *\n * The returned promise waits for the triggered validation to settle —\n * async validators included — so their errors have already landed in\n * `form.errors` when it resolves. It never rejects: landing errors is the\n * expected outcome here, not a failure. Resolves `true` when the triggered\n * scope is error-free, `false` otherwise. Without `name` the scope is all\n * fields plus the form-level `validate` result (which runs after field\n * validators settle, same pipeline as {@link ensureValidate}); with `name`\n * only those fields' own errors count and form-level `validate` is\n * skipped (RHF semantics).\n *\n * Fire-and-forget callers may ignore the promise: the validator kicks\n * still happen synchronously, matching the pre-promise behavior.\n *\n * @param form\n * @param name field name(s) to trigger, or all fields when omitted\n * @param options extra behavior toggles ({@link TriggerOptions}); omitted,\n * validation alone runs — no touched marking\n * @return whether the triggered scope is error-free once validation settles\n */\nexport async function trigger(\n form: Form,\n name?: Name | Name[],\n options?: TriggerOptions\n): Promise<boolean> {\n // Never reject (an error landing is a normal outcome, not a failure), so\n // waitUntil's isReject is permanently false. Without a name the wait is\n // deliberately conservative — every FIELD validator, unrelated in-flight\n // ones included, because the round covers the whole form (and the\n // form-level validate's own window is excluded via fieldsSettled —\n // callers wait that out through the kick's promise instead, so a pending\n // window never gates the next kick). With a name the wait narrows to the\n // triggered keys only: a slow async validator on field B must not hold\n // trigger('a') hostage when the round never reads B.\n const settle = (keys?: string[]) =>\n waitUntil(\n form.emitter,\n 'validating',\n () =>\n keys === undefined\n ? fieldsSettled(form)\n : keys.every(key => !form.validating.has(key)),\n () => false\n );\n\n if (name === undefined) {\n form.validators.forEach(validator => validator());\n await settle();\n if (form.validate) await runFormValidate(form);\n // shouldTouch marks the whole registered scope — every key the round\n // could have validated — pass or fail alike.\n if (options?.shouldTouch) touchKeys(form, [...form.validators.keys()]);\n // First error across the errors Map — the same rule a failed submit's\n // auto-focus applies (a form-level error may land first; it has no\n // element, so it is a silent no-op like every unbound path).\n if (options?.shouldFocus) {\n const firstKey = form.errors.keys().next().value;\n if (firstKey !== undefined) emit(form.emitter, 'focusError', firstKey);\n }\n return !hasErrors(form);\n }\n\n const keys: string[] =\n typeof name === 'string' || isSegmentsPath(name)\n ? [createPath(name).key]\n : name.map(one => createPath(one).key);\n keys.forEach(key => form.validators.get(key)?.());\n await settle(keys);\n if (options?.shouldTouch) touchKeys(form, keys);\n // Focus the first errored key among the triggered scope — trigger('a')\n // never focuses B's pre-existing error.\n if (options?.shouldFocus) {\n const firstKey = keys.find(key => form.errors.has(key));\n if (firstKey !== undefined) emit(form.emitter, 'focusError', firstKey);\n }\n return keys.every(key => !form.errors.has(key));\n}\n\n/** trigger's `shouldTouch` marking: touch every key in the triggered scope\n * through {@link setTouchedByPath}, which no-ops on already-touched keys\n * and emits the path-carrying 'touched' event per newly touched one. Keys\n * are the stored JSON-stringified segments shape, so parse them back into\n * Path — normalizePath passes segment arrays through untouched, making the\n * key round-trip exact. */\nfunction touchKeys(form: Form, keys: string[]): void {\n keys.forEach(key => setTouchedByPath(form, createPath(JSON.parse(key))));\n}\n\n/**\n * Flatten a form-level validate result and write each leaf error through\n * setError. Nested objects descend into deeper paths ({a: {b: 'msg'}} sets\n * the 'a.b' error), array values contribute every non-empty string they\n * hold as separate errors (zod flatten() formErrors style), and\n * FieldError-shaped objects are stored as-is. Falsy values are skipped.\n *\n * When `footprint` is passed (validateDeps forms only), every leaf this\n * round actually stored is recorded into it — the exact stored array —\n * so the next round can drop exactly what this one wrote.\n */\nfunction setFormErrors(\n form: Form,\n result: Record<string, any>,\n segments: PathSegments = [],\n footprint?: Map<string, FieldError[]>\n): void {\n Object.entries(result).forEach(([key, value]) => {\n // Error-tree keys are explicit object keys, not path expressions:\n // a numeric key ('0' — Standard Schema issue paths stringify array\n // indices) stays a literal string segment instead of feeding the\n // path parser, whose dotted-numeric rule governs path strings only.\n const path: PathSegments = [\n ...segments,\n ...(isIndex(key) ? [key] : normalizePath(key))\n ];\n if (typeof value === 'string') {\n if (value) {\n setError(form, path, value);\n recordFootprint(form, path, footprint);\n }\n } else if (Array.isArray(value)) {\n setError(form, path, value);\n recordFootprint(form, path, footprint);\n } else if (isFieldError(value)) {\n setError(form, path, value);\n recordFootprint(form, path, footprint);\n } else if (value && typeof value === 'object') {\n setFormErrors(form, value, path, footprint);\n }\n });\n}\n\n/** Record one leaf write of a form-level validate round: the path key and\n * the exact array now stored there. Nothing is recorded when the write\n * normalized away (all-empty arrays) — there is no error to own. The\n * stored array is read back from the errors Map because setErrorByPath\n * owns normalization. */\nfunction recordFootprint(\n form: Form,\n segments: PathSegments,\n footprint: Map<string, FieldError[]> | undefined\n): void {\n if (!footprint) return;\n const path = createPath(segments);\n const stored = form.errors.get(path.key);\n if (stored) footprint.set(path.key, stored);\n}\n\n/**\n * Land a form-level validate result. A plain record keeps the\n * long-standing behavior — flattened into field errors by\n * {@link setFormErrors}. A branded {@link ValidationOutcome} splits\n * instead: `errors` flattens exactly like a plain record, and `values`\n * (the schema's parsed output — coerced/transformed values included)\n * becomes the form's parsedValues baseline. Falsy results are skipped,\n * branded or not.\n *\n * Forms that opted into `validateDeps` additionally get round-scoped\n * error ownership: before the new result lands, the errors the previous\n * round wrote are dropped ({@link clearFormValidateErrors}), so a re-run\n * that passes makes the cross-field error disappear — and the new\n * round's own writes become the tracked footprint. Forms without the\n * option keep the historical write-only behavior untouched.\n */\nfunction applyValidateResult(\n form: Form,\n result: ValidateResult<any> | undefined\n): void {\n const footprint = form.validateDeps ? getFormErrorFootprint(form) : undefined;\n if (footprint) {\n clearFormValidateErrors(form, footprint);\n footprint.clear();\n }\n if (!result) return;\n if (typeof result === 'object' && VALIDATION_OUTCOME in result) {\n const outcome = result as ValidationOutcome<any>;\n if (outcome.errors) setFormErrors(form, outcome.errors, [], footprint);\n setParsedValues(form, outcome.values);\n return;\n }\n setFormErrors(form, result as Record<string, any>, [], footprint);\n}\n\n/** Per-form error footprint of the last form-level validate round: every\n * path key it flattened onto, with the exact array instance it stored.\n * Tracked only for forms that opted into `validateDeps` — held in a\n * WeakMap so the Form shape and the non-opted pipeline stay untouched. */\nconst formErrorFootprints = new WeakMap<Form, Map<string, FieldError[]>>();\n\nfunction getFormErrorFootprint(form: Form): Map<string, FieldError[]> {\n let footprint = formErrorFootprints.get(form);\n if (!footprint) {\n footprint = new Map();\n formErrorFootprints.set(form, footprint);\n }\n return footprint;\n}\n\n/** Does the form still show an error the last form-level round wrote?\n * Compared by identity, not key membership: once a field validator,\n * `setServerErrors`, a manual `setError` or `clearErrors` replaces the\n * stored array, that error is no longer the round's to own — neither the\n * dep-change gate nor the next round's clearing may touch it. */\nfunction hasFormValidateErrors(form: Form): boolean {\n const footprint = formErrorFootprints.get(form);\n if (!footprint) return false;\n for (const [key, written] of footprint) {\n if (form.errors.get(key) === written) return true;\n }\n return false;\n}\n\n/** Drop the last form-level round's errors before the next round lands.\n * Per key the stored array is identity-checked — an error overwritten or\n * cleared by anyone else in between survives — and each drop emits the\n * same path-payload 'errors' event {@link setErrorByPath} would, so\n * subscribed fields re-render exactly like on any error write. */\nfunction clearFormValidateErrors(\n form: Form,\n footprint: Map<string, FieldError[]>\n): void {\n for (const [key, written] of footprint) {\n const stored = form.errors.get(key);\n if (stored !== written) continue;\n form.errors.delete(key);\n emit(form.emitter, 'errors', createPath(JSON.parse(key)));\n }\n}\n\n/** Key the form-level validate round reserves in `form.validating` while\n * its debounce window is pending or its async round is in flight. Real\n * path keys are JSON-stringified segments (always bracketed), so a bare\n * word can never collide. */\nconst FORM_VALIDATING_KEY = '__form_validate__';\n\n/** Are all FIELD validation rounds drained? trigger/ensureValidate wait on\n * this before kicking the form-level validate (its errors gate whether the\n * form-level round may run at all). The form validate's own reserved key\n * is deliberately excluded: its window is waited out through the kick's\n * returned promise instead, so a pending window or in-flight form round\n * never gates the next kick — a kick during an in-flight round opens a\n * new window and the newer round supersedes, mirroring the per-field\n * `validateDebounce` contract. */\nfunction fieldsSettled(form: Form): boolean {\n for (const key of form.validating) {\n if (key !== FORM_VALIDATING_KEY) return false;\n }\n return true;\n}\n\n/** Sentinel telling {@link settleFormValidate} the round landed cleanly —\n * distinct from every rejection payload, including `undefined`. */\nconst SETTLED = Symbol('form-validate-settled');\n\n/** Per-form bookkeeping for the debounced form-level validate: the\n * pending window timer, the in-flight round, and the waiters merged into\n * the current window group. Held in a WeakMap so the Form instance shape\n * is untouched for forms that never set `validateDebounce`. */\ntype FormValidateState = {\n timer: ReturnType<typeof setTimeout> | null;\n controller: AbortController | null;\n /** Identity of the in-flight round; a superseded round's outcome\n * (rejection included) is dropped by comparing against it. */\n round: object | null;\n /** Whether this state currently holds FORM_VALIDATING_KEY in\n * form.validating. */\n marked: boolean;\n waiters: Array<{resolve: () => void; reject: (error: unknown) => void}>;\n};\n\nconst formValidateStates = new WeakMap<Form, FormValidateState>();\n\nfunction getFormValidateState(form: Form): FormValidateState {\n let state = formValidateStates.get(form);\n if (!state) {\n state = {\n timer: null,\n controller: null,\n round: null,\n marked: false,\n waiters: []\n };\n formValidateStates.set(form, state);\n }\n return state;\n}\n\n/**\n * Run the form-level `validate` and land its result, honoring the form's\n * `validateDebounce` option.\n *\n * Undebounced (`0`/undefined) the caller's await *is* the validate call —\n * the long-standing pipeline, unchanged: no validating mark, no round\n * gating, immediate values snapshot, rejection propagating to the caller.\n *\n * Debounced, the kick opens (or restarts — kicks inside the window merge)\n * a window during which the form counts as validating, so `trigger` /\n * `ensureValidate` / submit wait the window out exactly like a field's\n * `validateDebounce` window. When the timer fires, the round reads the\n * then-current values, supersedes (aborts) any in-flight round, and lands\n * its result. The returned promise settles once the window group's final\n * round has landed — rejecting when that round's validate callback threw\n * or its promise rejected, mirroring the undebounced propagation — so\n * merged callers all observe the same outcome.\n *\n * Only called under `if (form.validate)`.\n */\nfunction runFormValidate(form: Form): Promise<void> {\n const validate = form.validate;\n if (!validate) return Promise.resolve();\n const debounce = form.validateDebounce ?? 0;\n if (debounce <= 0) {\n // Standalone controller: nothing supersedes an undebounced call, so\n // its signal never fires — it exists for argument-shape parity with\n // the debounced rounds (and with field-level meta.signal).\n const controller = new AbortController();\n return Promise.resolve(\n validate(getValues(form), {form, signal: controller.signal})\n ).then(result => {\n applyValidateResult(form, result);\n });\n }\n const state = getFormValidateState(form);\n // (Re)open the window: a kick while the timer is pending restarts it\n // (only the last kick's values run); one while a round is in flight\n // keeps the validating mark held and defers to the new window's round.\n if (state.timer !== null) clearTimeout(state.timer);\n else {\n state.marked = true;\n form.validating.add(FORM_VALIDATING_KEY);\n emit(form.emitter, 'validating');\n }\n state.timer = setTimeout(() => {\n state.timer = null;\n const round = (state.round = {});\n runFormValidateRound(form, state, round).then(\n () => settleFormValidate(form, state, round, SETTLED),\n error => settleFormValidate(form, state, round, error)\n );\n }, debounce);\n return new Promise<void>((resolve, reject) => {\n state.waiters.push({resolve, reject});\n });\n}\n\n/** Run one form-level validate round with the form's current values.\n * Aborts the previous in-flight round's signal; a superseded round's\n * outcome — rejection included — is dropped by the round gate, exactly\n * like the field-level lock. */\nfunction runFormValidateRound(\n form: Form,\n state: FormValidateState,\n round: object\n): Promise<void> {\n const validate = form.validate;\n if (!validate) return Promise.resolve();\n state.controller?.abort();\n const controller = (state.controller = new AbortController());\n let outcome: Promise<any>;\n try {\n outcome = Promise.resolve(\n validate(getValues(form), {form, signal: controller.signal})\n );\n } catch (error) {\n outcome = Promise.reject(error);\n }\n return outcome.then(\n result => {\n if (state.round === round) applyValidateResult(form, result);\n },\n error => {\n if (state.round === round) throw error;\n }\n );\n}\n\n/** Land the window group's outcome: release the validating mark — after\n * the round's errors/values have already landed, because 'validating'\n * subscribers (trigger, ensureValidate) re-read state on wake — and\n * settle every merged waiter. A superseded round never lands here (the\n * newer round owns the release), and a window that re-opened while the\n * round was in flight defers: the mark and the waiters carry over to the\n * pending timer's round. */\nfunction settleFormValidate(\n form: Form,\n state: FormValidateState,\n round: object,\n outcome: unknown\n): void {\n if (state.round !== round) return;\n state.round = null;\n if (state.timer !== null) return;\n if (state.marked) {\n state.marked = false;\n form.validating.delete(FORM_VALIDATING_KEY);\n emit(form.emitter, 'validating');\n }\n const waiters = state.waiters;\n state.waiters = [];\n for (const waiter of waiters) {\n if (outcome === SETTLED) waiter.resolve();\n else waiter.reject(outcome);\n }\n}\n\n/**\n * Form-level twin of the gated validator kick in `useField`'s onChange:\n * re-run the form-level `validate` after a user change to a field listed\n * in `validateDeps`. Called from the field's own change pipeline (typing\n * and `changeValue` alike — both route through the mounted field's\n * onChange), so programmatic `setValue` writes do not re-run it, exactly\n * like they do not re-run field validators.\n *\n * The gate mirrors the per-field matrix with the *changed field's*\n * effective `mode` (a per-field override governs when its changes may\n * fire validation) and the form-level `reValidateMode` against the last\n * round's error footprint ({@link hasFormValidateErrors} — field\n * validators' errors never arm this kick):\n * - `mode` `'onChange'`/`'all'` — every dep change re-runs;\n * - `mode` `'onTouched'` — dep changes re-run once the field was touched;\n * - otherwise the re-run waits for `reValidateMode: 'onChange'` (the\n * default) while the last round's error is still live — the\n * submit-then-fix flow: the mismatch lands on submit, editing the\n * dependency re-runs the validate and clears it.\n * `reValidateMode: 'onBlur'`/`'onSubmit'` never re-run on a change (a\n * change is not a blur; submit re-runs are the submit pipeline's job).\n *\n * The kick is fire-and-forget: async round rejections are swallowed\n * (nothing in an event handler can await them), while a synchronous\n * throw inside the validate callback propagates to the caller exactly\n * like a field validator's does.\n *\n * A no-op unless the form set `validateDeps` listing `path` — forms\n * without the option pay one property check here.\n */\nexport function revalidateFormOnChange(\n form: Form,\n path: Path,\n mode: ValidationMode\n): void {\n if (!form.validateDeps?.has(path.key) || !form.validate) return;\n if (\n mode === 'onChange' ||\n mode === 'all' ||\n (mode === 'onTouched' && hasTouchedByPath(form, path)) ||\n (form.reValidateMode === 'onChange' && hasFormValidateErrors(form))\n ) {\n runFormValidate(form).catch(() => {});\n }\n}\n\n/** Per-form registry of field-level `validateDeps` declarations ({@link\n * revalidateDependentsOnChange}): dep path key -> every dependent field key\n * that listed it. Held in a WeakMap so the Form shape is untouched for\n * forms whose fields never declare deps. */\nconst fieldValidateDeps = new WeakMap<Form, Map<string, Set<string>>>();\n\n/** Register one field's validateDeps declaration: `key` re-validates when\n * any path in `depKeys` takes a user change. Idempotent per (key, dep)\n * pair, so StrictMode's double effect is harmless. */\nexport function registerFieldValidateDeps(\n form: Form,\n key: string,\n depKeys: string[]\n): void {\n let deps = fieldValidateDeps.get(form);\n if (!deps) {\n deps = new Map();\n fieldValidateDeps.set(form, deps);\n }\n for (const depKey of depKeys) {\n let dependents = deps.get(depKey);\n if (!dependents) {\n dependents = new Set();\n deps.set(depKey, dependents);\n }\n dependents.add(key);\n }\n}\n\n/** Drop one field's validateDeps registration ({@link\n * registerFieldValidateDeps}). Entries nobody lists anymore are removed so\n * the registry never outlives its fields. */\nexport function unregisterFieldValidateDeps(\n form: Form,\n key: string,\n depKeys: string[]\n): void {\n const deps = fieldValidateDeps.get(form);\n if (!deps) return;\n for (const depKey of depKeys) {\n const dependents = deps.get(depKey);\n if (!dependents?.delete(key)) continue;\n if (!dependents.size) deps.delete(depKey);\n }\n}\n\n/**\n * Field-level twin of {@link revalidateFormOnChange}: after a user change\n * to `path`, re-run every field validator that declared `path` in its\n * `validateDeps` (useField option). Same channel, same gate: the kick\n * rides the changed field's own onChange pipeline (typing and\n * `changeValue` alike), so programmatic `setValue` writes never fire it —\n * exactly like field validators and the form-level `validateDeps`.\n *\n * The gate mirrors the form-level matrix with the *changed field's*\n * effective `mode` and the form-level `reValidateMode` against each\n * dependent's live error:\n * - `mode` `'onChange'`/`'all'` — every dep change re-runs the dependent;\n * - `mode` `'onTouched'` — once the changed field was touched;\n * - otherwise the re-run waits for `reValidateMode: 'onChange'` (the\n * default) while the dependent still shows an error — the\n * submit-then-fix flow: the mismatch lands on submit, editing the\n * dependency re-validates the dependent and a passing round clears it\n * (a field validator owns its whole key, so the re-run's result\n * replaces whatever the previous round wrote — the field-level shape\n * of the form-level footprint reclaim).\n *\n * The kick is an ordinary validator kick: the dependent's own\n * `validateDebounce` window applies, and a synchronous throw inside its\n * validate propagates to the caller like any field validator's would.\n *\n * A no-op unless some field declared `path` as a dep — forms without any\n * field-level `validateDeps` pay one property check here.\n */\nexport function revalidateDependentsOnChange(\n form: Form,\n path: Path,\n mode: ValidationMode\n): void {\n const dependents = fieldValidateDeps.get(form)?.get(path.key);\n if (!dependents?.size) return;\n for (const dependent of dependents) {\n // A self-dep changes nothing: the field's own onChange above already\n // validated it under the same gate.\n if (dependent === path.key) continue;\n if (\n mode === 'onChange' ||\n mode === 'all' ||\n (mode === 'onTouched' && hasTouchedByPath(form, path)) ||\n (form.reValidateMode === 'onChange' && form.errors.has(dependent))\n ) {\n form.validators.get(dependent)?.();\n }\n }\n}\n\n/** The Error {@link ensureValidate} rejects with: `message` is the first\n * error's display text ({@link getFirstError}) — the long-standing shape\n * — and `.errors` carries the complete flattened error list ({@link\n * getErrors}: `{path, type, message}` entries, dotted display paths) so\n * catchers can branch on types and locate fields without re-reading the\n * form. */\nexport type FormValidationError = Error & {errors: FieldErrorEntry[]};\n\n/** Build {@link ensureValidate}'s rejection: first error's message, every\n * error attached. */\nfunction validationError(form: Form): FormValidationError {\n const error = new Error(getFirstError(form)) as FormValidationError;\n error.errors = getErrors(form);\n return error;\n}\n\n/**\n * Validate and throw if any field error.\n * @param form\n * @return resolve if no error; reject and stop validate if has an error\n */\nexport async function ensureValidate(form: Form): Promise<void> {\n form.validators.forEach(validator => validator());\n\n await waitUntil(\n form.emitter,\n 'validating',\n () => fieldsSettled(form),\n () => hasErrors(form)\n ).catch(() => {\n throw validationError(form);\n });\n\n if (form.validate) {\n await runFormValidate(form);\n if (hasErrors(form)) throw validationError(form);\n }\n}\n\n/**\n * Validate and return if any field error.\n * @param form\n * @return error message string or void\n */\nexport async function validate(form: Form): Promise<void | string> {\n return ensureValidate(form).catch(e => e.message);\n}\n"],"names":["setFormErrors","form","result","segments","footprint","Object","entries","forEach","key","value","path","isIndex","normalizePath","setError","recordFootprint","Array","isArray","isFieldError","createPath","stored","errors","get","set","applyValidateResult","validateDeps","formErrorFootprints","Map","getFormErrorFootprint","written","delete","emit","emitter","JSON","parse","clearFormValidateErrors","clear","VALIDATION_OUTCOME","outcome","setParsedValues","values","WeakMap","FORM_VALIDATING_KEY","SETTLED","formValidateStates","settleFormValidate","state","round","timer","marked","validating","waiters","waiter","resolve","reject","async","name","options","settle","keys","waitUntil","fieldsSettled","validators","validator","validate","Promise","debounce","validateDebounce","controller","AbortController","getValues","signal","then","getFormValidateState","clearTimeout","add","setTimeout","abort","error","runFormValidateRound","push","runFormValidate","hasErrors"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import{emit as e}from"@for-fun/event-emitter";import{v as t,j as s,k as i,l as a,m as n,c as r,o as l,u as o,p as u,q as c}from"./errors-CzWtwjO0.mjs";function f(e){let s=t.get(e);return s?s.version>0&&(s.result=d(e),s.version=0):(s={version:0,result:d(e)},t.set(e,s)),s.result}function d(e){const{initialValues:t,parsedValues:s,values:i,deleted:a}=e,n=new Set;let r=s??t;for(const[e,t]of i)r=l(r,JSON.parse(e),t,n);for(const e of a)r=o(r,JSON.parse(e));return r}function m(t,s,i,r){const{emitter:l,values:o,deleted:c}=t;o.set(s.key,i),function(e,{key:t}){if(!e.size)return;const s=`${t.slice(0,-1)},`;for(const t of e.keys())t.startsWith(s)&&e.delete(t)}(o,s),function(e,{key:t}){if(!e.size)return;for(const s of e)(s===t||s.startsWith(`${t.slice(0,-1)},`)||t.startsWith(`${s.slice(0,-1)},`))&&e.delete(s)}(c,s),u(t,s),a(t),n(t),e(l,"change",s)}function v(t,s){t.initialValues===s||c(t.initialValues,s)||(t.initialValues=s,t.parsedValues=void 0,t.values.clear(),t.deleted.clear(),i(t),a(t),n(t),e(t.emitter,"change"))}function g(t,l,o){const u=[];t.initialValues=l??t.initialValues,t.parsedValues=void 0,s(t);const{emitter:c,touched:f,values:d,deleted:v,validating:g}=t;d.clear(),v.clear(),i(t),f.clear(),g.clear(),t.isSubmitting=!1,t.submitCount=0,t.isSubmitSuccessful=void 0,a(t),n(t);for(const{segments:e,value:s}of u)m(t,r(e),s);e(c,"change"),e(c,"touched"),e(c,"validating"),e(c,"submitting"),e(c,"submitCount"),e(c,"submitSuccessful"),e(c,"reset")}export{f as g,g as r,v as s};
2
+ //# sourceMappingURL=values-B1IV-6V4.mjs.map