Result validity
Result validity is whether a node’s committed result may be exposed to ordinary reads. A ready node carries a result and a validity alongside it. Validity attaches to the result, not to identity, topology, or readiness.
States
type ResultValidity =
| { readonly _tag: "Current"; readonly currentAt?: number }
| { readonly _tag: "Stale"; readonly staleAt: number }
| { readonly _tag: "Expired"; readonly expiredAt: number };
| State | Exposed to ordinary reads |
|---|---|
Current | Yes |
Stale | Yes |
Expired | No |
Stale data still reads as Ready. It is displayable; it is just past its fresh window. Expired data is hidden from ordinary reads and reloads through readiness.
Policies
A driver declares how validity is decided with the resultValidity field. Omitted means Static.
| Policy | Behavior |
|---|---|
Static | Each successful acquire, refresh, or action commits Current. The default. |
Manual | The runtime does not time-age the result. The driver sets validity. |
TimeBound | The runtime derives Current, Stale, or Expired from the loaded time and the clock. |
driver: Frond.Driver.Async<ProfileSpec>({
resultValidity: {
_tag: "TimeBound",
staleAfter: "30 seconds",
expireAfter: "5 minutes",
},
acquire: Frond.Driver.Acquire((ctx) => fetchProfile(ctx.args.userId)),
}),
With TimeBound, the runtime ages the result on its own — no driver code runs to mark it stale or expired.
Setting validity manually
Under Manual (or any policy where the driver knows better than the clock), commit validity with the result using Frond.resultCommit, or set it from the driver context.
return Frond.resultCommit(prices, {
validity: { _tag: "Current", currentAt: Date.now() },
});
// inside an action or refresh
ctx.setResultValidity({ _tag: "Stale", staleAt: Date.now() });
See Drivers for the full driver context.
Expired reloads through readiness
An expired result is not a failure and not an eviction. Ordinary reads hide it instead of exposing it as Ready data. Active readiness paths then reload the node.
When readiness runs again, it runs dependency readiness and acquire again, and commits the new result on success. The graph record, id, key, args, dependency edges, and live leases are preserved — only ordinary result exposure is cleared while the expired result is not displayable.
In React, useNode does not return expired data. It schedules readiness and throws a Suspense promise, so the nearest boundary shows its fallback while the node reloads.
Refresh and validity
| Validity | refresh() |
|---|---|
Current | Runs. |
Stale | Runs. |
Expired | Returns a Failure result with ResultExpired. |
A stale refresh that fails keeps the stale result visible and records an operation failure. Expired data does not recover through refresh — it recovers through readiness. See Lifecycle.
Dependencies do not cascade
Each node owns its own validity. A dependency going Stale does not invalidate the nodes that depend on it. A node acquiring against an expired dependency fails through a typed dependency failure rather than silently reading expired data.
Reading validity
handle.read() exposes displayable data as Ready. Current and Stale results are displayable; Expired is hidden from ordinary reads and recovered through readiness. In React, useNodeState exposes displayable validity directly:
const state = FrondReact.useNodeState(ProfileNode, { userId });
state.resultValidity; // { _tag: "Current" | "Stale" }
Use it to render “loaded a while ago, refreshing” affordances over stale data. Raw diagnostic surfaces can still show expired validity when you need to inspect why a node re-entered readiness.
Next: Liveness — how the runtime keeps a node’s live resource running while it is observed.