Elevator
🏷️ state data
🏷️ copy data on transition
🏷️ conditional transitions
🏷️ immediate (always) transitions
🏷️ delayed transition
🏷️ state entry side-effect
🏷️ send event to self
This example models an elevator (aka lift). This model operates completely standalone, but can also be combined into a group of coordinated elevators with the elevators controller.
The elevator machine’s state says where it is now and where it’s going, plus it has a queue of floors to visit next (if any). When a VISIT_FLOOR event is received (either by summoning it with the button in the lobby, or a passenger pressing a button on the control panel inside), the elevator either opens the doors (if already at the floor), else adds the floor to its queue.
Various states have onEntry() side-effects which ultimately use setTimeout() to send events to the machine to simulate 5 seconds to
- open/close doors
- move up/down one floor
This machine’s state data is homogenous and we’re using enableCopyDataOnTransition to simplify some transitions that don’t update the data.
Definition
Section titled “Definition”import {  type StateLifecycleSideEffectFunction,  defineMachine,} from "yay-machine";
export interface ElevatorState {  readonly name:    | "doorsClosing"    | "doorsClosed"    | "doorsOpening"    | "doorsOpen"    | "goingUp"    | "goingDown";  readonly currentFloor: number;  readonly actionStarted: number; // performance.now()  readonly floorsToVisit: readonly number[];}
export interface VisitFloorEvent {  readonly type: "VISIT_FLOOR";  readonly floor: number;}
export interface CloseDoorsEvent {  readonly type: "CLOSE_DOORS";}
export interface ClosedDoorsEvent {  readonly type: "CLOSED_DOORS";}
export interface OpenDoorsEvent {  readonly type: "OPEN_DOORS";}
export interface OpenedDoorsEvent {  readonly type: "OPENED_DOORS";}
export interface ReachedNextFloorEvent {  readonly type: "REACHED_NEXT_FLOOR";}
export type ElevatorEvent =  | VisitFloorEvent  | CloseDoorsEvent  | ClosedDoorsEvent  | OpenDoorsEvent  | OpenedDoorsEvent  | ReachedNextFloorEvent;
const sleepThen =  (    doneEvent: ElevatorEvent,    { time = 5000, restartTimer = false } = {},  ): StateLifecycleSideEffectFunction<ElevatorState, ElevatorEvent> =>  ({ state, send }) => {    const elapsed = restartTimer ? 0 : performance.now() - state.actionStarted;    const delay = Math.max(time - elapsed, 0);    const timer = setTimeout(send, delay, doneEvent);    return () => {      clearTimeout(timer);    };  };
const compareFloorToCurrent = (state: ElevatorState, floor: number) =>  floor - state.currentFloor;
const insertFloor = (state: ElevatorState, floor: number): ElevatorState => {  if (state.floorsToVisit.includes(floor)) {    const e = new Error();    console.warn("looks wrong we already have floor", floor, state, e.stack);    return state;  }  const nextFloor = state.floorsToVisit[0];  let floorsToVisit = [...state.floorsToVisit, floor];  floorsToVisit.sort((a, b) => a - b);  if (nextFloor !== undefined) {    const direction = compareFloorToCurrent(state, nextFloor);    const splitIndex = floorsToVisit.findLastIndex((floor) =>      direction < 0 ? floor < state.currentFloor : floor <= state.currentFloor,    );    if (splitIndex !== -1) {      const [lowerFloors, upperFloors] = [        floorsToVisit.slice(0, splitIndex + 1),        floorsToVisit.slice(splitIndex + 1),      ];      if (direction > 0) {        floorsToVisit = upperFloors.concat(lowerFloors.toReversed());      } else {        floorsToVisit = lowerFloors.toReversed().concat(upperFloors);      }    }  }
  return { ...state, floorsToVisit };};
const isAtFloor = (state: ElevatorState, floor: number) =>  floor === state.currentFloor &&  state.name !== "goingUp" &&  state.name !== "goingDown";
/** * Models an elevator moving between floors */export const elevatorMachine = defineMachine<ElevatorState, ElevatorEvent>({  enableCopyDataOnTransition: true, // most transitions don't change the state-data, so copy it by default  initialState: {    name: "doorsClosed",    currentFloor: 1,    actionStarted: -1,    floorsToVisit: [],  },  states: {    doorsClosing: {      onEnter: sleepThen({ type: "CLOSED_DOORS" }),      on: {        OPEN_DOORS: {          to: "doorsOpening",          data: ({ state }) => ({            ...state,            actionStarted: 5000 - performance.now() - state.actionStarted,            floorsToVisit: state.floorsToVisit.toSpliced(0, 1),          }),        },        CLOSED_DOORS: { to: "doorsClosed" },      },    },    doorsClosed: {      on: {        OPEN_DOORS: {          to: "doorsOpening",          data: ({ state }) => ({            ...state,            actionStarted: performance.now(),            floorsToVisit: state.floorsToVisit.toSpliced(0, 1),          }),        },      },      always: [        {          to: "goingUp",          when: ({ state }) =>            !!state.floorsToVisit[0] &&            state.floorsToVisit[0] > state.currentFloor,          data: ({ state }) => ({ ...state, actionStarted: performance.now() }),        },        {          to: "goingDown",          when: ({ state }) =>            !!state.floorsToVisit[0] &&            state.floorsToVisit[0] < state.currentFloor,          data: ({ state }) => ({ ...state, actionStarted: performance.now() }),        },      ],    },    doorsOpening: {      onEnter: sleepThen({ type: "OPENED_DOORS" }),      on: {        OPENED_DOORS: { to: "doorsOpen" },        CLOSE_DOORS: {          to: "doorsClosing",          data: ({ state }) => ({ ...state, actionStarted: performance.now() }),        },      },    },    doorsOpen: {      onEnter: sleepThen({ type: "CLOSE_DOORS" }, { restartTimer: true }),      on: {        OPEN_DOORS: {          to: "doorsOpen",        },        VISIT_FLOOR: {          to: "doorsOpen",          when: ({ state, event }) => state.currentFloor === event.floor,        },        CLOSE_DOORS: {          to: "doorsClosing",          data: ({ state }) => ({ ...state, actionStarted: performance.now() }),        },      },    },    goingUp: {      onEnter: sleepThen({ type: "REACHED_NEXT_FLOOR" }),      on: {        REACHED_NEXT_FLOOR: [          {            to: "goingUp",            when: ({ state }) =>              state.floorsToVisit[0] !== state.currentFloor + 1,            data: ({ state }) => ({              ...state,              currentFloor: state.currentFloor + 1,              actionStarted: performance.now(),            }),          },          {            to: "doorsOpening",            data: ({ state }) => ({              ...state,              actionStarted: performance.now(),              currentFloor: state.floorsToVisit[0],              floorsToVisit: state.floorsToVisit.toSpliced(0, 1),            }),          },        ],      },    },    goingDown: {      onEnter: sleepThen({ type: "REACHED_NEXT_FLOOR" }),      on: {        REACHED_NEXT_FLOOR: [          {            to: "goingDown",            when: ({ state }) =>              state.floorsToVisit[0] !== state.currentFloor - 1,            data: ({ state }) => ({              ...state,              currentFloor: state.currentFloor - 1,              actionStarted: performance.now(),            }),          },          {            to: "doorsOpening",            data: ({ state }) => ({              ...state,              actionStarted: performance.now(),              currentFloor: state.floorsToVisit[0],              floorsToVisit: state.floorsToVisit.toSpliced(0, 1),            }),          },        ],      },    },  },  on: {    VISIT_FLOOR: [      {        to: "doorsOpening",        when: ({ state, event }) =>          isAtFloor(state, event.floor) && state.name !== "doorsOpening",        data: ({ state }) => ({          ...state,          actionStarted: performance.now(),          floorsToVisit: state.floorsToVisit.toSpliced(0, 1),        }),      },      {        // to: *current-state*        data: ({ state, event }) => insertFloor(state, event.floor),        when: ({ state, event }) =>          !isAtFloor(state, event.floor) || state.name !== "doorsOpening",      },    ],  },});import { elevatorMachine } from "./elevatorMachine";
const elevator = elevatorMachine.newInstance().start();
// passenger requests elevator at floor 5elevator.send({ type: "VISIT_FLOOR", floor: 5 });
// time passes...
// ... doors open at floor 5// passenger enters elevator and presses floor 12 buttonelevator.send({ type: "VISIT_FLOOR", floor: 12 });
// time passes...
elevator.stop();