feat(jarl): 685 — requireMatch / useRequiredRoute, a checked assertion for a route read known to match - #100
Open
randomdevpete wants to merge 3 commits into
Conversation
… is already guaranteed Ticket: 685
… MatchedRoute Ticket: 685
…sted for the type checker Ticket: 685
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.
What
requireMatch(route, name?)injarl-atoms: narrows aRouteReturn<T>to the new exportedMatchedRoute<T>(Extract<RouteReturn<T>, { match: true }>), throwing"<name> does not match the current location"otherwise.useRequiredRoute(routeAtom, name?)injarl-react: the React form —useRoutefor a route atom whose match is guaranteed by where the component renders.isActivenow returns the sharedMatchedRoute<T>predicate type instead of hand-writing the sameExtract.DataGridApp.tsx'sdefaults/defaultFilterfallback — real code that existed only to satisfy the type checker — is gone from bothrowsAtomand the component, replaced byrequireMatch/useRequiredRoute.Why not a derived "this chain always matches" type
Full write-up in
packages/jarl-atoms/DESIGN-NOTES.md. Three reasons, in order of how hard they kill it:DataGridApproots oncreateRootAtom({ basePath: "/demos/data-grid" }), andstripBasePathreportsmatch: falseoutside that prefix — so a sound derivation has to call the chain partial. What actually guarantees the match is the<Route on={dataGridDemoRoute}>inApp.tsx: knowledge that lives above the atoms and can't be recovered from them.transformRouteAtomcan't report its own totality without changing howReturnis inferred for every existing caller.A manual
alwaysMatches: trueflag was rejected too, on soundness — it asserts what nothing checks, so a wrong guarantee surfaces asundefinedfield access far from the claim.requireMatchis the same caller-made assertion, but checked: right where it's wrong, it throws instead of silently going stale.The one-render-stale hazard
Without the Navigation API (Firefox/Safari today, and jsdom),
locationAtomfalls back to listening onpopstate, which stays silent for ahistory.pushStatemade outside jarl. A route read can therefore be one render stale after such a call. Where the oldRouteReturn.values ?? fallbackpattern just rendered stale-but-plausible data through that window,useRequiredRouteturns it into a thrown error mid-render.Verdict: acceptable as designed, not worth softening. Ticket 778 (this PR's base) switches
locationAtom's subscription to the Navigation API'scurrententrychangeevent wherever it's available, and that event fires for every same-document navigation regardless of what triggered it, including apushStatemade outside jarl — so on any browser with the Navigation API (Chrome, Edge) the window is closed entirely, not just narrowed. The exposure is real only on Firefox/Safari/jsdom, and even there it's bounded to exactly one render.Making
useRequiredRouteswallow one stale frame (return the previous match instead of throwing) was considered and rejected: it can't distinguish "stale because of an external pushState" from "stale because this component is genuinely rendering somewhere its route no longer matches," which is precisely the invariant this hook exists to check. Softening it here would silently defeat the same soundness argument that ruled outalwaysMatches: true. Documented instead — theuseRequiredRoutedocblock now states the hazard and the mitigation (navigate through jarl) directly.Notes
requireMatch,useRequiredRouteand the exportedMatchedRoute<T>type are pure additions;isActive's changed return-type annotation (MatchedRoute<T>in place of the hand-writtenExtract<...>) is the same structural type, not a behavioural or type change for callers. Twofeatcommits, minor bump, no!.task-778-no-navigation-blocking-api), itself on feat(atoms)!: 675 — one asyncRouteAtom API for async route data #98 (task-675-...) — base this PR againsttask-778-no-navigation-blocking-api, not the default branch.Closes ticket 685.