Skip to content

@polyspec/fsm-npm ​

한국어

The npm consumer of fsm (docs/plans/fsm-v1.md, C-2). The package is private and is a test bed: it is not published. It shows how the generated npm code of a fsm definition is used, and it checks that code against the conformance vectors. The generated code is ES modules with type declarations (.js and .d.ts) and has no runtime dependency.

Generate into your project ​

Run the generator with the project directory whose fsm.config.json names the store, and write into the source directory of your package. Your package.json needs "type": "module", because the generated files are ES modules:

sh
fsm-gen gen --project /path/to/order-machines --kind npm --out ./src
text
[fsm-gen] gen: ok

Run the command again after every change of the definition. The vectors come from fsm-gen vectors --kind npm --out <file>, written into a test file of your own (the test bed writes tests/vectors.mjs).

Generated files ​

FileContent
src/fsm.js, src/fsm.d.tsthe shared module fsm: the frozen object Code (Conflict: 'E_CONFLICT' and so on) and the class Error
src/order.js, src/order.d.tsone module per machine: the frozen objects State (the value of each member is its code) and Event (its position, from 1), the Step type and the functions
src/index.js, src/index.d.tsthe entry point: export * as order from './order.js' and export * as fsm from './fsm.js'

Each file starts with // Code generated by fsm-gen from <machine>; DO NOT EDIT..

API ​

The entry point exports one namespace per machine and the shared fsm namespace:

js
import { order, fsm } from './src/index.js';

The functions of order (src/order.d.ts):

FunctionResult
initial(): Statethe initial state
code(state: State): numberthe code of a state
key(state: State)the key of a state, such as 'draft'
fromCode(value: number): Statethe state of a code; a value that is not a safe integer, or no state, throws E_UNKNOWN_CODE
isFinal(state: State): booleanwhether a state is final
eventsFrom(state: State): Event[]the events that have an edge from a state, in declaration order
target(expected: State, event: Event): Statethe state that the event leads to; throws E_INVALID_TRANSITION when it has no edge
apply(expected: State, event: Event, actual: State): Stepthe result of one attempt (see the root README, Transitions)

State.Draft is 1, Event.Next is 1, and so on; Step is { to, changed }.

The persistence example examples/persist.mjs holds the complete apply use inside a transaction; it is the pattern to copy (see Persistence). The pure call for a value that is not in a database:

js
import { order, fsm } from './src/index.js';

const step = order.apply(order.State.Draft, order.Event.Next, order.State.Draft);
console.log(step);
try {
  order.apply(order.State.Draft, order.Event.Next, order.State.Shipped);
} catch (error) {
  console.log(error instanceof fsm.Error, error.code, error.expected, error.actual, error.event);
}

This program was run in a temporary consumer project with the generated files of this checkout ("type": "module"). Its output:

text
{ to: 2, changed: true }
true E_CONFLICT 1 3 1

Errors ​

The functions throw fsm.Error (src/fsm.js), a subclass of Error:

PropertyValue
code'E_CONFLICT', 'E_INVALID_TRANSITION', 'E_UNKNOWN_CODE' or 'E_RESERVED_CODE'; the same strings are the members of fsm.Code (Conflict, InvalidTransition, UnknownCode, ReservedCode)
messagethe message, which names the machine
expectedthe expected state code, or undefined when the error has none
actualthe stored state code, or undefined
eventthe event position (from 1), or undefined

Check it with error instanceof fsm.Error and read error.code.

Which properties a code sets is the rule of spec B-6 (docs/spec/fsm.md), and the vectors of tests/vectors.mjs assert it:

Codeexpectedactualevent
E_CONFLICTsetsetset
E_INVALID_TRANSITIONsetundefinedset
E_UNKNOWN_CODE, E_RESERVED_CODEundefinedundefinedundefined

The message names the machine and, for E_UNKNOWN_CODE, holds the value (order: 99 is not a state of order).

Persistence ​

examples/persist.mjs is the pattern of docs/spec/fsm.md, B-4 on SQLite with polyspec orm. The stored state is a code in a row. A transition runs in one transaction: the row is read under its lock (forUpdate), the event is applied to the state that the caller observed, and the row is updated only when the result is changed:

js
// packages/js/examples/persist.mjs, transition
export async function transition(db, machine, id, expected, event) {
  return db.transaction(async () => {
    const row = await new Item().connect(db).id(id).forUpdate().get();
    const step = machine.apply(expected, event, machine.fromCode(row.getStatus()));
    if (step.changed) await row.setStatus(machine.code(step.to)).update();
    return step;
  });
}

machine is the generated module of the machine (./src/order.js), so the same function serves every machine. An error of apply is thrown inside the transaction, and the transaction rolls back. A retry with the same expected returns changed: false and writes nothing. open, seed and statusOf in the same file create the tables and read a row. The model Item is generated by orm-gen from contracts/schema/persist.dbs into examples/model/.

Tests ​

From the checkout:

sh
make -C packages/js install   # npm ci: the pinned orm release and the TypeScript compiler
make -C packages/js test      # tests/conformance.test.mjs through kit's run-tests
make test-npm                      # the same from the root

The run passes 89 of 89 cases. tests/conformance.test.mjs runs one test per vector case of tests/vectors.mjs, named by its case name <feature>__<id>; the persist cases run examples/persist.mjs on the dialect of FSM_TEST_DIALECT, SQLite by default. tests/persist.test.mjs runs two transitions of one row at the same time and the deadlock retry (make npm-persist-test). The root tests/npm-types checks the .d.ts files with the TypeScript compiler (make npm-types). The root make gen-check checks that the generated files are the ones fsm-gen writes today.