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:
fsm = FSM.get('timer')(from theFSM.instancesMap)- The state change happens on
fsm, not onappitself
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.instancesMap - Object: from
FSM.instances2WeakMap
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) // trueListening 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 metadataNext steps
- Abort & Interruption —
abortActionwith context - API: ChangeOption.context