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