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

Drivers

The driver is the side-effect implementation behind a node. It loads the result, refreshes it, releases resources, and runs actions. Every node spec has exactly one driver.

driver: Frond.Driver.Async<ProfileSpec>({
  acquire: Frond.Driver.Acquire((ctx) => fetchProfile(ctx.args.userId)),
  refresh: Frond.Driver.Refresh(async (ctx) => {
    ctx.setResult(await fetchProfile(ctx.args.userId));
  }),
  release: Frond.Driver.Release((ctx) => ctx.node.result.close()),
}),

Async or Effect

Two flavors, same hook set:

ConstructorHooks returnUse when
Frond.Driver.Asynca value or a PromiseYour code is async/await or promise-based.
Frond.Driver.Effectan EffectYour code is written in Effect.

The hooks are the same either way:

HookRequiredPurpose
acquireYesProduce the initial result.
refreshNoRe-load a ready node’s result.
releaseNoClean up resources.
liveNoRun live work while observed. See Liveness.
actionsNoMediated methods on the node. See Actions.
resultValidityNoValidity policy. See Result validity.

acquire

The only required hook. It produces the result the node becomes ready with. Return the result directly, a Promise of it, or a Frond.resultCommit(...) to attach metadata.

acquire: Frond.Driver.Acquire((ctx) => fetchProfile(ctx.args.userId)),

acquire reads ctx.args and ctx.deps. It does not get ctx.node — the node instance does not exist until acquire returns the result it is built from. If acquire throws or rejects, the node enters readiness error. See Lifecycle.

refresh

Re-loads a ready node in the background while the current result stays visible. Commit the new result with ctx.setResult.

refresh: Frond.Driver.Refresh(async (ctx) => {
  ctx.setResult(await fetchProfile(ctx.args.userId));
}),

Refresh runs only on a node with displayable ready data, and rolls back to the previous result on failure. A node with no refresh hook rejects refresh requests.

release

Runs when the runtime releases the node’s resources. Use it to close what acquire opened — connections, timers, subscriptions.

release: Frond.Driver.Release((ctx) => ctx.node.result.close()),

Cleanup registered with ctx.disposers.add(fn) during acquire runs on release too; the release hook is for teardown the disposers do not cover. Release is best-effort: a throwing release is recorded but does not block the rest of cleanup. See Eviction and release.

The context

Each hook receives a ctx. The fields available depend on the hook.

Fieldacquirerefresh / actionreleaseDescription
argsThe node’s args.
depsResolved dependency instances.
nodeThe node instance.
signalAbortSignal for this work.
disposersRegister cleanup with disposers.add(fn).
signalsRuntime signal access.
setResultCommit a new result.
setResultValiditySet result validity.
patchResultMutate the current result in place.
refreshDepRe-acquire a single dependency.

Effect drivers add ctx.tryPromise((signal) => run(signal)) to bridge a promise into Effect with the abort signal wired.

AbortSignal

ctx.signal aborts when the work is cancelled — a newer attempt supersedes this one, or the node is released or evicted. Pass it to anything cancellable:

acquire: Frond.Driver.Acquire((ctx) =>
  fetch(`/api/profile/${ctx.args.userId}`, { signal: ctx.signal }).then((r) => r.json())
),

In Effect drivers, ctx.tryPromise wires the signal for you:

acquire: Frond.Driver.Acquire((ctx) =>
  ctx.tryPromise((signal) => fetch(`/api/profile/${ctx.args.userId}`, { signal }))
),

For cleanup that is not tied to a single fetch, register it with ctx.disposers.add(fn) and the runtime runs it on release.


Next: Args and deps — typing a node’s input and declaring what it depends on.