Job Queue
A simulated job queue where multiple jobs share a single BehavioralFsm definition, every job’s snapshot (state, plus any pending deferred input) is auto-persisted to localStorage on every transition, and a page refresh restores all jobs via rehydrate(). The centerpiece teaching moment: rehydrate() is silent — _onEnter doesn’t fire — so the app must explicitly re-establish side effects (timers) for in-flight jobs.
What it demonstrates
Section titled “What it demonstrates”dehydrate()/rehydrate(snapshot)for the full persist/restore cycle — snapshot everything machina tracks for a job (state, plus any pending deferred input) and restore it later with no_onEnter,_onExit, or events firing. The WeakMap entry is written as if the client had always been there.- Deferred inputs survive the round trip — click Pause on start on a still-
queuedjob to defer a pre-emptivepauseuntil it reachesprocessing, then refresh the page and start the job: it still pauses. A bare state string can’t carry that; the snapshot form does. See Persisting clients for the general pattern. - Two-step restore: rehydrate + resume — after
rehydrate()places a job inprocessing, the app dispatchesresumeto restart the tick timer. This is the canonical pattern for recovering side effects after a cold restore. createBehavioralFsmwith per-client state — one FSM definition drives all jobs. Each job is a plain object; state lives in the FSM’s internal WeakMap, not on the client.- localStorage persistence on every transition — the
transitionedevent handler serializes every job’s snapshot. The snapshot is stored alongside the client, not on it, matching therehydrate()API’s design.
States
Section titled “States”| State | What happens |
|---|---|
queued | Registered but not running. start transitions to processing. pause (the Pause on start button) defers itself until processing is reached — a pre-emptive pause, and the deferred input that demonstrates the snapshot round trip. |
processing | Tick timer advances currentStep. Random failure chance on each tick. pause stops the timer. resume restarts it silently. |
paused | Timer stopped. resume transitions back to processing, firing _onEnter to restart the timer. |
failed | Something went wrong during a tick. retry resets currentStep to 0 and transitions to processing for a fresh run. |
completed | Terminal. Job reached totalSteps. No inputs handled. |
The rehydrate pattern
Section titled “The rehydrate pattern”On page load, main.ts reads localStorage and restores each job in two steps:
The resume handler on processing restarts the tick timer without transitioning — the job stays in processing and picks up where it left off. This dual meaning of resume is intentional:
- From
paused:resumetransitions toprocessing, which fires_onEnterand starts the timer normally. - While in
processing:resumerestarts the timer in-place. No transition, no events beyondhandling/handled. This is the post-rehydrate path.
Persistence design
Section titled “Persistence design”The job’s dehydrate() snapshot is persisted alongside the client, not on it. The serialized shape stores it as a sibling field:
This matches the rehydrate() API: fsm.rehydrate(client, snapshot) takes the snapshot as a separate argument because BehavioralFsm stores state (and deferred inputs) in a WeakMap, not on the client object. Using the snapshot form instead of a bare state string is what lets a queued job’s pre-emptive pause — deferred until it reaches processing (see fsm.ts) — survive the round trip. A bare state string can only ever restore the “queued” part; the deferral itself would silently vanish.
On deserialization, timer is set to null. The resume input re-establishes it for processing jobs.
Stale storage recovery
Section titled “Stale storage recovery”rehydrate() throws synchronously for unknown state names — including ones referenced inside a corrupted snapshot. main.ts wraps the restore loop in try/catch — if a schema change makes old data incompatible, it clears localStorage and starts fresh rather than crashing: