This guide explains how the Finite State Machine (FSM) utility works and how to use it with PureMVC (MultiCore) in TypeScript. It includes practical examples showing both programmatic setup and JSON-driven configuration via FSMInjector.
- A lightweight
StateMachineMediatorthat manages namedStateobjects - Declarative transitions between states triggered by simple action strings
- Optional notifications fired when entering, exiting, or after changing state
- A
FSMInjectorthat builds and registers theStateMachinefrom a JSON config
npm install @puremvc/puremvc-typescript-multicore-framework
npm install @puremvc/puremvc-typescript-util-state-machine
import {
StateMachine,
FSMInjector,
State,
type FSM,
} from '@puremvc/puremvc-typescript-util-state-machine';-
StateMachine (Mediator)
- Name:
StateMachine.NAME("StateMachine") - Notifications it cares about:
StateMachine.ACTION— trigger transitions (type = action string; body = optional data)StateMachine.CANCEL— cancel a pending transition during exiting/entering
- Notification it emits:
StateMachine.CHANGED— broadcast after a successful transition; body = newState; type = state name
- Holds a registry of
Stateinstances and trackscurrentState.
- Name:
-
State
- Properties:
name, optionalentering,exiting,changednotification names - Methods:
defineTransition(action: string, target: string)— define an action that leads to another stategetTarget(action: string)— resolve the target state name for an action
- Properties:
-
FSM JSON schema (for
FSMInjector)- Types exported as
FSM,StateDef,Transition - Shape:
type Transition = { action: string; target: string }; type StateDef = { name: string; entering?: string; // notification to send on entering exiting?: string; // notification to send on exiting changed?: string; // notification to send after state becomes current transitions?: Transition[]; }; type FSM = { initial: string; states: StateDef[] };
- Types exported as
When an actor sends StateMachine.ACTION with the desired action in the notification type, the StateMachine checks the current state for a matching transition:
- If found, an exit sequence starts:
- If the current state has
exiting, it sends that notification with:- body = any
datapassed in the ACTION - type = next state name (so observers know where we’re going)
- body = any
- Any observer can cancel the transition by sending
StateMachine.CANCELwhile handling the exit notification.
- If the current state has
- If not canceled, an enter sequence starts for the next state:
- If the next state has
entering, it sends that notification with body = the samedata. - Observers may also cancel here by sending
StateMachine.CANCEL.
- If the next state has
- If not canceled:
currentStatebecomes the next state.- If the state has
changed, it sends that notification with body = the samedata. - Then
StateMachine.CHANGEDis sent with body = the newStateand type = its name.
Notes:
- Cancelation resets the internal flag and the
currentStateremains unchanged. - The
StateMachineis aMediator; it should be registered with yourFacade. - On registration (
onRegister), if an initial state is set, it transitions to it immediately (running the enter/changed notifications for that state only).
This minimal example shows how to create states, define transitions, register the StateMachine, and fire actions.
import {
Facade,
Notifier,
INotification,
Mediator,
} from '@puremvc/puremvc-typescript-multicore-framework';
import { StateMachine, State } from '@puremvc/puremvc-typescript-util-state-machine';
const MULTITON_KEY = 'com.example.app';
// 1) Build your states
const opened = new State(
'OPENED',
'note/opening', // entering
null, // exiting
'note/opened', // changed
);
const closed = new State(
'CLOSED',
'note/closing', // entering
);
// 2) Define transitions
opened.defineTransition('CLOSE', 'CLOSED');
closed.defineTransition('OPEN', 'OPENED');
// 3) Create and register the StateMachine
const fsm = new StateMachine();
fsm.initializeNotifier(MULTITON_KEY);
fsm.registerState(opened /* initial? */);
fsm.registerState(closed, /* initial = */ true);
Facade.getInstance(MULTITON_KEY).registerMediator(fsm as Mediator);
// 4) Listen for changes and lifecycle notifications elsewhere
class DoorMediator extends Mediator {
public static NAME = 'DoorMediator';
constructor() { super(DoorMediator.NAME); }
listNotificationInterests(): string[] {
return [
'note/opening',
'note/opened',
'note/closing',
StateMachine.CHANGED,
];
}
handleNotification(note: INotification): void {
switch (note.name) {
case 'note/opening':
console.log('Entering OPENED with data:', note.body);
break;
case 'note/opened':
console.log('Changed to OPENED');
break;
case 'note/closing':
console.log('Entering CLOSED with data:', note.body);
break;
case StateMachine.CHANGED:
// note.body is the new State
console.log('FSM changed to:', (note.body as State).name);
break;
}
}
}
const facade = Facade.getInstance(MULTITON_KEY);
facade.registerMediator(new DoorMediator());
// 5) Trigger transitions from any Notifier-aware actor
class UserAction extends Notifier {
constructor() { super(); this.initializeNotifier(MULTITON_KEY); }
openDoor() {
this.sendNotification(StateMachine.ACTION, { by: 'user' }, 'OPEN');
}
closeDoor() {
this.sendNotification(StateMachine.ACTION, { by: 'user' }, 'CLOSE');
}
}
const user = new UserAction();
user.openDoor(); // CLOSED -> OPENED
user.closeDoor(); // OPENED -> CLOSEDTip: The action string is passed in the notification type. The optional body payload flows through exiting/entering/changed notifications.
Any observer of an exiting or entering notification can veto the move by sending StateMachine.CANCEL before the sequence completes.
class GuardMediator extends Mediator {
public static NAME = 'GuardMediator';
constructor() { super(GuardMediator.NAME); }
listNotificationInterests(): string[] {
return ['note/unlocking']; // suppose this is an exiting/entering note
}
handleNotification(note: INotification): void {
const shouldBlock = /* your rule */ false;
if (shouldBlock) {
this.facade.sendNotification(StateMachine.CANCEL);
}
}
}When canceled, the FSM remains in the current state and no changed or StateMachine.CHANGED notifications are sent.
Use FSMInjector when you want to drive the FSM from configuration rather than code.
import {
Facade,
} from '@puremvc/puremvc-typescript-multicore-framework';
import {
FSMInjector,
StateMachine,
type FSM,
} from '@puremvc/puremvc-typescript-util-state-machine';
const MULTITON_KEY = 'com.example.app';
const fsmConfig: FSM = {
initial: 'CLOSED',
states: [
{
name: 'OPENED',
entering: 'note/opening',
changed: 'note/opened',
transitions: [
{ action: 'CLOSE', target: 'CLOSED' },
],
},
{
name: 'CLOSED',
entering: 'note/closing',
transitions: [
{ action: 'OPEN', target: 'OPENED' },
{ action: 'LOCK', target: 'LOCKED' },
],
},
{
name: 'LOCKED',
entering: 'note/locking',
exiting: 'note/unlocking',
transitions: [
{ action: 'UNLOCK', target: 'CLOSED' },
],
},
],
};
// Build + register the StateMachine from JSON
const injector = new FSMInjector(MULTITON_KEY, fsmConfig);
const fsm = injector.inject(); // returns the created & registered StateMachine
// Listen for global CHANGED notifications
Facade.getInstance(MULTITON_KEY).registerMediator(new (class extends Mediator {
constructor() { super('LogFSM'); }
listNotificationInterests() { return [StateMachine.CHANGED]; }
handleNotification(note: INotification) {
console.log('FSM CHANGED ->', note.type); // state name
}
})());
// Fire actions as before
Facade.getInstance(MULTITON_KEY).sendNotification(StateMachine.ACTION, undefined, 'OPEN');How it works under the hood:
FSMInjector.inject()creates a newStateMachine, initializes it with your Multiton key, buildsStateinstances from eachStateDef, registers initial state, and registers theStateMachinewith yourFacade.- On
onRegister(), theStateMachineautomatically transitions to the initial state if one was provided.
- Keep action strings short and consistent (e.g.,
OPEN,CLOSE). Consider centralizing them in aconstenum or constants module. - Use the
exitingnotification’s type (which the FSM sets to the target state name) to detect where you’re headed, useful for preloading or validation. - Avoid side effects in
CHANGEDhandlers that could recursively trigger more actions unless you explicitly want chained transitions. - If a transition is often canceled, consider checking preconditions before sending
StateMachine.ACTIONto reduce churn. - Unit tests: mock observers can assert the exact order of notifications: exiting -> entering -> changed -> StateMachine.CHANGED.
-
Nothing happens when you send
StateMachine.ACTION:- Ensure the
StateMachineis registered with the same Multiton key as the sender. - Ensure the current state defines a transition for the given action string.
- Verify you passed the action in
type, notbody.
- Ensure the
-
Transition always aborts:
- Some observer may be sending
StateMachine.CANCELwhile handlingexitingorentering. Add logging to those handlers to find the culprit.
- Some observer may be sending
-
Didn’t enter the initial state:
- The initial state is set by passing
initial=truetoregisterState(...)or via the JSONinitialproperty. - Confirm
onRegister()is being called (i.e., theStateMachineis actually registered as a Mediator).
- The initial state is set by passing
-
StateMachine
- Constants:
NAME,ACTION,CHANGED,CANCEL - Methods:
registerState(state: State, initial?: boolean): voidgetState(name: string): State | undefinedremoveState(name: string): voidviewComponent: State | null— current state
- Constants:
-
State
- Constructor:
new State(name, entering?, exiting?, changed?) defineTransition(action, target)getTarget(action)
- Constructor:
-
FSMInjector
- Constructor:
new FSMInjector(multitonKey: string, fsm: FSM) inject(): StateMachine— creates states, registers theStateMachinewith the Facade
- Constructor:
If you need deeper details, see the source in src/fsm and the type definitions in src/types.ts.