Runtime and graph
The runtime is a single object that owns a graph of nodes. All loads, refreshes, and evictions happen through it. Create it once at the top of your app.
Create the runtime
import * as Frond from "@frondruntime/core";
export const runtime = Frond.createRuntime();
createRuntime(options?) returns a Runtime. It starts empty — no nodes, no drivers, no fetches. Nodes are created lazily, the first time something asks for one.
Use one runtime per app. The client and the React provider both read through it.
The graph
The graph is the set of nodes currently alive, plus the dependency edges between them. A node is one slot with an identity, a result once it loads, and a lifecycle the runtime drives. Nodes model your domain objects — a user profile, a websocket connection, a feature-flag client.
Nodes are lazy:
- Ask for a node and it joins the graph.
- Evict it and it is removed, along with its dependents.
The graph holds only what has been asked for and not yet evicted.
Identity
Every node has a graph node id of the form tag:key:
- tag — names the kind of node (
resources/profile), set on the spec. - key — derived from args by the spec’s
key(args)function.
Same tag and key produce the same id, which resolves to the same node.
runtime.client.node(ProfileNode, { userId: "u_42" }).nodeId;
// -> "resources/profile:v1:{\"userId\":\"u_42\"}"
Requesting u_42 from several places resolves to one node — one fetch, one instance. A dependency edge points at an id, so a node depending on the session and a component reading the session resolve to the same node. See Identity and keys.
Read the graph
Outside React
runtime.client.node(spec, args) returns a handle. Creating the handle resolves the id but does not acquire the node.
const handle = runtime.client.node(ProfileNode, { userId: "u_42" });
await handle.ensureReady(); // acquire and wait until ready
handle.read(); // current state, no acquire
Handle methods:
| Method | Description |
|---|---|
ensureReady() | Acquire and resolve when ready; reject on readiness error. |
ensure() | Wire the node into the graph without forcing acquire. |
read() | Read current state synchronously. |
refresh() | Re-acquire while keeping current data visible. |
runAction(action, input) | Run a declared node action. |
updateArgs(nextArgs) | Reconcile args (same id only). |
releaseResources(reason) | Tear down resources, keep the wiring. |
evict(mode?, reason?) | Remove from the graph. Default mode "selfAndDependents". |
Inside React
Wrap the tree in FrondProvider, then read nodes with hooks.
import * as FrondReact from "@frondruntime/react";
<FrondReact.FrondProvider runtime={runtime}>
<App />
</FrondReact.FrondProvider>;
Nodes are MobX-backed, so a component that reads reactive node fields is wrapped in observer:
import * as FrondReact from "@frondruntime/react";
import { observer } from "mobx-react-lite";
const Profile = observer(({ userId }: { userId: string }) => {
const profile = FrondReact.useNode(ProfileNode, { userId });
return <h1>{profile.displayName}</h1>;
});
A handle from runtime.client and a useNode call, both for u_42, resolve to the same id and read the same node. See Wire to React.
One owner
The runtime owns every lifecycle fact: readiness, in-flight attempts, refreshes, cancellation, eviction, and liveness. Callers — React, MobX, devtools, tests, scripts — go through it rather than mutating node state directly.
Each node serializes its work through the runtime. Late completion of a superseded attempt does not overwrite newer state, and concurrent operations on one node run in order rather than interleaving. This holds for every caller, so it does not depend on the UI disabling a control.
Next: Identity and keys — how the key half of the id is built. Then the lifecycle a node moves through once it has an id.