Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/marco-ippolito/fiume

zero-dependency, lightweight finite state machine in Typescript
https://github.com/marco-ippolito/fiume

finite-state-machine fsm javascript typescript

Last synced: 2 days ago
JSON representation

zero-dependency, lightweight finite state machine in Typescript

Awesome Lists containing this project

README

        

# Fiume 🏞️

[![npm version](https://img.shields.io/npm/v/fiume)](https://www.npmjs.com/package/fiume)
[![build status](https://img.shields.io/github/actions/workflow/status/marco-ippolito/fiume/ci.yml)](https://github.com/marco-ippolito/fiume/actions)
[![biome](https://img.shields.io/badge/code%20style-biome-brightgreen.svg?style=flat)](https://biomejs.dev/)
![npm bundle size](https://img.shields.io/bundlephobia/minzip/fiume?label=Bundle%20Size&link=https://bundlephobia.com/package/fiume@latest)

**Fiume** is a zero-dependency, simple, and flexible state machine
library written in TypeScript.
It supports _Deterministic_ and partially _Non-Deterministic_
state machines.
It is compatible with all JavaScript runtimes and is designed to manage
the flow of a system through various states.
This library provides a lightweight and intuitive way to define states,
transitions, and hooks for state entry, exit, and transition events.

Unlike other libraries, **Fiume** does not require hardcoding state transitions.
Instead, you can write the transition logic inside the `transitionTo` function.

## Docs

You can find documentation and examples at [fiume.dev](https://fiume.marcoippolito.dev).

## Installation

```bash
npm install fiume
```

## Usage

```ts
import { StateMachine, State } from "fiume";

// Define a simple ON-OFF machine
const states: Array = [
{
id: "OFF",
initial: true,
transitionGuard: ({ event }) => event === 'button clicked',
transitionTo: () => "ON",
},
{
id: "ON",
transitionGuard: ({ event }) => event === 'button clicked',
transitionTo: () => "OFF",
},
];

// Create a state machine instance
const machine = StateMachine.from(states);

// Start the state machine
await machine.start();
console.log(machine.currentStateId); // OFF

// Trigger a transition by sending an event
await machine.send('button clicked');
console.log(machine.currentStateId); // ON

// Trigger another transition
await machine.send('button clicked');
console.log(machine.currentStateId); // OFF

// Trigger another transition
await machine.send('wrong event'); // wrong event wont trigger the transition
console.log(machine.currentStateId); // OFF

```

With `autoTransition` set to `true`, the machine does not wait
for the `send` method to trigger the transition to the next state:

```ts
import { StateMachine, State } from "fiume";

const states: Array = [
{
id: "OFF",
initial: true,
autoTransition: true,
transitionTo: () => "ON",
},
{
id: "ON",
final: true,
},
];

const machine = StateMachine.from(states);
await machine.start();
console.log(machine.currentStateId); // ON

```

You can define custom hooks `onEntry`, `onExit` and `onFinal`:

```ts

const states: Array = [
{
id: "OFF",
initial: true,
transitionTo: async ({ context, event, sharedData }) => "ON",
onEntry: async ({ context, event, sharedData }) => console.log(event.name, event.value),
onExit: async ({ context, event, sharedData }) => console.log(event.name, event.value),
},
{
id: "ON",
final: true,
transitionTo: ({ context, event, sharedData }) => "OFF",
onEntry: async ({ context, event, sharedData }) => console.log(event.name, event.value),
onExit: ({ context, event, sharedData }) => console.log(event.name, event.value),
onFinal: ({ context, event, sharedData }) => console.log(event.name, event.value),
},
];

type MyContext = { foo: string, bar: string }
type MyEvent = { name: string, value: number }

const machine = StateMachine.from(
states,
{ context: { foo: 'foo', bar: 'bar' }}
);

// Start the state machine
await machine.start();

await machine.send({ name: 'foo', value: 1 });
machine.currentStateId; // ON
await machine.send({ name: 'foo', value: 2 });
machine.currentStateId; // OFF

```

You can also `subscribe` to state transitions:

```ts

const states: Array = [
{
id: "ONE",
initial: true,
transitionTo: () => "TWO",
},
{
id: "TWO",
transitionTo: () => "THREE",
},
{ id: "THREE", final: true },
];

const machine = StateMachine.from(states);
await machine.start();

// Subscribe to state transitions
const subId = machine.subscribe(
({ context, currentStateId }) => console.log(currentStateId)
); // ONE, TWO
console.log(machine.currentStateId); // ONE
await machine.send();
console.log(machine.currentStateId); // TWO

// Unsubscribe the previous subscription
machine.unsubscribe(subId);

await machine.send();
console.log(machine.currentStateId); // THREE

```

## API

### StateMachine

#### Constructor

- `StateMachine.from`: A static function that returns a
new instance of the state machine. It takes the following parameters:
- `states`: An array of `State` objects representing the states of the state machine.
- `options` (optional): Configuration options for the state machine:
- `id` (string): The id of the machine,
- `context`: User-defined object.

> Don't add in `context` objects that cannot be copied,
like database connections, sockets, emitter, request, etc. Instead use `sharedData`!

- `sharedData`: User-defined object.

> Use `sharedData` to store database connection, sockets, request/response, etc.

Example:

```ts
import { StateMachine } from "fiume";
const machine = StateMachine.from(states, options);

```

- `StateMachine.fromSnapshot`: A static function that returns a new instance
of the state machine from an existing snapshot. It takes the following parameters:
- `snapshot`: The snapshot object produced by `machine.createSnapshot()`.
- `states`: An array of `State` objects representing the states of the state machine.
- `sharedData` (optional): User-defined object.

Example:

```ts
import { StateMachine } from "fiume";
const machine = StateMachine.from(states, options);
await machine.start();
const snapshot = machine.createSnapshot();
const refromSnapshot = StateMachine.fromSnapshot(snapshot, states);

```

#### Public Methods

- `start` (async): Initiates the state machine and
triggers the execution of the initial state.

- `send` (async): Send events to states that are not `autoTransition`.
If current state has `autoTransition: false`,
calling the `send` function is required to move to next state.
If the machine is in a final state and `isFinished` set to `true`,
using `send` will reject.

- `createSnapshot`: Returns a snapshot of the current machine
with the following properties:
- snapshotId (string): Id of the current snapshot.
- machineId: (string): Id of the machine.
- stateId: (string): Id of the current state when snapshot is taken.
- context: (TContext): User defined context

>`sharedData` will not be snapshotted!

- `subscribe`: You can register a callback that will be invoked
on every state transition between the `onEntry` and `onExit` hooks.
The callback returns the `subscriptionId` and receives `context`,
`event`, `sharedData`, and `currentStateId`.

- `unsubscribe`: Remove the subscription with the given `subscriptionId`.

#### Public properties

- `id` string: The id of the machine,
if not supplied in the constructor, will be a randomUUID.

- `currentStateId` string: The id of current state of the machine.

- `isFinished` boolean: True if the machine has finished in a final state.

- `context`: User defined context.

- `sharedData` (TSharedData): User defined data shared with the state machine.

> Do not add in `context`, objects that cannot be copied,
like database connections, `EventEmitter`, `Request`,
`Socket`, use `sharedData` instead!

### State

Represents a state in the state machine.

- `id`: (required) Unique identifier for the state.
- `transitionTo` (optional): Function or AsyncFunction that defines the
transition logic to move to another state, must return the id of the next state.
- `autoTransition` (optional): Boolean, if `true` the machine will transition to
the next state without waiting for an event. If set to `true`
is not possibile to use `transitionGuard`.
- `onEntry` (optional): Hook called when entering the state.
- `onExit` (optional): Hook called when exiting the state.
- `onFinal` (optional): Hook called when execution has ended in final state.
- `initial` (optional): Boolean indicating whether the state is the initial state,
there can only be one initial state.
- `final` (optional): Boolean indicating whether the state is a final state.
- `transitionGuard` (optional): Function or AsyncFunction takes as input
a user event and defines whether or not transition to the next state

## License

This library is licensed under the [Apache 2.0 License](LICENSE).
Feel free to use, modify, and distribute it as needed. Contributions are welcome!