feat: async Validator support with stale-result protection (bd 9jk) - #71
Open
timkindberg wants to merge 10 commits into
Open
feat: async Validator support with stale-result protection (bd 9jk)#71timkindberg wants to merge 10 commits into
timkindberg wants to merge 10 commits into
Conversation
timkindberg
added a commit
that referenced
this pull request
Jul 15, 2026
Roadmap bookkeeping surfaced while implementing 9jk (PR #71): async docs + example (5ss.8) and the RHF form-state-slot bridge recipe (5ss.9), both under the V1 DX epic 5ss and discovered-from 9jk. Also records the 9jk implementation-progress comment. Co-authored-by: Cursor <cursoragent@cursor.com>
timkindberg
added a commit
that referenced
this pull request
Jul 16, 2026
) From the three-lens library review (history/2026-07-15-…-tribe-review.md): Soundness - Single async detector: export `isThenable` from Core; use it in both `fromStandardSchema` (was `instanceof Promise`, missed cross-realm/library thenables → false valid verdict) and the React store. - `ValidationResult<T>` is now a discriminated union on `valid` (`data?: never` on the invalid arm); AJV + the fake contract validator branch on the verdict. The submit-time `as Output` is retained as a documented assertion boundary. - `statusStore` dec* now floors at zero so a double-settle can't wedge a pending boolean negative. DX / coverage - Add `formatValidationFailure(unknown): string|null` beside `useValidationFailure`; use it in example 16. - Add a React-level test proving a rejecting validator surfaces via the hook. Docs - README leads with the batteries-included bound `SchemaFields`; documents the superseded-submit-still-fires-`onValid` hazard (+ disable-while-pending guard) and the swallowed `onValid` rejection; adds a migration note for the removed `validation` return. Fix stale `Validator` JSDoc + `displayPolicy` comment. ADRs - ADR 025 addendum: discriminated result + output-assertion boundary. - ADR 046 post-implementation review addendum: fixes applied + deferred items. Gate green (typecheck + lint + test). Co-authored-by: Cursor <cursoragent@cursor.com>
…9jk) Introduce AsyncValidator as a sibling of Validator (ADR 041) rather than widening the sync seam: a single validation slot accepts either and branches on the returned value's Promise-shape, so synchronous callers are untouched. - core: AsyncValidator type + re-exports; toStandardSchemaAsync / fromStandardSchemaAsync adapters, with shared result-mapping helpers. - validation-contract: the contract suite awaits uniformly so sync and async validators run identical purity/error-mapping assertions. - validation-zod: createZodAsyncValidator via safeParseAsync, sharing the Zod->ValidationResult mapping with the sync path. Co-authored-by: Cursor <cursoragent@cursor.com>
… async orchestration (bd 9jk) Move all form + validation state out of useFormTree's React state into a framework-neutral form store (ADR 044), which also owns the async validation orchestration (ADR 042-046). useFormTree becomes a thin binding. Store (zero React imports; the reuse seam ADR 008 will earn): - statusStore: ref-counted isValidating/isSubmitting + a failure surface, notifying only on the 0<->1 edges. - formStore: generation authority (supersede-on-start staleness), retained errors, the dual-natured submit (errors gated by authority; onValid ungated on its own click-time verdict; isSubmitting spans an async onValid), and the sync/async branch on result Promise-shape. Validator is swappable in place. React binding: - useFormTree instantiates one store, syncs the validator via an effect, and returns a bound SchemaFields that auto-provides the store (no manual ValidationProvider). Dropped the `validation` return object. - showErrorsWhen is now a hook option AND a reactive SchemaFields prop. - New fan-out-free selector hooks: useIsValidating / useIsSubmitting / useValidationFailure; exported FormStoreProvider for advanced composition. Tests/examples: 19 store-level orchestration tests + 3 React async E2E tests; 7 React test files and 4 example apps migrated to the new API. Full gate green. Co-authored-by: Cursor <cursoragent@cursor.com>
Roadmap bookkeeping surfaced while implementing 9jk (PR #71): async docs + example (5ss.8) and the RHF form-state-slot bridge recipe (5ss.9), both under the V1 DX epic 5ss and discovered-from 9jk. Also records the 9jk implementation-progress comment. Co-authored-by: Cursor <cursoragent@cursor.com>
…rnesses The render-count harnesses render a custom counting adapter under FormStoreProvider (not the bound SchemaFields), so the display policy must be set on the provider, not the useFormTree option. The validation harness needs 'always' for the errored field to render; passing it to the hook option left the provider at the 'touched' default, so the untouched field never showed its error and the render-count assertion failed. Move the policy to the provider. Co-authored-by: Cursor <cursoragent@cursor.com>
Add examples/basic-react example 16 demonstrating the async validation feature from 9jk end-to-end: an async validator (createZodAsyncValidator over a remote uniqueness rule), pending signals (useIsValidating / useIsSubmitting), run-failure surface (useValidationFailure) distinct from an invalid verdict, stale-result protection, and an awaited onValid. Rewrite the README validation section onto the store-owned API (FormStoreProvider + store, dropping the removed `validation` return) and add an async subsection. Ratifies the public names chosen in 9jk (useIsValidating/useIsSubmitting/useValidationFailure/FormStoreProvider). Widen the async React test's in-flight delay so the transient pending window is reliably observable (was racing Playwright click overhead). Closes 5ss.8. Co-authored-by: Cursor <cursoragent@cursor.com>
The async uniqueness rule was an object-level Zod refine, which only runs after every field passes — so the username check didn't fire until the email was also valid. Scope it to the username field (baseSchema.shape) so the remote check runs as soon as the username itself is valid, and drop the redundant onChange so it's one check per field visit (on blur). Also file jsonschema-form-62a: ValidationSummary lists errors in validator order, not field/DOM order. Co-authored-by: Cursor <cursoragent@cursor.com>
Example 16: add a value-cache on the simulated remote check and a skip-when-unchanged blur guard, so tabbing through unedited fields no longer re-fires the async check (the library runs the whole validator per trigger and any async rule makes the whole schema async — dedupe/debounce are consumer-owned, ADR 021). Correct the field-level refine comment to state the real cause: an invalid_type on any field aborts object-level refines, and submit omits empty inputs so a blank field is undefined. README: add a "costs and caveats" note to the async section covering the whole-validator-per-trigger cost and the field- vs object-level .refine gotcha. Co-authored-by: Cursor <cursoragent@cursor.com>
) From the three-lens library review (history/2026-07-15-…-tribe-review.md): Soundness - Single async detector: export `isThenable` from Core; use it in both `fromStandardSchema` (was `instanceof Promise`, missed cross-realm/library thenables → false valid verdict) and the React store. - `ValidationResult<T>` is now a discriminated union on `valid` (`data?: never` on the invalid arm); AJV + the fake contract validator branch on the verdict. The submit-time `as Output` is retained as a documented assertion boundary. - `statusStore` dec* now floors at zero so a double-settle can't wedge a pending boolean negative. DX / coverage - Add `formatValidationFailure(unknown): string|null` beside `useValidationFailure`; use it in example 16. - Add a React-level test proving a rejecting validator surfaces via the hook. Docs - README leads with the batteries-included bound `SchemaFields`; documents the superseded-submit-still-fires-`onValid` hazard (+ disable-while-pending guard) and the swallowed `onValid` rejection; adds a migration note for the removed `validation` return. Fix stale `Validator` JSDoc + `displayPolicy` comment. ADRs - ADR 025 addendum: discriminated result + output-assertion boundary. - ADR 046 post-implementation review addendum: fixes applied + deferred items. Gate green (typecheck + lint + test). Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
The async-validation branch replaced ValidationProvider + useFormTree's `errors` return with FormStoreProvider + `store`. Rebasing onto main pulled in the renderNodeRules customize examples (16/17), which still used the old API; update them so the combined tree typechecks. Co-authored-by: Cursor <cursoragent@cursor.com>
timkindberg
force-pushed
the
jsonschema-form-9jk-async-validator
branch
from
July 16, 2026 03:06
f7fb030 to
9beae06
Compare
This was referenced Jul 17, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements bd 9jk — async validation with stale-result protection — in two logical commits:
Core async seam —
AsyncValidatoras a sibling ofValidator(ADR 041), not a widening: one validation slot accepts either and branches on the returned value's Promise-shape, so sync callers are untouched. AddstoStandardSchemaAsync/fromStandardSchemaAsync, a uniformly-awaiting validator contract suite, andcreateZodAsyncValidator(viasafeParseAsync).Store-owned React state (ADR 042–046, per ADR 044) — moves all form + validation state out of
useFormTree's React state into a framework-neutral form store that also owns the async orchestration.useFormTreebecomes a thin binding.The store (zero React imports — the reuse seam ADR 008 will earn)
statusStore— ref-countedisValidating/isSubmitting+ afailuresurface, notifying only on the 0↔1 edges.formStore— generation authority (supersede-on-start staleness, no cancellation needed), retained errors (never blanked mid-flight), the dual-natured submit (errors gated by authority;onValidungated on its own click-time verdict;isSubmittingspans an asynconValid), the run-failure surface (thrown/rejected validator ≠ invalid verdict), and the sync/async branch on result shape.React binding changes (public API churn — pre-1.0, ADR-sanctioned)
useFormTreeowns one store, syncs the validator via an effect, and returns a boundSchemaFieldsthat auto-provides the store — no manual<ValidationProvider>for the common path.validationreturn object.showErrorsWhenmoved to auseFormTreeoption and a reactiveSchemaFieldsprop (toggle live, no input remount).useIsValidating/useIsSubmitting/useValidationFailure.FormStoreProviderfor advanced/custom composition (e.g. an RHF bridge sits beside it, not against it).Test plan
formStore.test.ts) covering the ADR 042–046 matrix: seam, authority/staleness, ref-counted pending, retained errors, dual-natured submit, failure surface.async-validation.test.tsx): pending signals, async success handler, live supersede.npm run gate(typecheck + lint + format + test) green across all packages.Reviewer notes
Made with Cursor