fsmoothy
is a TypeScript library for building functional state machines. It's inspired by aasm and provide magic methods for transitions.
Let's create a basic order state machine to showcase the features of the library. The diagram below illustrates the states and transitions of the state machine.
stateDiagram-v2
draft --> assembly: create
assembly --> warehouse: assemble
assembly --> shipping: ship
warehouse --> warehouse: transfer
warehouse --> shipping: ship
shipping --> delivered: deliver
The library was initially designed to use enums
for events and states. However, using string enums would provide more convenient method names. It is also possible to use string
or number
as event or state types, but this approach is not recommended.
enum OrderItemState {
draft = 'draft',
assembly = 'assembly',
warehouse = 'warehouse',
shipping = 'shipping',
delivered = 'delivered',
}
enum OrderItemEvent {
create = 'create',
assemble = 'assemble',
transfer = 'transfer',
ship = 'ship',
deliver = 'deliver',
}
interface IOrderItemContext {
place: string;
}
To create state machine, we need to instantiate StateMachine
class and pass the initial state and the state transition table to the constructor.
import { StateMachine, t } from 'fsmoothy';
const orderItemFSM = new StateMachine({
id: 'orderItemsStatus',
initial: OrderItemState.draft,
ctx: {
place: 'My warehouse',
},
transitions: [
t(OrderItemState.draft, OrderItemEvent.create, OrderItemState.assembly),
t(
OrderItemState.assembly,
OrderItemEvent.assemble,
OrderItemState.warehouse,
),
t(
OrderItemState.warehouse,
OrderItemEvent.transfer,
OrderItemState.warehouse,
{
guard(context: IOrderItemContext, place: string) {
return context.place !== place;
},
onExit(context: IOrderItemContext, place: string) {
context.place = place;
},
},
),
t(
[OrderItemState.assembly, OrderItemState.warehouse],
OrderItemEvent.ship,
OrderItemState.shipping,
),
t(
OrderItemState.shipping,
OrderItemEvent.deliver,
OrderItemState.delivered,
),
],
});
Let's take a look at the IStateMachineParameters<State, Event, Context>
interface. It has the following properties:
id
- a unique identifier for the state machine (used for debugging purposes)initial
- the initial state of the state machinectx
- initial context of the state machinetransitions
- an array of transitionssubscribers
- an object with subscribers array for eventsThe most common way to define a transition is by using the t
function, which requires three arguments (guard is optional).
t(from: State | State[], event: Event, to: State, guard?: (context: Context) => boolean);
We also able to pass optional onEnter
and onExit
functions to the transition as options:
t(
from: State | State[],
event: Event,
to: State,
options?: {
guard?: (context: Context) => boolean;
onEnter?: (context: Context) => void;
onExit?: (context: Context) => void;
},
);
In such cases, we're using next options:
from
- represents the state from which the transition is permittedevent
- denotes the event that triggers the transitionto
- indicates the state to which the transition leadsguard
- a function that verifies if the transition is permissibleonEnter
- a function that executes when the transition is triggeredonExit
- a function that executes when the transition is completedTo make a transition, we need to call the transition
method of the fsm or use methods with the same name as the event. State changes will persist to the database by default.
await orderItemFSM.create();
await orderItemFSM.assemble();
await orderItemFSM.transfer('Another warehouse');
await orderItemFSM.ship();
We're passing the place
argument to the transfer
method. It will be passed to the guard
and onExit
functions.
We can add transition dynamically using the addTransition
method.
const newOrderItemFSM = orderItemFSM.addTransition([
t(
OrderItemState.shipping,
OrderItemEvent.transfer,
OrderItemState.shipping,
{
guard(context: IOrderItemContext, place: string) {
return context.place !== place;
},
onExit(context: IOrderItemContext, place: string) {
context.place = place;
},
},
),
]);
It returns a new instance of the state machine with the added transition. Context and current state will be copied to the new instance.
You can get the current state of the state machine using the current
property.
console.log(orderItemFSM.current); // draft
Also you can use is
+ state name
method to check the current state.
console.log(orderItemFSM.isDraft()); // true
Also is(state: State)
method is available.
You can check if the transition is available using the can
+ event name
method.
console.log(orderItemFSM.canCreate()); // true
await orderItemFSM.create();
console.log(orderItemFSM.canCreate()); // false
await orderItemFSM.assemble();
Arguments are passed to the guard
function.
await orderItemFSM.transfer('Another warehouse');
console.log(orderItemFSM.canTransfer('Another warehouse')); // false
Also can(event: Event, ...args)
method is available.
You can subscribe to transition using the on
method. And unsubscribe using the off
method.
const subscriber = (state: OrderItemState) => {
console.log(state);
};
orderItemFSM.on(OrderItemEvent.create, subscriber);
await orderItemFSM.create();
orderItemFSM.off(OrderItemEvent.create, subscriber);
Also you're able to subscribe to transaction on initialization.
const orderItemFSM = new StateMachine({
initial: OrderItemState.draft,
transitions: [
t(OrderItemState.draft, OrderItemEvent.create, OrderItemState.assembly),
t(
OrderItemState.assembly,
OrderItemEvent.assemble,
OrderItemState.warehouse,
),
t(
[OrderItemState.assembly, OrderItemState.warehouse],
OrderItemEvent.ship,
OrderItemState.shipping,
),
t(
OrderItemState.shipping,
OrderItemEvent.deliver,
OrderItemState.delivered,
),
],
subscribers: {
[OrderItemEvent.create]: [(state: OrderItemState) => {
console.log(state);
}]
}
});
The state machine has the following lifecycle methods in the order of execution:
- guard
- onEnter
- transition
- subscribers
- onExit
You can access the fsm instance using this
keyword.
orderItemFSM.onEnter(function () {
console.log(this.current);
});
orderItemFSM.on(OrderItemEvent.create, function () {
console.log(this.current);
});
await orderItemFSM.create();
Library throws StateMachineError
if transition is not available. It can be caught using try/catch
and checked using isStateMachineError
function.
import { isStateMachineError } from 'typeorm-fsm';
try {
await orderItemFSM.create();
} catch (error) {
if (isStateMachineError(error)) {
console.log(error.message);
}
}
npm install fsmoothy
Check out the examples directory for more examples.
Take a look at the CHANGELOG for details about recent changes to the current version.
Generated using TypeDoc