Traffic Light
The light bulb waited for you to click. A traffic light drives itself: it holds each colour for a while, then advances on its own. This example shows how a state can generate its own events through a generator, so the machine keeps moving without any outside input.
Try it
The light cycles Red → Green → Yellow → Red automatically. Each state holds for a moment, then emits next.
The configuration
A plain cycle — each colour advances to the next on the same next event:
const config = {
key: "TrafficLight",
transitions: [
["", "*", "Red"],
["Red", "next", "Green"],
["Green", "next", "Yellow"],
["Yellow", "next", "Red"],
],
};
Self-driving states with startProcess
The interesting part is that no one dispatches next from outside. Each state emits it after holding for a while. The runner makes this natural: a load handler can return a generator, and every value it yields is dispatched back into the machine. The generator is stopped automatically when the state exits, so the timer never leaks.
import { startProcess } from "@statewalker/fsm";
const HOLD = { Red: 2500, Green: 2000, Yellow: 1000 };
const wait = (ms) => new Promise((r) => setTimeout(r, ms));
await startProcess({}, config, (stateKey) => {
const hold = HOLD[stateKey];
if (!hold) return [];
// On entry, wait, then yield "next" — which drives the transition.
return [async function* () {
await wait(hold);
yield "next";
}];
});
Each colour, on entry, starts its generator: it sleeps for its hold time and yields next. That event fires the transition to the following colour, whose generator starts in turn — a machine that runs forever with no external clock. Because the generator is return()-ed on exit, a colour that is left early (say you added a manual override) cancels its pending next cleanly.
Driving it by hand
Without the runner, the same idea is a setTimeout in onEnter and a clearTimeout in onExit — exactly what powers the live widget above:
process.onStateCreate((state) => {
state.onEnter(() => {
const id = setTimeout(() => process.dispatch("next"), HOLD[state.key] ?? 1500);
state.onExit(() => clearTimeout(id));
});
});
await process.dispatch("");
Next
- Coffee Machine — nested states and events that bubble up the hierarchy.
- The startProcess Runner — the full generator contract.