core
The engine: createMachine, assign, toPath, toValue.
A small finite state machine for JavaScript, shaped after SCXML. For multi-step forms: flows where the next question depends on the answers already given, where the user leaves halfway through, and where the flow gains a step while they are away.
Dependencies in the engine
Line, branch and function coverage
The shape it follows
Nothing anywhere records "the user is on step 3". The
position is a pure function of the answers, recomputed from
the initial state on every start(). Guards read
the data.
Load the row into context, start, land on the
first gap. No cursor to load, because there is no cursor to
store.
A later state is unreachable until its predecessors' guards pass. There is nothing to forge in a URL, because the position never comes from the URL.
Add a state and every already-"complete" record re-opens at it. Turning on a policy that inserts a step is how you reach users who finished last year.
A section is a self-contained machine that ends in a final state and names no successor. onFinal chains sections, and it belongs to the
composing flow — so the same definition drives the wizard and
the settings page.
The node id is the URL path. route(segments) gives a step page its redirect target and its Back link off
one replay of the trail.
A two-question signup, resumed from a database row:
import { createMachine, assign } from '@1state/core'
const signup = {
context: { email: null, password: null },
initial: 'email',
states: {
email: {
on: { answer: { target: 'email', actions: assign({ email: (_ctx, e) => e.value }) } },
go: [{ target: 'password', cond: (ctx) => ctx.email !== null }],
},
password: {
on: { answer: { target: 'password', actions: assign({ password: (_ctx, e) => e.value }) } },
go: [{ target: 'done', cond: (ctx) => ctx.password !== null }],
},
done: { type: 'final' },
},
}
const machine = createMachine(signup, { context: await load(accountId) })
await machine.start() // exactly where they left offThree of them. No dependencies in the engine, ESM only, Node 20+.
Deliberately. There is no machine state to persist — the position is derived from the answers, and the answers are your row. Persist the row, which you were going to do anyway, and you are done.
Why