feat(jarl): 685 — requireMatch / useRequiredRoute, a checked assertion for a route read known to match - #100
randomdevpete wants to merge 3 commits into
Conversation
726079a to
bdcce22
Compare
7cd13bb to
793a3b5
Compare
bdcce22 to
0a639b7
Compare
793a3b5 to
4777b37
Compare
4777b37 to
1f76d60
Compare
… is already guaranteed Ticket: 685
… MatchedRoute Ticket: 685
…sted for the type checker Ticket: 685
1f76d60 to
665022a
Compare
|
Held for a closer look, and taken off the critical path — nothing is blocked behind it now. Three things to settle before this lands. 1. The stale-read trade. 2. The comments break the project's rules in several places. Root cause is not this branch: 3. Rebased onto current master along the way, so this is reviewable as it stands. |
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.