Job Queue
A simulated job queue where multiple jobs share a single BehavioralFsm definition, state 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”rehydrate()for silent state restoration — place a client at a saved state without triggering_onEnter,_onExit, or any events. The WeakMap entry is written as if the client had always been there.- 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 all jobs and their states. State 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. |
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”State is persisted alongside the client, not on it. The serialized shape stores state as a sibling field:
This matches the rehydrate() API: fsm.rehydrate(client, state) takes state as a separate argument because BehavioralFsm stores state in a WeakMap, not on the client object.
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. 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: