Migrating from v4 to v6
v4.0.2 was the last public release of machina. v5 was never released. v6 is a ground-up TypeScript rewrite: the mental model of states, inputs, handlers, and transitions is unchanged, but almost every API surface has moved. This page is the translation guide.
Imports and creation
Section titled “Imports and creation”v4 exported a machina namespace object with constructor functions on it. v6 exports named factory functions.
v4
v6
.extend() is gone. v4 used prototype-based inheritance to share and override FSM behavior across instances. v6 replaces that pattern with plain factory functions — call createFsm inside a function, close over whatever you need, and return the instance. Less magic, easier to type, easier to follow.
Config changes
Section titled “Config changes”| v4 property | v6 equivalent | Notes |
|---|---|---|
namespace | id | Renamed. Used in lifecycle event payloads for debugging. |
initialState | initialState | Unchanged. |
states | states | Unchanged. |
initialize() | removed | Wire up external listeners outside the FSM config. See below. |
useSafeEmit | removed | v4’s useSafeEmit wrapped handlers in a try/catch. In v6, this is the caller’s responsibility. |
| (none) | context | New. A plain object scoped to this FSM, passed to every handler as ctx. |
namespace → id
initialize() is removed
v4’s initialize() was a lifecycle hook that ran after construction. It was typically used to set up timers, subscribe to external events, or wire callbacks. In v6, do that work outside the createFsm call:
context is new on createFsm
v6 has no this in handlers (see next section), so mutable state needs a home. That’s context. Pass a plain object; machina hands it to every handler as ctx. Omit it for FSMs that don’t need state.
Handler signatures
Section titled “Handler signatures”This is the biggest change in v6.
v4 bound handlers to this (the FSM instance). Reading state, calling transition(), calling deferUntilTransition() — all of it went through this. Arrow functions were broken because they captured the outer this instead.
v6 handlers receive a plain HandlerArgs object as the first argument. Arrow functions work fine. this is never involved.
The full HandlerArgs object:
| Field | Description |
|---|---|
ctx | The context object (or the client object for BehavioralFsm). Mutate freely. |
inputName | The input currently being handled. Useful in "*" catch-alls. |
defer | Function to queue the current input for replay after a future transition. |
emit | Function to emit a custom event through the FSM. |
Additional arguments passed to handle("inputName", arg1, arg2) are spread after the args object: handler({ ctx, inputName, defer, emit }, arg1, arg2).
Transitions
Section titled “Transitions”v4: call this.transition() imperatively from inside handlers.
v6: return a state name from a handler. Returning a string triggers the transition; returning nothing (or undefined) keeps the FSM in its current state.
transition() still exists as a public method for transitioning from outside a handler — a timer callback, an event listener, anything external to the FSM. Inside handlers, return is the pattern.
Deferred inputs
v4’s deferUntilTransition(state) and deferAndTransition(state) are replaced by the defer function in handler args.
deferAndTransition() is gone — it was shorthand for defer-then-transition. Just do both explicitly.
Methods
Section titled “Methods”| v4 method | v6 equivalent | Notes |
|---|---|---|
fsm.handle(input, ...args) | fsm.handle(input, ...args) | Unchanged. |
fsm.state | fsm.currentState() | Property → method. |
this.transition(state) | return state name from handler | Or fsm.transition(state) from outside a handler. |
this.deferUntilTransition(s) | defer({ until: "s" }) in args | Via the handler args object. |
this.deferUntilTransition() | defer() in args | No argument = next transition. |
this.deferAndTransition(s) | defer(…); return "s" | Removed — do both explicitly. |
fsm.processQueue() | removed | Queue processing is automatic. |
fsm.clearQueue() | removed | Queue processing is automatic. |
fsm.off(event, cb) | sub.off() | on() returns { off() } — use the subscription. |
| (none) | fsm.canHandle(input) | New. True if current state has a handler for this input. |
| (none) | fsm.reset() | New. Transitions back to initialState. |
| (none) | fsm.dispose() | New. Permanently shuts down; subsequent calls are no-ops. |
| (none) | fsm.compositeState() | New. Dot-delimited path through active child FSM states. |
Unsubscribing from events
v4’s fsm.off(eventName, callback) is removed. In v6, on() returns a subscription object with an off() method:
Events
Section titled “Events”Most event names are the same, but two changed:
transition→transitioning— v4 emitted"transition"before the state change. v6 renamed it to"transitioning"so the before/after pair reads clearly:transitioningthentransitioned.newfsm— removed. v4 emitted this when a new FSM was created. v6 doesn’t.
The rest are unchanged: transitioned, handling, handled, nohandler, invalidstate, deferred.
What else changed:
- Payloads no longer include
namespace. Useidif you need to identify the source FSM from outside. BehavioralFsmpayloads include aclientfield on every event so you can tell which client fired it.- Wildcard
"*"listener receives(eventName, data)as two separate arguments, not a single wrapped object.
Custom events via emit
v4 had fsm.emit() as a public method. v6 keeps that, and also surfaces emit inside handler args so handlers can fire custom events without needing a reference to the FSM instance:
BehavioralFsm
Section titled “BehavioralFsm”The concept is the same: one FSM definition, many independent clients.
Key differences:
- No
contextproperty in the config. The client IS the context. Handlerctxis the client object. - v4 stamped
__machina__onto client objects to track state. v6 uses an internalWeakMap— client objects stay clean. You don’t need to delete or ignore__machina__entries. - State lookup: read
bfsm.currentState(client)instead ofclient.__machina__[namespace].state. currentState(client)returnsundefinedfor a client the FSM has never seen. The first call tohandle(),transition(), orreset()initializes the client.
Hierarchical states
Section titled “Hierarchical states”_child works the same way: assign an FSM instance to a state’s _child property, and inputs are delegated to the child first, bubbling to the parent if unhandled.
What changed:
- v4 accepted
_child: { factory: fn }and_child: ConstructorFn— factory patterns for creating the child FSM. v6 only accepts a direct FSM instance. Create the child withcreateFsmbefore building the parent. compositeState()works the same.- Child auto-reset on parent re-entry works the same — entering a parent state always resets the child to its
initialState. dispose()cascades to children by default. Pass{ preserveChildren: true }to skip it.
Removed features
Section titled “Removed features”machina.utils— removed. No replacement..extend()— use factory functions.initialize()— do external setup after callingcreateFsm.useSafeEmit— removed. Emission is always safe.processQueue()/clearQueue()— removed. Deferred queue processing is automatic.deferAndTransition()— removed. Calldefer()then return a state name.- Plugin / mixin system — removed. No replacement.
__machina__client stamping — replaced by internalWeakMap. Client objects are not modified.
Quick reference
Section titled “Quick reference”| Pattern | v4 | v6 |
|---|---|---|
| Create FSM | new machina.Fsm({ namespace, ... }) | createFsm({ id, context, ... }) |
| Create behavioral | new machina.BehavioralFsm({ ... }) | createBehavioralFsm<Client>({ ... }) |
| Handler context | this (bound to FSM) | { ctx, inputName, defer, emit } arg |
| Arrow functions | broken (wrong this) | work fine |
| Transition (inside) | this.transition("state") | return "state" |
| Transition (outside) | fsm.transition("state") | fsm.transition("state") (unchanged) |
| Defer | this.deferUntilTransition("state") | defer({ until: "state" }) |
| Defer + transition | this.deferAndTransition("state") | defer(…); return "state" |
| Get state | fsm.state | fsm.currentState() |
| Subscribe | fsm.on(event, cb) | const sub = fsm.on(event, cb) |
| Unsubscribe | fsm.off(event, cb) | sub.off() |
| Behavioral handle | bfsm.handle(client, input) | bfsm.handle(client, input) (unchanged) |
| Behavioral state | client.__machina__[ns].state | bfsm.currentState(client) |
| Child FSM | _child: { factory: fn } or constructor | _child: fsmInstance |