Gate rendering on identity, and report whether a command form is executing - #2531
Gate rendering on identity, and report whether a command form is executing#2531woksin wants to merge 5 commits into
Conversation
An application gating routes on roles had to hand-roll the guard, and every hand-rolled version inherits the same flaw: isSet reads false both before the first identity fetch answers and after it answers anonymous. Nothing could tell those apart, so a guard either renders nothing forever for a genuinely anonymous caller, or flashes the signed-out view at a signed-in one. Add isLoading to the React identity context to separate the two, and a RequireRole component that renders one of three outcomes from it. Access is expressed as roles, a predicate over details, or both - both must pass when both are given. The forbidden and loading outcomes are slots rather than a redirect, because Arc.React does not depend on a router; an application passes its own navigation element. isLoading lives on IIdentityContext rather than IIdentity so the framework-agnostic package keeps no React lifecycle concern. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: 49eab3a7-0743-4895-8303-bf5632b6a992
Two independent reviews of the previous commit found the loading flag was only ever seeded, never re-raised. Nothing re-entered loading after mount, so during a refresh after sign-in the context read "not loading, not signed in" for the whole round-trip and a gate rendered its forbidden slot - navigating away, for an application that put a redirect there. The flash the flag exists to prevent was moved from first paint to refresh rather than removed. Raise it before every identity request, and lower it again on the paths that end one, including a refresh that rejects. RequireRole now denies instead of admitting when it is given no access rule at all. A renamed configuration key yields undefined, which read as "no roles required" and rendered guarded content to any authenticated caller; the props now require roles or a predicate at the type level, and deny with a warning at runtime, because undefined defeats the type. A roles that is not an array, an identity carrying no details, and a predicate that throws all deny for the same reason: a gate that cannot evaluate its rule has not decided yes. isLoading is optional where the provider value reaches consumers and required where the identity is read. The value shape travels out through the exported context, so requiring it there would stop a hand-built test or Storybook context from compiling, while a consumer reading the identity should never have to handle its absence. Absent reads as resolved - a hand-built context makes a gate deny rather than admit. Specs for refresh, for clearing the identity, and for a fetch that fails: all three were free variables that no assertion could observe. Three existing refresh specs turned out to exercise nothing - one had its refresh call commented out while keeping the name - and are repaired here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: 49eab3a7-0743-4895-8303-bf5632b6a992
The guard that stops a predicate from deciding on details it does not have tested for undefined only. A server answering with an explicit null for details produces null, not undefined, so the null reached the predicate - and a predicate phrased as an absence reads null as innocence and admits. An identity the application could say nothing about therefore passed a rule meant to inspect it. Treat both shapes as nothing to decide on. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: 49eab3a7-0743-4895-8303-bf5632b6a992
An application driving a command form had no way to know the command was in flight, so every caller that wanted to disable a button, show a spinner, or stop a second submit kept its own flag beside the form and set it around its own execute call. The form already knows; it just never said. Derive it once where the command is executed and publish it three ways: on the form context, to a children render function, and through a handle a caller can hold. A caller reads whichever one it is already positioned to read rather than tracking the state a second time. The handle is a named formRef prop rather than a forwarded ref. forwardRef erases generic parameters, and consumers write CommandForm with explicit type arguments, so forwarding it would cost them their typing. Clearing happens in a finally. Command.performRequest catches transport failures and answers with a failed result, so a broken network never rejects - but a command whose validator throws, whose payload cannot be built, or that overrides execute still can, and a form left reporting an execution that has already ended can never be submitted again. isExecuting is optional on the form context because that shape is exported and built by hand - specs and wrappers compose one to render a field in isolation - and a required member would stop every one of them compiling. Read it through useIsCommandExecuting, which answers false for a context that predates it. This reports on execution the form itself performs. A caller that takes the command instance and executes it directly, as the dialog wrappers do, is not executing through the form and the form does not claim to know about it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: 49eab3a7-0743-4895-8303-bf5632b6a992
…ty-loading-and-command-execution-state
|
Reviewer context, kept out of the body. Semver. Minor, and deliberately so. Access control. Verification. 208 spec files / 505 tests, Mutation evidence, each applied and restored with the restore verified:
The last two killing exactly one channel each is what shows the three publications are independently observed rather than one covering for the others. Repaired along the way. Three existing Known limitation, not fixed here. |
|
Moved to draft. Independent review returned do-not-ship with three confirmed blockers, two of them regressions this branch introduces. Recording them here rather than quietly fixing, because the first and third are consequences of the render-prop design rather than slips in it. The render prop reverts the user's input on submit, when the form also uses A render prop returning a fragment silently unbinds every field. Overlapping submits report idle while a command is still running. The Also confirmed: The Four mutations survived out of eighteen. Three of them — the Not merging this. The identity work in |
|
Closing this without merging. The identity half shipped on its own in #2536; the command-form execution state needs redesign rather than patching, so it is not worth keeping a stale branch open in front of it. What #2536 took: What remains open, with the evidence, is
Also found: The shape that avoids all of it, for whoever picks up #2497: publish the state through the form context and an imperative handle only, leave |
Added
RequireRolecomponent that renders its children only for an identity that is authenticated and allowed, with slots for the loading and forbidden outcomes so an application supplies its own navigation (Arc React has no declarative role gate #2496)isLoadingon the React identity context, separating an identity that has not been fetched yet from one that came back anonymous (Arc React has no declarative role gate #2496)CommandForm, published to the form context, to achildrenrender function, and through aformRefhandle (CommandForm exposes neither execution state nor parent-reachable form state #2497)useIsCommandExecutingfor reading whether the surrounding form's command is executing (CommandForm exposes neither execution state nor parent-reachable form state #2497)