
Security News
Attackers Are Hunting High-Impact Node.js Maintainers in a Coordinated Social Engineering Campaign
Multiple high-impact npm maintainers confirm they have been targeted in the same social engineering campaign that compromised Axios.
You can install this package via npm and yarn.
npm install ffsm
# or
yarn add ffsm
Using ffsm is easy. The default export is aptly called newStateMachine (but feel free to name it whatever you'd like). Simply import the constructor and define out your states as an object of key: Function pairs!
// stop-lights.js
import newStateMachine from 'ffsm';
const fsm = newStateMachine({
green: ({ states, transitionTo }) => {
console.log("green light!");
return transitionTo(states.yellow);
},
yellow: ({ states, transitionTo }) => {
console.log("yellow light!");
return transitionTo(states.red);
},
red: ({ states }) => {
console.log("red light!");
},
});
fsm.transitionTo(fsm.states.green);
// "green light!"
// "yellow light!"
// "red light!"
The classic traffic light state machine demonstrates the emphasis on simplicity for ffsm. The FSM moves to it's initial state with fsm.transitionTo(fsm.states.green), and then the internal handler is called. We destructure the state machine that's passed in, retrieving it's internal reference of states and the transitionTo function.
It's worth noting that transitionTo actually assigns a result to the relative state's state property, if one was given, otherwise it assings the optional payload passed to the handler.
A clear use for the state machine would be handling an HTTP Request. You might have some special logic to display a "success" or "error" based on the result of an http callback. ffsm allows you to define conditional state transitions as part of your handler.
// request-fsm.js
import newStateMachine from 'ffsm';
const fsm = newStateMachine({
send: ({ states, transitionTo }, uri) => {
try {
const response = await fetch(uri);
} catch (err) {
return transitionTo(states.error, err);
}
if (response.status >= 400) {
return transitionTo(states.fail, response);
}
return transitionTo(states.success, response);
},
fail: ({ states }, response) => {
console.log(`request failed with status code: ${response.status}`);
console.error(response.data);
return response;
},
success: ({ states }, response) => {
console.log('request succeeded!');
console.log(response.data);
return response;
},
error: ({ states }, error }) => {
console.log('something went wrong unexpectedly!');
console.error(error);
return error;
},
});
The above code is really easy to follow and understand. It has logical error handling, and it takes advantage of async/await while utilizing the strictly synchronous state machine. Not only that, this state machine is highly re-usable, since it makes HTTP requests for us.
To handle errors, we can simply call the state machine and check the resulting state:
// request-fsm.js
const result = fsm.transitionTo(fsm.states.send, '/hello-world');
if (!result.name === 'success') {
// handle error
} else {
// handle success
}
The result of the fsm is always the last executed state. This makes it easy for us to check the results, should we need to.
The returned state has a name key that matches the key of the handler. If you want to perform some additional validation checks, you can simply verify that key and do some extra handling. Though I'd recommend instead handling that logic within each state instead.
With the advent of great finite state machine (FSM) packages like Xstate and Machina JS, not to mention dozens of others, it's fair to question why I've created yet another FSM. ffsm was created because the public API's were too verbose and classical for my tastes. Don't get me wrong, xstate is a rock solid FSM, but it's API is not very pragmatic.
ffsm attempts to address that concern by providing an API which is function-first, allowing states to handle their own transitions internally. ffsm also tries to be different by keeping it's API very minimal and simple.
Due to the design of this FSM, there are some limitations.
states must be synchronous.state must handle it's own transitions.ffsm has no concept of a "start" state or an "end" state, and so you must be wary of infinite loops.The default export is the newStateMachine function.
// api.js
import newStateMachine from 'ffsm';
const fsm = newStateMachine({
STATE_NAME: ({ states, transitionTo }, payload) => {/* ... */},
});
As you can see, states are defined as the keys of the object, and their values are the transition functions called when moving to that state.
current allows you to retrieve the state that was last pushed onto the history stack.
// api.js
const state = fsm.current();
Note that if the history stack is empty, current will return undefined.
history returns a copy of the history stack for inspection purposes.
// api.js
const history = fsm.history();
History is displayed in chronological order, with the most recent being at the bottom. It's worth noting that all mutations are push-state, which means that transitionTo, undo and redo all push a new state onto the history stack, rather than attempting to splice the history array.
transitionTo accepts a state handler reference and an optional payload, then executes the handler function.
// api.js
fsm.transitionTo(fms.states.STATE_NAME, {someData: 'foo'});
transitionTo will validate that the handler reference passed in is one of the registered states within the state machine. This is what keeps the state machine finite.
transitionTo will also return the last state pushed onto the state stack after processing. This is possible because the state machine is synchronous, and fully performs it's work before returning.
// api.js
const state = fsm.transitionTo(fsm.states.STATE_NAME);
// do whatever with state ...
transitionTo is used both internally to switch between states and externally to declare the initial state. This, to me, feels very simple and clear.
undo steps back one referential state, and does not execute the handler.
// api.js
fsm.undo();
This can be useful when your next state depends on work done in the previous state. It's worth noting that undo will return the most recent state just like transitionTo.
redo steps forward one referential state, and does not execute the handler.
// api.js
fsm.redo();
This can be useful when you've stepped back a few states and now want to once-again step forward. Like above, rdo will return the most recent state just like transitionTo.
factoryStateMachine allows us to create single-use state machines more easily.
// example.js
import { factoryStateMachine } from 'ffsm';
const states = {
send: ({ states, transitionTo }, { method, url, data, headers }) => {
const send = async () => {
const h = {
'content-type': 'application/json',
};
return await fetch(url, {
method: method,
body: data,
headers: {
...h,
headers,
},
});
};
const res = send();
if (res.status >= 400) {
return transitionTo(states.error, {
request: { method, url, data, headers },
response: res,
});
}
return transitionTo(states.success, res);
},
error: (_, { request, response }) => {
console.error(`${response.status} error when sending HTTP request ${request.method}: ${request.url}`, request.data);
console.error('received response body: ', JSON.parse(response.data.body));
},
success: (_, res) => {
return JSON.parse(res.data.body);
},
};
export const requestFSM = (method, url, data, headers) => (
factoryStateMachine(states, states.send, { method, url, data, headers })
);
// use it later..
import { requestFSM } from 'example';
const { fsm, result } = requestFSM('POST', '/my-hello-world-api', { name: 'Tony' });
// fsm is the state machine.
// result is the state that returned after the state machine executed
// in this case, we could access result.state and have the already
// parsed JSON payload to work with.
There's a lot of interesting things to unpack:
factoryStateMachine accepts in the states object, an initialState and an optional payload.factoryStateMachine always runs the state machine from the provided initial state immediately after execution.factoryStateMachine returns an object with the state machine under the fsm property and the last ran state under the result propertyMuch like the factory API, the ability to easily create and throw away Finite State Machines will encourage effective use of them. Where factories allowed for easier creation of re-usable state machines, "Fire and Forget" intends to encourage easier single-use state machines.
import { fireStateMachine } from 'ffsm';
const result = fireStateMachine({
start: ({ states, transitionTo }, payload) => transitionTo(states.middle, `start-${payload}-`),
middle: ({ states, transitionTo }, payload) => transitionTo(states.end, `middle-${payload}-`),
end: (_, payload) => `end-${payload}`,
}, 'foo');
console.log(result.state); // "start-foo-middle-food-end-foo"
There's a two important characteristics here. First and foremost, the initial state is simply the first one that is defined. This is to encourage fireStateMachine to be a fire-and-forget API. If you want to re-use the state machine, you should instead use factoryStateMachine.
Second, the state machine does not return the machine itself, it only returns the last executed state. You cannot inspect the state machine for details about it's state history; it's all thrown away instead.
Please see CONTRIBUTING for details.
Please see CHANGELOG for more information on what has changed recently.
Hi! I'm a developer living in Vancouver. If you wanna support me, consider following me on Twitter @TBPixel, or if you're super generous buying me a coffee :).
The MIT License (MIT). Please see License File for more information.
FAQs
A simple, functional finite state machine in JavaScript
We found that ffsm demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
Multiple high-impact npm maintainers confirm they have been targeted in the same social engineering campaign that compromised Axios.

Security News
Axios compromise traced to social engineering, showing how attacks on maintainers can bypass controls and expose the broader software supply chain.

Security News
Node.js has paused its bug bounty program after funding ended, removing payouts for vulnerability reports but keeping its security process unchanged.