Skip to content

Machines

To build state-machines with yay-machine:

  1. Define the behaviour of the machine at compile-time

  2. 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

heaterMachine.ts
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 machine
const 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 off
const heater = heaterMachine.newInstance().start();
assert.deepStrictEqual(heater.state, {
name: "off",
temperature: 21,
integrityCheck: "2+2=4",
});
// switch on; machine enters "self-check" state
heater.send({ type: "ON" });
assert.deepStrictEqual(heater.state, {
name: "selfCheck",
temperature: 21,
integrityCheck: "2+2=4",
});
// wait for async self-check to complete
await new Promise<void>((resolve) => {
const unsubscribe = heater.subscribe(({ state }) => {
if (state.name === "heat") {
resolve();
unsubscribe();
}
});
});
// heater is in "heat" mode
assert.deepStrictEqual(heater.state, {
name: "heat",
temperature: 21,
integrityCheck: "2+2=4",
});
// make it hotter
heater.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" mode
for (let i = 0; i < 15; i++) {
heater.send({ type: "COOLER" });
}
assert.deepStrictEqual(heater.state, {
name: "cool",
temperature: 9,
integrityCheck: "2+2=4",
});