Appearance
@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 ./srctext
[fsm-gen] gen: okRun 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
| File | Content |
|---|---|
src/fsm.js, src/fsm.d.ts | the shared module fsm: the frozen object Code (Conflict: 'E_CONFLICT' and so on) and the class Error |
src/order.js, src/order.d.ts | one 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.ts | the 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):
| Function | Result |
|---|---|
initial(): State | the initial state |
code(state: State): number | the code of a state |
key(state: State) | the key of a state, such as 'draft' |
fromCode(value: number): State | the state of a code; a value that is not a safe integer, or no state, throws E_UNKNOWN_CODE |
isFinal(state: State): boolean | whether 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): State | the state that the event leads to; throws E_INVALID_TRANSITION when it has no edge |
apply(expected: State, event: Event, actual: State): Step | the 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 1Errors
The functions throw fsm.Error (src/fsm.js), a subclass of Error:
| Property | Value |
|---|---|
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) |
message | the message, which names the machine |
expected | the expected state code, or undefined when the error has none |
actual | the stored state code, or undefined |
event | the 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:
| Code | expected | actual | event |
|---|---|---|---|
E_CONFLICT | set | set | set |
E_INVALID_TRANSITION | set | undefined | set |
E_UNKNOWN_CODE, E_RESERVED_CODE | undefined | undefined | undefined |
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 rootThe 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.