Compare commits
5 commits
8f3fd1337b
...
e4ef1a7c9f
Author | SHA1 | Date | |
---|---|---|---|
e4ef1a7c9f | |||
269291af04 | |||
9d14ed5e84 | |||
a91bc95bd5 | |||
b6d66fd906 |
4 changed files with 84 additions and 58 deletions
66
README.md
66
README.md
|
@ -3,50 +3,42 @@
|
||||||
[](https://github.com/icetemple/it-fsm/actions/workflows/ci.yml)
|
[](https://github.com/icetemple/it-fsm/actions/workflows/ci.yml)
|
||||||
[](https://coveralls.io/github/icetemple/it-fsm?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
|
```ts
|
||||||
import { StateMachineBuilder } from "it-fsm";
|
import { StateMachineBuilder } from "it-fsm";
|
||||||
|
|
||||||
enum ProjectStatus {
|
const [locked, unlocked] = ['locked', 'unlocked'] as const;
|
||||||
Pending = "pending",
|
|
||||||
Active = "active",
|
|
||||||
Completed = "completed",
|
|
||||||
Archived = "archive",
|
|
||||||
}
|
|
||||||
|
|
||||||
const smbProject = new StateMachineBuilder()
|
const sm = new StateMachineBuilder()
|
||||||
.withStates(Object.values(ProjectStatus))
|
.withStates([locked, unlocked])
|
||||||
.withTransitions([
|
.withTransitions([
|
||||||
[ProjectStatus.Pending, [ProjectStatus.Active, ProjectStatus.Archived]],
|
[locked, { coin: unlocked }],
|
||||||
[ProjectStatus.Active, [ProjectStatus.Completed]],
|
[unlocked, { push: locked }],
|
||||||
]);
|
])
|
||||||
|
.build(locked);
|
||||||
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();
|
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
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
30
examples/turnstile.ts
Normal 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
44
fsm.ts
|
@ -1,23 +1,23 @@
|
||||||
type StateTransitions<Ctx, SN extends string> = WeakMap<
|
export type StateTransitions<Ctx, SN extends string> = WeakMap<
|
||||||
State<Ctx, SN>,
|
State<Ctx, SN>,
|
||||||
WeakSet<State<Ctx, SN>>
|
WeakSet<State<Ctx, SN>>
|
||||||
>;
|
>;
|
||||||
|
|
||||||
type StateOrName<Ctx, SN extends string> =
|
export type StateOrName<Ctx, SN extends string> =
|
||||||
| State<Ctx, SN>
|
| State<Ctx, SN>
|
||||||
| SN;
|
| SN;
|
||||||
|
|
||||||
type SourceTransitions<SN extends string> = Array<[SN, Array<SN>]>;
|
export type SourceTransitions<SN extends string> = Array<[SN, Array<SN>]>;
|
||||||
type SourceNamedTransitions<SN extends string> = Array<
|
export type SourceNamedTransitions<SN extends string> = Array<
|
||||||
[SN, Record<string, SN>]
|
[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");
|
export const _states = Symbol("states");
|
||||||
const _transitions = Symbol("transitions");
|
export const _transitions = Symbol("transitions");
|
||||||
const _actions = Symbol("actions");
|
export const _actions = Symbol("actions");
|
||||||
const _prevState = Symbol("previous state");
|
export const _prevState = Symbol("previous state");
|
||||||
const _currState = Symbol("current state");
|
export const _currState = Symbol("current state");
|
||||||
|
|
||||||
export class StateMachineBuilder<Ctx, SN extends string = string> {
|
export class StateMachineBuilder<Ctx, SN extends string = string> {
|
||||||
[_states]: Map<SN, Events<Ctx, SN>>;
|
[_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>;
|
transitions?: StateTransitions<Ctx, SN>;
|
||||||
actions?: Actions<Ctx, SN>;
|
actions?: Actions<Ctx, SN>;
|
||||||
}
|
}
|
||||||
|
|
||||||
type Actions<Ctx, SN extends string> = Map<
|
export type Actions<Ctx, SN extends string> = Map<
|
||||||
string,
|
string,
|
||||||
Array<[State<Ctx, SN>, State<Ctx, SN>]>
|
Array<[State<Ctx, SN>, State<Ctx, SN>]>
|
||||||
>;
|
>;
|
||||||
|
@ -219,10 +219,10 @@ export class StateMachine<Ctx, SN extends string = string> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const _stateName = Symbol("state name");
|
export const _stateName = Symbol("state name");
|
||||||
const _stateEvents = Symbol("state events");
|
export const _stateEvents = Symbol("state events");
|
||||||
|
|
||||||
interface Events<Ctx, SN extends string> {
|
export interface Events<Ctx, SN extends string> {
|
||||||
beforeExit?(
|
beforeExit?(
|
||||||
fromState: State<Ctx, SN>,
|
fromState: State<Ctx, SN>,
|
||||||
toState: 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>[],
|
states: State<Ctx, SN>[],
|
||||||
state: StateOrName<Ctx, SN>,
|
state: StateOrName<Ctx, SN>,
|
||||||
) {
|
) {
|
||||||
return validState<Ctx, SN>(normalizeState(states, state));
|
return validState<Ctx, SN>(normalizeState(states, state));
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeState<Ctx, SN extends string>(
|
export function normalizeState<Ctx, SN extends string>(
|
||||||
states: State<Ctx, SN>[],
|
states: State<Ctx, SN>[],
|
||||||
state: StateOrName<Ctx, SN>,
|
state: StateOrName<Ctx, SN>,
|
||||||
): State<Ctx, SN> | undefined {
|
): State<Ctx, SN> | undefined {
|
||||||
return isStr(state) ? stateFromName(states, state) : state;
|
return isStr(state) ? stateFromName(states, state) : state;
|
||||||
}
|
}
|
||||||
|
|
||||||
function validStateFromName<Ctx, SN extends string>(
|
export function validStateFromName<Ctx, SN extends string>(
|
||||||
states: State<Ctx, SN>[],
|
states: State<Ctx, SN>[],
|
||||||
name: SN,
|
name: SN,
|
||||||
) {
|
) {
|
||||||
return validState<Ctx, SN>(stateFromName(states, name));
|
return validState<Ctx, SN>(stateFromName(states, name));
|
||||||
}
|
}
|
||||||
|
|
||||||
function stateFromName<Ctx, SN extends string>(
|
export function stateFromName<Ctx, SN extends string>(
|
||||||
states: State<Ctx, SN>[],
|
states: State<Ctx, SN>[],
|
||||||
name: SN,
|
name: SN,
|
||||||
) {
|
) {
|
||||||
return states.find((state) => state.name === name);
|
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)) {
|
if (!isState<Ctx, SN>(val)) {
|
||||||
throw new TypeError("an instance of State class is expected");
|
throw new TypeError("an instance of State class is expected");
|
||||||
}
|
}
|
||||||
return val;
|
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;
|
return val instanceof State;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"allowJs": false,
|
"allowJs": false,
|
||||||
"lib": ["es7"],
|
"lib": ["es2017"],
|
||||||
"declaration": true,
|
"declaration": true,
|
||||||
"module": "commonjs",
|
"module": "commonjs",
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
|
|
Loading…
Add table
Reference in a new issue