English · Русский
The machjs library implements a Mealy state machine for TypeScript: you write a table of rules and get an object with dispatch, can and a bus of output events. The context belongs to the state, not to the machine; the same table is read by analysis, formatters and the inspector. The repository holds the library, the inspector, and the examples.
Guide · Examples · Inspector · Issues
A rule is a sentence of four words: FROM a state, ON an event, TO a state, and an optional EMIT on the way out. A turnstile is two rules:
FROM locked ON coin TO open EMIT opened
FROM open ON push TO locked
The same two rules as code. In the schema they are written as a table: state, event, the rules for that pair.
import { StateMachine } from "@evgkch/machjs";
import type { IState, IEvent, Merge } from "@evgkch/machjs";
type Q = IState<"locked" | "open">; // states
type Σ = Merge<IEvent<"coin"> | IEvent<"push">>; // input events
type Λ = IEvent<"opened">; // output events
const gate = new StateMachine<Q, Σ, Λ>(
{
locked: { coin: [{ to: "open", emit: "opened" }] },
open: { push: [{ to: "locked" }] },
},
{ type: "locked", context: undefined },
);
gate.rx.on("opened", () => console.log("the gate opens"));
gate.dispatch("push"); // UNHANDLED — locked has no rule for it
gate.dispatch("coin"); // OK — the transition, and opened on the way out
gate.state.type; // "open"No handler tests the phase: the event goes to dispatch, and what happens to it is written in the schema.
A full rule is seven words; everything but FROM, ON and TO is optional:
FROM <state> ON <event> [WHEN <guard>] TO <state> [WITH <context>] [EMIT <event> [BY <data>]]
The order of execution is WHEN → TO → WITH → EMIT → BY. BY works on the context already updated.
The same gate, but the fare is two coins. What was paid sits in the state's context, not in the machine:
FROM locked ON coin WHEN short TO locked WITH add
FROM locked ON coin TO open WITH add EMIT opened BY fare
FROM open ON push TO locked WITH clear
import { StateMachine } from "@evgkch/machjs";
import type { IState, IEvent, Merge } from "@evgkch/machjs";
type Paid = { paid: number };
type Q = Merge<IState<"locked", Paid> | IState<"open", Paid>>;
type Σ = Merge<IEvent<"coin"> | IEvent<"push">>;
type Λ = IEvent<"opened", Paid>;
const FARE = 2;
const short = (c: Paid) => c.paid + 1 < FARE; // WHEN: the guard
const add = (c: Paid) => ({ paid: c.paid + 1 }); // WITH: the target's context
const clear = () => ({ paid: 0 });
const fare = (c: Paid) => ({ paid: c.paid }); // BY: the output event's data
const gate = new StateMachine<Q, Σ, Λ>(
{
locked: {
coin: [
{ when: short, to: ["locked", add] },
{ to: ["open", add], emit: ["opened", fare] },
],
},
open: { push: [{ to: ["locked", clear] }] },
},
{ type: "locked", context: { paid: 0 } },
);
gate.rx.on("opened", ({ paid }) => console.log(`open, ${paid} paid`));
gate.dispatch("coin"); // locked → locked, paid = 1
gate.dispatch("coin"); // locked → open, paid = 2, and opened on the way out
gate.dispatch("push"); // open → locked, paid = 0short checks whether the sum after the next coin would still be below the fare. For the first coin the guard holds, and the first rule fires: the gate stays in locked. For the second coin the guard does not hold, and the next rule of the same cell is checked. It has no guard, and it opens the gate. An unconditional rule stands last in its cell: a rule after it can never fire, and validate from analysis finds it as dead-rule.
The rule text above is not an illustration. It is what toRules(gate.schema) from formatters prints, and what the inspector's editor reads.
Next: can runs the same check the next dispatch will run. That is what enables a control, with no phase test written by hand.
button.disabled = !gate.can("push").isOk();The guide has the rest.
| Directory | Package | What is in it |
|---|---|---|
packages/core |
@evgkch/machjs |
The machine, and the schema it is written as |
packages/inspector |
@evgkch/machjs-inspector |
Six widgets, two pages, the relay, the palette |
examples |
private | Four pages on the library and the widgets, a walkthrough each |
packages/ holds the published packages, and nothing else. analysis, formatters and debug are entry points of @evgkch/machjs, not packages of their own: the version is one.
| Document | What it covers |
|---|---|
| Guide · Руководство | The rule language, the schema, the verdicts, the graph, analysis |
| Inspector · Инспектор | The entry points, the widgets, the relay, the schema files |
| Examples · Примеры | The four pages and the shell they share |
| Contributing · Как работать | Building, the tests, releasing |