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

Liveness

Liveness is demand for a node’s driver live work — a running subscription, socket, or poll that pushes updates into the result. A node is live when something is observing it in a way that needs that work running. Liveness is separate from readiness: a node can be ready without being live.

Liveness is not React mount presence, not runtime retention, not event subscription, and not snapshot inspection. Only two things create it.

Sources

SourceCreated by
mobxObserving a MobX node result in a reactive context.
manualA live lease acquired through the runtime client.

When demand exists, the runtime runs the driver’s live resource. When the last demand goes away, it stops the resource.

Manual leases

Acquire a lease to hold a node live without observing its result — useful outside React, or when you need live work independent of rendering.

const lease = await runtime.client
  .node(WeatherNode, Frond.Args.none)
  .acquireLiveLease("manual", { cities: ["Lisbon"] });

// later
await lease.dispose();

The scope ({ cities: ["Lisbon"] }) must be a JSON-shaped value. A lease records demand only — it does not acquire, refresh, release, or evict the node, and does not imply readiness. Dispose it to drop the demand.

MobX observation

Reading a node’s result inside a MobX reaction creates mobx demand automatically. Because Frond’s node model is MobX-backed, an observer component that reads a live result registers observation, and the runtime starts the driver’s live resource while that observation is active.

This is why React components that read node fields are wrapped in observer — see Wire to React. A component that mounts but never reads a live field creates no liveness; the observation is what registers demand, not the mount.

A live node

A driver delivers live work with Frond.Driver.Live. It is optional: a node with no live resource still tracks demand, but there is nothing to start. acquire seeds the result; the live resource runs only while there is demand and pushes updates into that result.

import * as Frond from "@frondruntime/core";
import { observable, runInAction } from "mobx";

type WeatherResult = { temperature: number };
type WeatherSpec = Frond.NodeSpec<{
  args: Frond.Args.None;
  key: Frond.Key.Singleton;
  result: WeatherResult;
}>;

// stand-in for a websocket/SSE subscription
type WeatherFeed = { close: () => void };
declare function openWeatherFeed(onTemperature: (temperature: number) => void): WeatherFeed;

export class WeatherNode extends Frond.NodeBase<WeatherSpec> {
  static readonly spec = Frond.resourceSpec<WeatherSpec>({
    tag: Frond.tag("weather/current"),
    key: () => Frond.Key.singleton(),
    driver: Frond.Driver.Async<WeatherSpec>({
      acquire: Frond.Driver.Acquire(() => observable({ temperature: 18 })),
      live: Frond.Driver.Live<Frond.NodeBase<WeatherSpec>, WeatherFeed>({
        start: (ctx) =>
          openWeatherFeed((temperature) => {
            runInAction(() => {
              ctx.node.result.temperature = temperature;
            });
          }),
        stop: (_ctx, feed) => feed.close(),
      }),
    }),
  });
}

start opens the feed and returns it as the live resource; stop closes it. The feed writes into ctx.node.result, which is observable, so each push re-renders observers.

Read the node in an observer component:

import * as Frond from "@frondruntime/core";
import * as FrondReact from "@frondruntime/react";
import { observer } from "mobx-react-lite";

const CurrentWeather = observer(() => {
  const weather = FrondReact.useNode(WeatherNode, Frond.Args.none);
  return <b>{weather.result.temperature}</b>;
});

Reading weather.result.temperature inside the observer registers mobx observation. While <CurrentWeather /> is mounted and reading, the node has demand, so the runtime calls start and the feed pushes temperatures into the result. When the component unmounts, observation ends, demand drops, and the runtime calls stop. The runtime starts and stops the resource; the component writes no useEffect or subscription.

Demand changes

start and update receive the active demand snapshot. The runtime drives the resource as demand changes:

Demand changeEffect
Goes inactivestop the resource (DemandInactive).
Stays equivalentNo-op.
Changes, update providedCall update.
Changes, no updatestop (DemandChanged), then start a new resource.

Live cleanup belongs to stop, not to acquire/operation disposers. Release, eviction, graph stop, and expiry all stop the active resource through the same path.

React presence is not liveness

A mounted React component does not, by itself, create live demand. To run live work in React, either read a MobX result that reports observation, or acquire a manual lease through the runtime client. Reads and subscriptions are presentation; liveness is driver demand.


Next: Spec and class — how to author a node.