import assert from "./assert.js"; import { log } from "./logging.js"; export default class FSM { constructor(data, events) { // data is really only for logging/debugging this.data = data; this.events = events; this.state = "START"; this.on_state = undefined; } onTransition(cb) { this.transition_cb = cb; } transition(next_state) { this.state = next_state; if(this.transition_cb) { this.transition_cb(this); } } event_names() { return Object.getOwnPropertyNames(Object.getPrototypeOf(this.events)).filter(k => k !== "constructor"); } async do(event, ...args) { const evhandler = this.events[event]; assert(evhandler !== undefined, `Invalid event ${event}. Available ones are '${this.event_names()}'.`); // NOTE: you have to use .call to pass in the this.events object or else this === undefined in the call const next_state = await evhandler.call(this.events, this.state, ...args); assert(next_state, `The event "${event}" returned "${next_state}" but must be a truthy true state.`); if(Array.isArray(next_state)) { assert(next_state.length == 2, `Returning an array only allows 2 elements (state, func) but you returned ${next_state}`); let [state, func] = next_state; log.debug(`FSM ${this.events.constructor.name}: (${event}) = ${this.state} -> ${state} (${args})`, "DATA:", this.data, func ? `THEN ${func.name}()` : undefined); this.transition(state); await func(); } else { log.debug(`FSM ${this.events.constructor.name}: (${event}) = ${this.state} -> ${next_state} (${args})`, "DATA:", this.data); this.transition(next_state); } return this.state; } }