Skip to content

Composing FSMs

A single class can contain multiple independent state machines, isolated via opt.context.

Scenario

For example, an App class manages both a “timer” state machine and a “connection” state machine. They have independent states that don’t interfere.

Usage

class App extends FSM {
// Timer state machine (context='timer')
@ChangeState([FSM.INIT, FSM.OFF], FSM.ON, { context: 'timer' })
async startTimer() {}
@ChangeState(FSM.ON, FSM.OFF, { context: 'timer' })
async stopTimer() {}
// Connection state machine (context='conn')
@ChangeState(FSM.INIT, 'connected', { context: 'conn' })
async connect() {}
}

When app.startTimer() is called:

  1. fsm = FSM.get('timer') (from the FSM.instances Map)
  2. The state change happens on fsm, not on app itself

app.state is still app’s own state (likely FSM.INIT since no @ChangeState without context acts on app).

FSM.get(context)

static get(context: string | object): IFSM
  • String: from FSM.instances Map
  • Object: from FSM.instances2 WeakMap

If absent, creates a minimal proxy instance (Object.create(FSM.prototype)) and registers it.

const fsm1 = FSM.get('timer')
const fsm2 = FSM.get('timer')
console.log(fsm1 === fsm2) // true

Listening to composed FSMs

Listen on the context-associated FSM, not app:

const timerFsm = FSM.get('timer')
timerFsm.on(FSM.STATECHANGED, (newState) => {
console.log('timer:', newState)
})

context as a function

context can also be a function, dynamically decided from arguments at runtime:

@ChangeState(FSM.INIT, 'connected', {
context: (this, url) => `conn:${url}`
})
async connect(url: string) {}

Each connect(url) gets the FSM associated with url.

stateDiagram and context

@ChangeState with opt.context set does not register in the module-level stateDiagram Map (to avoid mixing edges from multiple state machines). So each sub-FSM’s diagram must be fetched separately:

const timerFsm = FSM.get('timer')
console.log(timerFsm.stateDiagram) // note: proxy instance has no decorator metadata

Next steps