A tiny event-driven finite state machine library for TypeScript/JavaScript.
npm install dipper.js
dipper.js
hadles state management while consolidating a few key concepts: states
, state machine
, events
, transitions
and hooks
.
import { State } from 'dipper.js';
const stateOne = new State({ name: 'state one' });
const stateTwo = new State(); // naming states is optional,
// it is there to facilitate debugging
// and for future visualization tools
Defines state behaviors, most of time the you'll need to setup the hooks enter
and/or leave
, which the state machine
executes by default.
Custom hooks will be exectuted when triggered through the state machine
.
import { Hook } from 'dipper.js';
const sayHiOnEnter: Hook = {
name: 'enter',
action: () => {
console.log('hi');
}
}
const sayByeOnLeave: Hook = {
name: 'leave',
action: () => {
console.log('bye');
}
}
const screamOnEscape: Hook = {
name: 'escape',
action: () => {
console.log('AAAAAARRRGH');
}
}
This is how to assiciate a hook
with a state
import { State, Hook } from 'dipper.js';
const greeter = (new State())
.hook(sayHiOnEnter)
.hook(sayByeOnLeave)
.hook(screamOnEscape);
or
import { State, Hook } from 'dipper.js';
const greeter = (new State())
.hook({
name: 'enter',
action: () => {
console.log('hi');
}
})
.hook({
name: 'leave',
action: () => {
console.log('bye');
}
})
.hook({
name: 'escape',
action: () => {
console.log('AAAAAARRRGH');
}
});
import { StateMachine } from 'dipper.js';
const stateMachine = new StateMachine();
stateMachine.run({initialState: greeter});
stateMachine.trigger(`escape`);
You may use the subscriptions
attribute to hold any state relevant subscriptions, the state machine
will automatically unsubscribe from them before moving to the next state.
const subscription = $someObservable.subscribe(() => {
// do something
});
stateMachine.subscriptions.add(subscription);
If you have actions that need to be taken before and/or after every state you may do so by setting the before()
and after()
callbacks. Those callbacks also have access to the state machine context
stateMachine.before = (context) => {
console.log(`about to enter a state`, context);
}
stateMachine.after = (context) => {
console.log(`just left a state`, context);
}
Consider the case of a traffic light that has three states
:
green
yellow
red
import { StateMachine } from 'dipper.js';
import { green, red, yellow } from './traffic-light.states';
const stateMachine = (new StateMachine())
.transit({ from: green, to: yellow, on: 'next' })
.transit({ from: yellow, to: red, on: 'next' })
.transit({ from: red, to: green, on: 'next' });
stateMachine.run({ initialState: green });
The event next
triggers the transition between states.
Events can be emitted in two different ways:
- Directly into a state
state.emit('event-name');
- Into the current state through a state machine
stateMachine.emit('event-name');
.emit()
will send down an event to a state and may or may not trigger a state transition.trigger()
will execute a state hook
Local data (context) is emitted to the current state through the emit
.
const state = (new State())
.hook({
name: 'enter',
action: (context) => console.log(`hi ${context.local.personName}`);
})
...
state.emit('some event', { personName: 'John' }); // hi John
The global context is exposed to every state in by the state machine.
const stateMachine = new StateMachine();
stateMachine.context = { country: 'Italy' };
...
const state = (new State())
.hook({
name: 'enter',
action: (context) => console.log(`${context.local.personName} lives in ${context.global.country}`);
});
...
state.emit('some event', { personName: 'Jessica' }); // Jessica lives in Italy
Typing is optional but will allow you to take full advantage of the development tools.
The global
and local
component of ActionContext
can be typed in a number of ways. Below are some examples:
When creating a state
interface GlobalContext {
country: string;
}
interface LocalContext {
personName: string;
}
const state = (new State<GlobalContext, LocalContext>())
.hook({
name: 'enter',
action: (context) => {
console.log(`${context.local.personName} lives in ${context.global.country}`);
}
});
However, often times you'll need to get different data to different hooks, therefore you might not want to type the local context upon creating the state. Alternatively it can be typed within the hook action like so:
const state = (new State<GlobalContext>())
.hook({
name: 'enter',
action: (context: ActionContext<GlobalContext, LocalContext>) => {
// global and local contexts are type here
console.log(`${context.local.personName} lives in ${context.global.country}`);
}
})
.hook({
name: 'escape',
action: (context) {
// global context is still typed here!
}
});
Whe creating a state machine
const stateMachine = new StateMachine<GlobalContext>();
...
const context = stateMachine.context; // context is of type GlobalContext
Therefore:
context.global.country // ✅
context.global.personName // ❌
- Unit Tests
- JSDocs