Compare commits

...

5 commits

4 changed files with 84 additions and 58 deletions

View file

@ -3,50 +3,42 @@
[![ci](https://github.com/icetemple/it-fsm/actions/workflows/ci.yml/badge.svg)](https://github.com/icetemple/it-fsm/actions/workflows/ci.yml)
[![Coverage Status](https://coveralls.io/repos/github/icetemple/it-fsm/badge.svg?branch=master)](https://coveralls.io/github/icetemple/it-fsm?branch=master)
Simple finite state machine
Simple full-featured finite state machine for your project
### Installation
### Why it-fsm?
`npm install --save it-fsm`
- 🚯 333 LOC - 0 dependencies
- 🍒 Sophisticated object-oriented design
### Usage
### Getting started
```ts
import { StateMachineBuilder } from "it-fsm";
enum ProjectStatus {
Pending = "pending",
Active = "active",
Completed = "completed",
Archived = "archive",
}
const [locked, unlocked] = ['locked', 'unlocked'] as const;
const smbProject = new StateMachineBuilder()
.withStates(Object.values(ProjectStatus))
const sm = new StateMachineBuilder()
.withStates([locked, unlocked])
.withTransitions([
[ProjectStatus.Pending, [ProjectStatus.Active, ProjectStatus.Archived]],
[ProjectStatus.Active, [ProjectStatus.Completed]],
]);
async function main() {
const project1 = { id: 1, status: ProjectStatus.Pending };
const project2 = { id: 2, status: ProjectStatus.Completed };
// Build FSM with current project status
const smForProject1 = smbProject.build(project1.status);
const smForProject2 = smbProject.build(project2.status);
console.log(smForProject2.allowedTransitionStates()); // []
console.log(smForProject1.allowedTransitionStates()); // [active, archived]
await smForProject1.changeState(ProjectStatus.Active);
console.log(smForProject1.allowedTransitionStates()); // [completed]
await smForProject1.changeState(ProjectStatus.Completed);
console.log(smForProject1.allowedTransitionStates()); // []
}
main();
[locked, { coin: unlocked }],
[unlocked, { push: locked }],
])
.build(locked);
```
or with deno
```ts
import { StateMachineBuilder } from "https://raw.githubusercontent.com/icetemple/it-fsm/master/fsm.ts";
// ...
```
You can find the full example in the examples folder.
### Install
If you want to use it in Node.js or the browser, you may need to install it as follows
`npm install --save it-fsm`

30
examples/turnstile.ts Normal file
View file

@ -0,0 +1,30 @@
import { StateMachineBuilder } from "../fsm.ts";
const [locked, unlocked] = ["locked", "unlocked"] as const;
const smbTurnstile = new StateMachineBuilder()
.withStates([locked, unlocked])
.withTransitions([
[locked, { coin: unlocked }],
[unlocked, { push: locked }],
]);
async function main() {
const sm = smbTurnstile.build(locked);
function logCurrentState() {
console.log("current state", JSON.stringify(sm.currentState.name));
}
logCurrentState();
await sm.trigger("coin", {});
logCurrentState();
await sm.trigger("push", {});
logCurrentState();
await sm.trigger("push", {});
logCurrentState();
}
if (import.meta.main) {
main();
}

44
fsm.ts
View file

@ -1,23 +1,23 @@
type StateTransitions<Ctx, SN extends string> = WeakMap<
export type StateTransitions<Ctx, SN extends string> = WeakMap<
State<Ctx, SN>,
WeakSet<State<Ctx, SN>>
>;
type StateOrName<Ctx, SN extends string> =
export type StateOrName<Ctx, SN extends string> =
| State<Ctx, SN>
| SN;
type SourceTransitions<SN extends string> = Array<[SN, Array<SN>]>;
type SourceNamedTransitions<SN extends string> = Array<
export type SourceTransitions<SN extends string> = Array<[SN, Array<SN>]>;
export type SourceNamedTransitions<SN extends string> = Array<
[SN, Record<string, SN>]
>;
type SourceActions<SN extends string> = Record<string, Array<[SN, SN]>>;
export type SourceActions<SN extends string> = Record<string, Array<[SN, SN]>>;
export const _states = Symbol("states");
const _transitions = Symbol("transitions");
const _actions = Symbol("actions");
const _prevState = Symbol("previous state");
const _currState = Symbol("current state");
export const _transitions = Symbol("transitions");
export const _actions = Symbol("actions");
export const _prevState = Symbol("previous state");
export const _currState = Symbol("current state");
export class StateMachineBuilder<Ctx, SN extends string = string> {
[_states]: Map<SN, Events<Ctx, SN>>;
@ -123,12 +123,12 @@ export class StateMachineBuilder<Ctx, SN extends string = string> {
}
}
interface StateMachineOpts<Ctx, SN extends string> {
export interface StateMachineOpts<Ctx, SN extends string> {
transitions?: StateTransitions<Ctx, SN>;
actions?: Actions<Ctx, SN>;
}
type Actions<Ctx, SN extends string> = Map<
export type Actions<Ctx, SN extends string> = Map<
string,
Array<[State<Ctx, SN>, State<Ctx, SN>]>
>;
@ -219,10 +219,10 @@ export class StateMachine<Ctx, SN extends string = string> {
}
}
const _stateName = Symbol("state name");
const _stateEvents = Symbol("state events");
export const _stateName = Symbol("state name");
export const _stateEvents = Symbol("state events");
interface Events<Ctx, SN extends string> {
export interface Events<Ctx, SN extends string> {
beforeExit?(
fromState: State<Ctx, SN>,
toState: State<Ctx, SN>,
@ -274,42 +274,46 @@ export class State<Ctx, SN extends string = string> {
}
}
function validNormalizedState<Ctx, SN extends string>(
export function validNormalizedState<Ctx, SN extends string>(
states: State<Ctx, SN>[],
state: StateOrName<Ctx, SN>,
) {
return validState<Ctx, SN>(normalizeState(states, state));
}
function normalizeState<Ctx, SN extends string>(
export function normalizeState<Ctx, SN extends string>(
states: State<Ctx, SN>[],
state: StateOrName<Ctx, SN>,
): State<Ctx, SN> | undefined {
return isStr(state) ? stateFromName(states, state) : state;
}
function validStateFromName<Ctx, SN extends string>(
export function validStateFromName<Ctx, SN extends string>(
states: State<Ctx, SN>[],
name: SN,
) {
return validState<Ctx, SN>(stateFromName(states, name));
}
function stateFromName<Ctx, SN extends string>(
export function stateFromName<Ctx, SN extends string>(
states: State<Ctx, SN>[],
name: SN,
) {
return states.find((state) => state.name === name);
}
function validState<Ctx, SN extends string>(val: unknown): State<Ctx, SN> {
export function validState<Ctx, SN extends string>(
val: unknown,
): State<Ctx, SN> {
if (!isState<Ctx, SN>(val)) {
throw new TypeError("an instance of State class is expected");
}
return val;
}
function isState<Ctx, SN extends string>(val: unknown): val is State<Ctx, SN> {
export function isState<Ctx, SN extends string>(
val: unknown,
): val is State<Ctx, SN> {
return val instanceof State;
}

View file

@ -1,7 +1,7 @@
{
"compilerOptions": {
"allowJs": false,
"lib": ["es7"],
"lib": ["es2017"],
"declaration": true,
"module": "commonjs",
"moduleResolution": "node",