@salilvnair/state-machine
v1.0.1
Published
Generic visual state machine library — ck8t card style, React Flow canvas, consumer-provided logic
Maintainers
Readme
@salilvnair/state-machine
Generic visual state machine library — a ck8t-styled React Flow canvas for building, running, and debugging event-driven workflows, with all persistence left to the host app.
Repository: https://github.com/salilvnair/state-machine
Built for Daakia's stateful mock server engine — a mock route can be gated by a real transition event from a workflow built here, so a mock's response changes based on prior calls — but the library itself has no Daakia-specific code baked in.
Table of contents
- Features
- Install
- Quick start
- Bringing your own persistence —
SMConsumerBase - The engine, without React
- Public API
- Styling
- TypeScript
- Local development
- Publishing to npm
- License
Features
- Visual canvas — React Flow based, ck8t card visual style, with triggers, states, conditions, functions, and terminals as draggable blocks
- Consumer-provided persistence — the library never talks to a database
directly; implement
SMConsumerBaseonce and every save/load/delete call is routed through your host app's own storage - React-free execution engine —
@salilvnair/state-machine/engineis a separate entry point with zero React/canvas imports, safe to run in a plain Node.js process (e.g. inside a mock server) to evaluate transitions against real events - Workflows panel + Inspector + Execution panel — the full authoring
experience (
StateMachineWorkspace) ships as one embeddable component - Built-in block library — trigger, state, condition, function, terminal — each with its own visual definition, tree-shakeable via named exports
- REST/SOAP starter examples —
REST_EXAMPLE/SOAP_EXAMPLEconfigs to seed a new workspace or a demo
Install
npm install @salilvnair/state-machine @salilvnair/dui @xyflow/react react react-dom zustand@salilvnair/dui, @xyflow/react, react, react-dom, and zustand are
peer dependencies — install the versions your app already uses; this avoids
shipping a second copy of React (or a second DUI theme context) inside the
library bundle.
Import the three stylesheets once, in your app's entry point (or wherever you mount the workspace):
import '@salilvnair/state-machine/src/style/tokens.css'
import '@salilvnair/state-machine/src/style/ck8t-blocks.css'
import '@salilvnair/state-machine/src/style/canvas.css'Quick start
import { StateMachineWorkspace, useSMWorkspaceStore } from '@salilvnair/state-machine'
import '@salilvnair/state-machine/src/style/tokens.css'
import '@salilvnair/state-machine/src/style/ck8t-blocks.css'
import '@salilvnair/state-machine/src/style/canvas.css'
// Once, at app startup — hydrates the workspace from your storage and wires
// every future save/delete call back to it. See SMConsumerBase below.
await useSMWorkspaceStore.getState().registerConsumer(new MySMConsumer())
export function WorkflowsPage() {
return (
<StateMachineWorkspace
onCopyWorkflowId={(machine) => navigator.clipboard.writeText(machine.id)}
onConnectWorkflow={(machine) => openConnectDialog(machine)}
/>
)
}That's the whole embeddable experience — canvas, side nav, workflows list, inspector, and execution/debug panel all come with it.
Bringing your own persistence — SMConsumerBase
The library never imports a database, fetch, or postMessage — you tell
it how to load and save by extending SMConsumerBase once:
import { SMConsumerBase } from '@salilvnair/state-machine'
class MySMConsumer extends SMConsumerBase {
async onLoadWorkspace() {
const [machines, folders, todos] = await Promise.all([
db.query('SELECT * FROM sm_machines'),
db.query('SELECT * FROM sm_folders'),
db.query('SELECT * FROM sm_todos'),
])
return { machines, folders, todos }
}
async onSaveMachine(machine) {
await db.upsert('sm_machines', machine)
}
async onDeleteMachine(id) {
await db.delete('sm_machines', { id })
}
async onManualSave(machine) {
// fired by the Save button in the topbar — use for an explicit,
// user-initiated save distinct from autosave-on-change
}
}registerConsumer() calls onLoadWorkspace() immediately to hydrate the
Zustand store, then wires every subsequent mutation to your onSave*/
onDelete* methods — the canvas, workflows panel, and todo list all read
from and write through the same store.
The engine, without React
@salilvnair/state-machine/engine is a separate build entry with no React,
@xyflow/react, or DUI in its import chain — safe to import from a plain
Node.js process (a mock server, a CLI, a test runner) to evaluate a saved
workflow's transitions against real events:
import { StateMachineEngine } from '@salilvnair/state-machine/engine'
const engine = new StateMachineEngine(workflowConfig)
const result = engine.send('ORDER_PLACED', { orderId: '123' })
// result.state, result.trace, ...This is what lets a stateful mock server evaluate a workflow's current state on every incoming request without pulling in a browser-only dependency graph.
Public API
Everything below is exported from the package root (@salilvnair/state-machine)
unless noted otherwise.
| Export | What it is |
|---|---|
| StateMachineWorkspace | The full embeddable canvas + side nav + inspector + execution panel |
| WorkflowsPanel, SideNav | Individual pieces of the workspace, for a custom layout |
| StateMachineCanvas | Just the React Flow canvas, no chrome |
| SMConsumerBase, ISMConsumer | The persistence contract — extend/implement to wire your storage |
| StateMachineEngine | The transition-evaluation engine (also available React-free via /engine) |
| useSMStore, useSMWorkspaceStore, useSMTabsStore, useSMTodoStore | The Zustand stores backing the canvas, workspace, open tabs, and todo list |
| BlockRegistry | Registry of block type → visual definition, for adding custom block types |
| triggerDefinition, stateDefinition, conditionDefinition, functionDefinition, terminalDefinition | The five built-in block definitions, individually importable for tree-shaking |
| configToGraph | Converts a plain StateMachineConfig into React Flow nodes/edges |
| REST_EXAMPLE, SOAP_EXAMPLE | Starter workflow configs |
Plus the full type surface: StateMachineConfig, StateDefinition,
TransitionDefinition, GuardFn, ActionFn, SMEvent, SMNodeData,
SMNodeType, SMNodeDefinition, SMBlockManifest, SMPortDefinition,
ExecutionStatus, TraceEntry, SMachine, SMachineFolder,
SMWorkspaceCallbacks, SMTab, SMTodoItem.
Styling
Three stylesheets ship from src/style/ rather than a single bundled CSS
file, so you can see exactly what each layer does:
| File | Contents |
|---|---|
| tokens.css | Design tokens + ck8t CSS-variable aliases |
| ck8t-blocks.css | The ck8t bs-* visual system the block cards use |
| canvas.css | Canvas, node, and edge layout styles |
Import all three — they're small and load once.
TypeScript
The package ships hand-generated .d.ts declarations built alongside the
JS output (vite-plugin-dts, rollupTypes: false) — every exported symbol
keeps its own declaration file rather than being flattened into one giant
type, so "go to definition" in your editor lands on the real source-shaped
file.
Local development
git clone https://github.com/salilvnair/state-machine.git
cd state-machine
npm install
npm run dev # standalone demo app at localhost:5173npm run build # builds the standalone demo app
npm run build:lib # builds the publishable library into dist/lib
npm run typecheck # tsc --noEmitPublishing to npm
This section is for library maintainers.
Prerequisites
- Node.js 18+ and npm 9+
- Publish access to the
@salilvnairscope:npm login
Step 1 — Bump the version
npm version patch # bug fix: 1.0.0 -> 1.0.1
npm version minor # new feature: 1.0.0 -> 1.1.0
npm version major # breaking: 1.0.0 -> 2.0.0Step 2 — Build
prepublishOnly runs this automatically on npm publish, but build first
to inspect the output:
npm run build:libVerify dist/lib contains (at minimum): index.mjs, index.d.ts,
engine.mjs, engine/index.d.ts, state-machine.css.
Step 3 — Verify what will be published
npm pack --dry-runOnly dist/, src/style/, README.md, LICENSE, and package.json
should appear — no src/canvas, src/store, etc. (those live in dist/lib
as compiled output + declarations instead).
Step 4 — Test the tarball locally
npm pack
# creates salilvnair-state-machine-X.Y.Z.tgz
# in a consumer app:
npm install /path/to/salilvnair-state-machine-X.Y.Z.tgzStep 5 — Publish
Scoped packages default to private, so --access public is required —
though publishConfig.access: "public" in package.json already sets this
as the default, so a plain npm publish is enough:
npm publish
# rehearsal first:
npm publish --dry-runStep 6 — Push tags
git push && git push --tagsLicense
MIT © Salil V Nair — see LICENSE.
