← State machine lesson
Lab

State machine

type Phase = 'stored' | 'testing' | 'held' | 'released';
type LotEvent =
  | { type: 'start' }
  | { type: 'abandon' }
  | { type: 'pass' | 'hold'; runId: number };
type Lot = { phase: Phase; lastRun: number; activeRun: number | null };
type Decision = { lot: Lot; reason: string; effect: string };

// Which events each phase allows, and the phase each one leads to.
const transitions: Record<Phase, Partial<Record<LotEvent['type'], Phase>>> = {
  stored: { start: 'testing' },
  testing: { abandon: 'stored', pass: 'released', hold: 'held' },
  held: { start: 'testing' },
  released: {},
};

const storedLot = (): Lot => ({ phase: 'stored', lastRun: 0, activeRun: null });
const accept = (lot: Lot, effect = 'none'): Decision =>
  ({ lot, reason: 'accepted', effect });
const refuse = (lot: Lot, reason: string): Decision =>
  ({ lot, reason, effect: 'none' });

// Decides and returns; it does no work. Its caller commits the lot.
function transition(lot: Lot, event: LotEvent): Decision {
  const next = transitions[lot.phase][event.type];
  if (next === undefined) return refuse(lot, 'not-allowed');
  if (event.type === 'start') {
    const run = lot.lastRun + 1;
    return accept({ phase: next, lastRun: run, activeRun: run }, 'start-test');
  }
  if (event.type === 'abandon') {
    return accept({ ...lot, phase: next, activeRun: null });
  }
  // A result counts only if it comes from the active run.
  if (event.runId !== lot.activeRun) return refuse(lot, 'stale-run');
  return accept({ ...lot, phase: next, activeRun: null });
}

// The owner: one place holds the lot and commits each decision.
let lot = storedLot();
function dispatch(event: LotEvent): string {
  const decision = transition(lot, event);
  lot = decision.lot;
  const reason = decision.reason;
  console.log(`${event.type}: ${lot.phase}, run ${lot.lastRun}, ${reason}`);
  return reason;
}

dispatch({ type: 'start' });
const abandoned = lot.activeRun!;
dispatch({ type: 'abandon' });
dispatch({ type: 'start' });
const current = lot.activeRun!;
const late = dispatch({ type: 'pass', runId: abandoned });
dispatch({ type: 'pass', runId: current });
console.log(`late pass: ${late}; the lot ends ${lot.phase}`);