v0 · Developer Preview Frond is under active development. APIs may change between releases.

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 };
StateExposed to ordinary reads
CurrentYes
StaleYes
ExpiredNo

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.

PolicyBehavior
StaticEach successful acquire, refresh, or action commits Current. The default.
ManualThe runtime does not time-age the result. The driver sets validity.
TimeBoundThe 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

Validityrefresh()
CurrentRuns.
StaleRuns.
ExpiredReturns 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.