Machines
To build state-machines with yay-machine:
-
Define the behaviour of the machine at compile-time
-
Create instances and operate the machine(s) at run-time
Define at compile-time
We describe a state-machine’s behaviour - it’s potential state types, event types, transitions and side-effects - with defineMachine<StateType, EventType>({ /* ... */ })
, like so
import { defineMachine } from "yay-machine";
const MIN_TEMP = 0;const MIN_HEAT = 12;const MAX_TEMP = 50;
type HeaterState = { readonly name: "off" | "selfCheck" | "heat" | "cool" | "error"; readonly temperature: number; readonly integrityCheck: string; readonly errorCode?: string;};
type HeaterEvent = | { readonly type: "ON" } | { readonly type: "CHECK_OK" } | { readonly type: "CHECK_FAILED" } | { readonly type: "OFF" } | { readonly type: "HOTTER" } | { readonly type: "COOLER" };
// simulate some self-diagnostics integrity-check of a physical machineconst isHeaterOk = async (state: HeaterState): Promise<boolean> => state.integrityCheck === "2+2=4";
/** * A room-heater/cooler */export const heaterMachine = defineMachine<HeaterState, HeaterEvent>({ enableCopyDataOnTransition: true, initialState: { name: "off", temperature: 21, integrityCheck: "2+2=4" }, states: { off: { on: { ON: { to: "selfCheck" }, }, }, selfCheck: { onEnter: ({ send, state }) => { isHeaterOk(state).then((ok) => { if (!ok) { send({ type: "CHECK_FAILED" }); } else { send({ type: "CHECK_OK" }); } }); }, on: { CHECK_OK: [ { to: "heat", when: ({ state }) => state.temperature >= MIN_HEAT }, { to: "cool" }, ], CHECK_FAILED: { to: "error", data: ({ state }) => ({ ...state, errorCode: "NOTOK" }), }, }, }, heat: { on: { HOTTER: { to: "heat", data: ({ state }) => ({ ...state, temperature: state.temperature + 1, }), when: ({ state }) => state.temperature < MAX_TEMP, }, COOLER: [ { to: "heat", data: ({ state }) => ({ ...state, temperature: state.temperature - 1, }), when: ({ state }) => state.temperature > MIN_HEAT, }, { to: "cool", data: ({ state }) => ({ ...state, temperature: state.temperature - 1, }), }, ], }, }, cool: { on: { HOTTER: [ { to: "cool", data: ({ state }) => ({ ...state, temperature: state.temperature + 1, }), when: ({ state }) => state.temperature + 1 < MIN_HEAT, }, { to: "heat", data: ({ state }) => ({ ...state, temperature: state.temperature + 1, }), }, ], COOLER: { to: "cool", data: ({ state }) => ({ ...state, temperature: state.temperature - 1, }), when: ({ state }) => state.temperature > MIN_TEMP, }, }, }, }, on: { OFF: { to: "off" }, },});
This creates a “blueprint” for machine instances.
Operate at run-time
Once we have a machine-definition, we can easily create any number of machine-instances, start them, subscribe to their state, send them events, and eventually stop them.
import assert from "assert";import { heaterMachine } from "./heaterMachine";
// initially offconst heater = heaterMachine.newInstance().start();assert.deepStrictEqual(heater.state, { name: "off", temperature: 21, integrityCheck: "2+2=4",});
// switch on; machine enters "self-check" stateheater.send({ type: "ON" });assert.deepStrictEqual(heater.state, { name: "selfCheck", temperature: 21, integrityCheck: "2+2=4",});
// wait for async self-check to completeawait new Promise<void>((resolve) => { const unsubscribe = heater.subscribe(({ state }) => { if (state.name === "heat") { resolve(); unsubscribe(); } });});
// heater is in "heat" modeassert.deepStrictEqual(heater.state, { name: "heat", temperature: 21, integrityCheck: "2+2=4",});
// make it hotterheater.send({ type: "HOTTER" });heater.send({ type: "HOTTER" });heater.send({ type: "HOTTER" });assert.deepStrictEqual(heater.state, { name: "heat", temperature: 24, integrityCheck: "2+2=4",});
// switch to "cool" modefor (let i = 0; i < 15; i++) { heater.send({ type: "COOLER" });}assert.deepStrictEqual(heater.state, { name: "cool", temperature: 9, integrityCheck: "2+2=4",});