FRONTEND

Live vs settled

The engine holds the live value. Monad holds the last commit. Show both. They are not the same number.

While a partition is live, Monad still answers reads of those slots. It answers with the last committed value. The node answers with the live one. Both are correct. They are not the same number.

“Live” means the delegated slots. Everything else a view touches (another contract, an undelegated variable, a balance) is read at the block the node pinned when the session opened, not at Monad’s head. A view that mixes the two returns live delegated state next to pinned everything else.

app/state.ts
await interlude.read("currentScore");        // the node: live, ahead of the chain
await interlude.readSettled("currentScore"); // Monad: the last committed value

const status = await interlude.status();
status.pendingDiffs.length;
status.committedBatches;

Show both

A user who sees only the live number has to trust you that it will settle. A user who sees only the settled number thinks the last ten actions did not happen. The honest UI is the live value, with the settled one next to it, and a sense of how many diffs are still pending.

status() also names the app, the validator and the pinned base block. That is the fastest way to check the node is serving the app your frontend thinks it is, before you debug a grant that was signed for someone else.

Live and settled, without polling

The SDK already holds the loops you would write. watchRead re-reads a view each time the node applies a call, and once when it starts. Every send also returns settled, a promise that resolves once a batch committed on Monad carries that very call.

app/live.ts
// Re-read a view now, then each time the node applies a call. One WebSocket per client.
const stop = interlude.watchRead("currentScore", undefined, setScore);

// The live result comes back at once. settled resolves once a commit on Monad carries the call.
const { hash, settled } = await session.send("play");
const { batchIndex, settlementHash } = await settled; // the batch, and the Monad tx that carried it

// Kept only the hash? The same wait:
await interlude.waitSettled({ hash });

stop();

interlude.watch(onCall) hears every call the node applies (app, calldata, return, logs) for any contract; watchRead is built on it, and every watcher on a client shares one socket. In React the same thing is useWatch(fn, args) (React). settled is lazy: it polls only once you await it. It rejects with SettlementLostError when the node no longer knows the call (a restart dropped what it had not committed: send it again), and with SettlementTimeoutError after 60 s without a commit.

Not during a dispute. A node that serves through one keeps the calls it takes and commits them after the verdict, and it forgets a receipt after about ten minutes. settled looks for the call in the queue the node holds and never calls it lost while the dispute is open: it waits for the verdict (When your app is challenged). SDK 0.2.2 and older can reject such a call with SettlementLostError, and sending it again is a second copy of the action: update to 0.2.3.

watch and watchRead need a global WebSocket: every browser, and Node 22 or later. On Node 20, watch delivers nothing and watchRead falls back to polling the view every 500 ms.

Forcing a commit

The node publishes on an interval, every 10 s on a node ship started. interlude.commit() publishes now, and only where you hold the node’s commit token: on a local interlude dev node, which sets none, or a node you started yourself with INTERLUDE_COMMIT_TOKEN (pass it as commitToken on the client). A node ship started refuses it: its token is control’s, not yours. There, await a send’s settled, or interlude.waitSettled({ hash }), to know when Monad has the call.

Every method and option, hooks included, is in the SDK’s README on npm.