How the Recovery page works
Part of the Recovery sample tour.
The page is React 19, Vite and graphql-ws, on the client trax client
generates from the host's manifest (src/trax/). The host runs only persisted documents: the machine operations and
runProgress, which the host generates and registers itself, and the page's own document for each train it calls
(src/graphql/*.graphql), which trax client --documents writes into trax.documents.json and the host registers
when it starts. The page signs in as the demo user (recovery-user, src/connection.ts), so it sees only its own
drafts and runs and sends nothing under operations; requeueing, resuming and asking afresh are an operator's, in the
dashboard. Over the socket the key travels in the connection_init payload as apiKey, because a browser cannot set
headers on a WebSocket upgrade. Both views keep their panels a fixed
size, so nothing moves as a run fills them.
The code panels import the sample's .cs files raw at build time. A panel shows the part that acts (a train's chain
from Junctions() to Resolve(), or a machine's states from its first m.In(...)), re-indents each chain so the
step names sit in one column, and fits its font to the longest line between 12 and 11 pixels; a line still too long
wraps under itself with a hanging indent, and the line numbers stay the file's.
Food order. Place order arms the scenario's trouble with the generated orderTrouble, then sends the order
machine's own operations through its generated server, start, addItem and placeOrder, as
scripts/recordings/order.mjs does: placeOrder twice, 120 ms apart, under one request id. The laptop and the
phone each follow the order's push, orderChanged, on a socket of their own, and neither polls the order. The run
Ordered queued is the one the order's own answer names, invokedRun { runId }; the page follows it with
runProgress and reads ordersTabletCalls while the run lasts, so a tablet call
that went unanswered shows as a failed try, a retry and the next try of SendToRestaurant. Everything on screen is
worked out from that list of timed events (src/order/orderView.ts): the phone's status and notifications, the
machine's line to light (the .On(...) of the event sent, the m.In(...) or outcome line of the state reached, the
.Invokes(...) line while the run is on its junctions, the .After(...) line when the timer clears the cart), the
train's junction to light, and the timeline's boxes. Reset discards an order still in its cart, since a user's
open carts count against their drafts.
Workflows. Run sends the generated startRun, which schedules a one-off manifest for the caller
(ScheduleOptions.StartedBy) and answers the id of its first run. For each attempt the page:
- subscribes to
runProgress(runId), then readsrunProgressonce and merges both byposition, keeping whichever copy of a step is further along; - when the attempt fails and the scheduler queues its retry, follows the run the stream names in
retriedAs, as the next attempt; - reads the decision journal as soon as a question's step arrives, and once more when the attempt ends.
The refund and topic map trains carry [TraxRunProgress(Answers = true)], so their steps show the starter each
answer and the junctions on a track.
Junction events say whether an answer was replayed, not why one was not, nor what was asked. The sample adds a
small [TraxQuery] train, decisionJournal(runId), that reads IDataContext.RecordedDecisions and the run's
ReplayDecisionsOf and ReplayAbandoned, for the run's starter only (IRunStarters). Each decision carries the question as Trax put it to the decider (its
instructions and each option's description), the answer the run acted on, the model and the decider, so the model
panel shows a card per question with its words and its answer: the choice and how sure (Damaged, 91% sure), or the
probability of yes and the track it took (93% yes: the Yes track). Its badge tells replayed from asked
afresh: state changed (a replay_refused reason) from asked afresh: on purpose (no replay link).
The timeline is a fixed grid with one row per attempt and one box per node of the train's declared chain, as
src/cases.ts writes it down (src/workflowView.ts): a junction, a question (with its answer), a routing step (named by the
junction on the track the run took), and the checkpoint. A Parallel step's branches are laid side by side in the
row. A box fills while its step runs. The checkpoint's box reads saved on the attempt that stored it and
restored on an attempt that started from it, whose earlier boxes are restored with it: a retry that starts from
the checkpoint records only the steps after it. Each row ends in a status pill, Completed, Crashed or
Recovered.
A ROUTE step carries replayed: false even when the decision it routes on was replayed; the timeline and the model
panel read the question's step.
Change the data during the backoff sends the generated changeCaseData for the case the run reads, which only
the caller who started the run may change. Asking afresh, requeueing and resuming from NotifyCustomer are an
operator's: the dashboard does each, on the same run.