@bemedev/fsf
v0.7.3
Published
A library for finite state functions
Downloads
85
Readme
Final State Functions
Never use "if" again. Prototype, test, and code. RED-GREEN-BLUE as Uncle BoB says
Introduction
State machines are a useful concept in computer science and programming, and are often used to model the behavior of systems. In this journey, I explore this new way of programming to answer questions like what state machines are, how they work, how can I implement them in my workflow.
A state machine is a mathematical model of computation that represents the behavior of a system as a sequence of states and transitions between those states. At any given time, a state machine is in a specific state, and when certain conditions are met, it can transition to a new state.
Simple Cute machine, image from XState
State machines can be implemented in a variety of ways, such as using a switch statement or a series of if-else statements. In other hand, state machines allow abstraction of methods/functions, guards (if-else) and developpers can implement after defining the state machine, the logic of the system. I used to say it's an industrial way to do programming in opposition to the craftmanship model.
The "XState" library is the best implementation of state machines. It goes a step further as they implement state charts, where you can have events, children state machines, parallel states for examples.
So I take inspiration of this library to create my own one only focus of create of synchronous function. It's the only missing thing inside this library.
I try my best to follow the syntax of XState, so you can use it can be used with the Stately Editor
Features
| | '@bemedev/fsf' | | --------------------------- | :----------------: | | Finite states | ✅ | | Initial state | ✅ | | Transitions (object) | ✅ | | Transitions (string target) | ✅ | | Delayed transitions | ❌ | | Eventless transitions | ✅ | | Nested states | ❌ | | Parallel states | ❌ | | History states | ❌ | | Final states | ✅ | | Context | ✅ | | Entry actions | ✅ | | Exit actions | ✅ | | Transition actions | ✅ | | Parameterized actions | ✅ | | Transition guards | ✅ | | Parameterized guards | ✅ | | Asynchronous | ❌ | | Spawned actors | ❌ |
NB: Only for sync functions
If you want to use statechart features such as nested states, parallel states, history states, activities, invoked services, delayed transitions, transient transitions, etc. please use XState.
Quick start
Installation
npm i @bemedev/fsf //or
yarn add @bemedev/fsf //or
pnpm add @bemedev/fsf
Usage (machine)
import { describe, expect, test } from 'vitest';
import { createLogic, interpret } from '@bemedev/fsf';
describe('#4: Complex, https query builder', () => {
type Context = {
apiKey?: string;
apiUrl?: string;
url?: string;
};
type Events = { products?: string[]; categories?: string[] };
const queryMachine = createLogic(
{
schema: {
context: {} as Context,
// Add null option to make arguments optionals
events: {} as Events | null,
data: {} as string,
},
context: {},
initial: 'preferences',
states: {
preferences: {
always: {
actions: ['setUrl', 'setApiKey', 'startUrl'],
target: 'categories',
},
},
categories: {
always: [
{
cond: 'hasCategories',
target: 'products',
actions: 'setCategories',
},
'products',
],
},
products: {
always: [
{
cond: 'hasProducts',
target: 'final',
actions: 'setProducts',
},
'final',
],
},
final: {
data: 'query',
},
},
},
{
strict: true,
actions: {
setApiKey: ctx => {
ctx.apiKey = '123';
},
setUrl: ctx => {
ctx.apiUrl = 'https://example.com';
},
startUrl: ctx => {
const { apiUrl, apiKey } = ctx;
ctx.url = `${apiUrl}?apikey=${apiKey}`;
},
setCategories: (ctx, { categories }) => {
const _categories = categories?.join(',');
ctx.url += `&categories=${_categories}`;
},
setProducts: (ctx, { products }) => {
const _products = products?.join(',');
ctx.url += `&categories=${_products}`;
},
},
guards: {
hasCategories: (_, { categories }) =>
!!categories && categories.length > 0,
hasProducts: (_, { products }) =>
!!products && products.length > 0,
},
datas: {
query: ctx => ctx.url,
},
},
);
const func = interpret(queryMachine);
test('#1: no args', () => {
// So here, arguments are optionals !
expect(func()).toBe('https://example.com?apikey=123');
});
test('#2: categories', () => {
expect(func({ categories: ['a', 'b'] })).toBe(
'https://example.com?apikey=123&categories=a,b',
);
});
test('#3: products', () => {
expect(func({ products: ['a', 'b'] })).toBe(
'https://example.com?apikey=123&categories=a,b',
);
});
test('#4: categories and products', () => {
expect(func({ products: ['a', 'b'], categories: ['c', 'd'] })).toBe(
'https://example.com?apikey=123&categories=c,d&categories=a,b',
);
});
});