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:
| Constructor | Hooks return | Use when |
|---|---|---|
Frond.Driver.Async | a value or a Promise | Your code is async/await or promise-based. |
Frond.Driver.Effect | an Effect | Your code is written in Effect. |
The hooks are the same either way:
| Hook | Required | Purpose |
|---|---|---|
acquire | Yes | Produce the initial result. |
refresh | No | Re-load a ready node’s result. |
release | No | Clean up resources. |
live | No | Run live work while observed. See Liveness. |
actions | No | Mediated methods on the node. See Actions. |
resultValidity | No | Validity 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.
| Field | acquire | refresh / action | release | Description |
|---|---|---|---|---|
args | ✓ | ✓ | The node’s args. | |
deps | ✓ | ✓ | Resolved dependency instances. | |
node | ✓ | ✓ | The node instance. | |
signal | ✓ | ✓ | ✓ | AbortSignal for this work. |
disposers | ✓ | ✓ | ✓ | Register cleanup with disposers.add(fn). |
signals | ✓ | ✓ | Runtime signal access. | |
setResult | ✓ | ✓ | Commit a new result. | |
setResultValidity | ✓ | ✓ | Set result validity. | |
patchResult | ✓ | ✓ | Mutate the current result in place. | |
refreshDep | ✓ | Re-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.