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 that closes over whatever you need, and return the instance. The result is easier to type and 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 typically set up timers or subscriptions to external events. 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 home is context. Pass a plain object, and machina hands it to every handler as ctx. FSMs without internal state can omit it.
Handler signatures
Section titled “Handler signatures”This is the biggest change in v6.
v4 bound handlers to this (the FSM instance). Everything, from reading state to calling transition() or deferUntilTransition(), went through this. Arrow functions were broken because they captured the outer this.
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, and returning nothing (or undefined) leaves 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, so 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() }, so 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, and later 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, and v6 renamed it so the before/after pair reads clearly:transitioningthentransitioned.newfsmis removed. v4 emitted it when a new FSM was created, and 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. BehavioralFsmbuilt-in event payloads include aclientfield so you can tell which client fired it.- Wildcard
"*"listener receives(eventName, data)as two arguments. v4 passed one 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”One FSM definition still serves 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, and 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. machina delegates inputs to the child first, and unhandled inputs bubble to the parent.
What changed:
- v4 accepted
_child: { factory: fn }and_child: ConstructorFn, factory patterns for creating the child FSM. v6 accepts only 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 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 without a replacement..extend(): use factory functions.initialize(): do external setup after callingcreateFsm.useSafeEmit: removed, since emission is always safe.processQueue()/clearQueue(): removed, because deferred queue processing is automatic.deferAndTransition(): removed. Calldefer()and then return a state name.- Plugin / mixin system: removed without a replacement.
__machina__client stamping: replaced by an internalWeakMap, and machina doesn’t modify client objects.
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 |