Companion film
Replay
Walk through edits and rule firings as an event stream, inspect the past, then return to unchanged live state.
23 secGrid 0.61.0
Open film page →Guided build · 09
Subscribe over SSE, adopt the initial snapshot, order revisions, detect stale events, and reconnect through explicit resynchronization.
You will finish with: A resilient event-consumer workflow that treats SSE as invalidation plus synchronization rather than a gap-free value log.
You will subscribe to one Grid model over Server-Sent Events (SSE), synchronize its initial state, make a deliberate input change, and recover after intentionally missing an event. The finished client loop treats reconnect as resynchronization—not as gap-free replay.
The included SSE transcript is a deterministic local fixture. It is not a capture from a Runtime Host and is not a runtime execution receipt.
You need:
/readyz response is 200 and status: "ok";curl, and jq;realtime-revenue.grid and sample-stream.sse.export GRID_URL="http://localhost:3000"
export GRID_TOKEN="replace-with-your-api-token"
Use only the Node API. Node owns the SSE connection and fan-out; Runtime Host remains private and owns model execution. Browser EventSource cannot add an Authorization header, so these terminal commands use curl. For a browser client, use same-origin session authentication or a scoped server-side proxy as described under browser authentication.
An SSE stream is a sequence of fields separated by blank lines, not NDJSON. Inspect the included transcript:
sed -n 's/^data: //p' sample-stream.sse \
| jq -r '.type // "heartbeat"'
Deterministic local checkpoint:
stream:hello
model:snapshot
heartbeat
symbol:update
The named heartbeat event contains {} and therefore has no top-level type. A production parser must retain the SSE event name instead of treating every data: line as the same message class.
Create the request from the exact file and deploy it in protected mode:
jq -n --rawfile source realtime-revenue.grid '{
id: "realtime-revenue",
protectedMode: true,
source: $source
}' > /tmp/realtime-revenue-deploy.json
curl --fail-with-body \
-H "Authorization: Bearer $GRID_TOKEN" \
-H "Content-Type: application/json" \
-X POST "$GRID_URL/api/models" \
--data-binary @/tmp/realtime-revenue-deploy.json \
| jq '{id, sourceHash, declaredInputs, declaredOutputs, diagnostics}'
Expected checkpoint on a compatible build: Revenue and TaxRate are declared inputs and NetRevenue is a declared output.
If that ID already exists, delete only this tutorial model with DELETE /api/models/realtime-revenue, then deploy again. Do not generalize cleanup to all models.
In terminal A, keep the default snapshot behavior:
curl --no-buffer --fail-with-body \
-H "Authorization: Bearer $GRID_TOKEN" \
"$GRID_URL/api/models/realtime-revenue/events"
Expected stream prefix:
stream:hello, with a server-owned stream ID;model:snapshot, containing control-plane state;heartbeat event every five seconds while the connection remains healthy.The initial model snapshot is not a dump of every evaluated workbook value. After adopting it, explicitly resolve the symbols visible to the client.
In terminal B:
curl --fail-with-body \
-H "Authorization: Bearer $GRID_TOKEN" \
-H "Content-Type: application/json" \
-X POST "$GRID_URL/api/models/realtime-revenue/symbols/resolve" \
--data '{"symbols":["Revenue","NetRevenue"]}' \
| tee /tmp/realtime-initial.json \
| jq '{
revenue: .Revenue.value.value,
netRevenue: .NetRevenue.value.value,
requestRev: .Revenue.requestRev,
resolvedRev: .NetRevenue.resolvedRev
}'
Expected live checkpoint: Revenue = 100000 and NetRevenue = 79000. Record the server-owned revisions; do not manufacture or renumber them in the client.
Still in terminal B, write the declared input:
curl --fail-with-body \
-H "Authorization: Bearer $GRID_TOKEN" \
-H "Content-Type: application/json" \
-X PUT "$GRID_URL/api/models/realtime-revenue/input" \
--data '{"symbol":"Revenue","value":120000}' \
| tee /tmp/realtime-write.json \
| jq '{symbol, status, value, requestRev, resolvedRev, lastError}'
Terminal A should receive a symbol:update for the new model revision. Resolve Revenue and NetRevenue again instead of assuming the event included every dependent value.
Expected checkpoint:
Revenue = 120000andNetRevenue = 94800.
For each symbol, accept a newly received snapshot only when its server-owned revision is not older than the snapshot your client already displays. The exact revision integers vary; their ordering is the contract.
In terminal A, stop curl with Ctrl-C. While disconnected, write another value in terminal B:
curl --fail-with-body \
-H "Authorization: Bearer $GRID_TOKEN" \
-H "Content-Type: application/json" \
-X PUT "$GRID_URL/api/models/realtime-revenue/input" \
--data '{"symbol":"Revenue","value":140000}' \
| jq '{symbol, status, value, requestRev, resolvedRev, lastError}'
Reconnect in terminal A with the step 3 command. You should not expect the disconnected update to replay: this stream emits no SSE id field and has no Last-Event-ID replay contract.
Repair the stale client state by adopting the new initial snapshot and resolving the visible symbols again.
Expected repaired checkpoint:
Revenue = 140000andNetRevenue = 110600.
A client that reconnects with ?snapshot=false and does not refetch has no synchronization proof. Remove that parameter unless another explicit synchronization path exists.
When a network error occurs or the named heartbeat stays silent beyond your chosen threshold:
Honor Retry-After if the reconnect path receives overload. Do not blindly retry mutations; read backpressure and retries.
curl --fail-with-body \
-H "Authorization: Bearer $GRID_TOKEN" \
-X DELETE "$GRID_URL/api/models/realtime-revenue"
Expected response: true.
94,800.140,000 write, reconnect, and restore 110,600 by resynchronizing.Last-Event-ID replay or expose a bearer token in a browser URL.Keep Realtime model events beside the implementation, and use Model lifecycle API for the resolve and input-write response shapes.