@pyreon/machine — API Reference
Generated from
machine'ssrc/manifest.ts— the same source that powersllms.txtand MCPget_api. Do not edit this page by hand; edit the manifest. For the conceptual guide, see machine.
Lightweight state machine library built on Pyreon signals. A machine is a constrained signal — it can only hold values from the configured state set and can only transition between them via named events. Guards enable conditional transitions with optional payloads. There is no built-in context or side-effect system — use existing Pyreon signals alongside the machine for data and effects. The machine reads like a signal and subscribes like one, making it natural in JSX and reactive scopes.
Features
createMachine({ initial, states }) — constrained signal with typed transitions
machine() reads state reactively like a signal
machine.send(event, payload?) — trigger a transition; returns the settled state
machine.matches(...states) — check current state against one or more values
machine.can(event, payload?) — predicts send() exactly (evaluates the guard, throw-safe)
Guards with optional payload for conditional transitions (throw → denied)
Eventless (always) transitions — transient/condition states that resolve synchronously
Final states — isFinal() + onDone() for terminal states
Lifecycle listeners — onEnter / onExit / onTransition / onDone
Complete example
A full, end-to-end usage of the package:
import { createMachine } from '@pyreon/machine'
// Define states and transitions — type-safe:
const machine = createMachine({
initial: 'idle',
states: {
idle: {
on: { FETCH: 'loading' },
},
loading: {
on: {
SUCCESS: 'done',
ERROR: 'error',
},
},
done: {},
error: {
on: { RETRY: 'loading' },
},
},
})
// Read state — works like a signal:
machine() // 'idle'
// Transition via events:
machine.send('FETCH') // → 'loading'
machine.send('SUCCESS') // → 'done'
// Query capabilities:
machine.matches('done') // true
machine.matches('idle', 'done') // true — matches any of the listed states
machine.can('RETRY') // false — not in 'error' state
machine.nextEvents() // [] — 'done' has no transitions
// Guards — conditional transitions:
const auth = createMachine({
initial: 'loggedOut',
states: {
loggedOut: {
on: {
LOGIN: {
target: 'loggedIn',
guard: (creds: { token: string }) => creds.token.length > 0,
},
},
},
loggedIn: {
on: { LOGOUT: 'loggedOut' },
},
},
})
auth.send('LOGIN', { token: 'abc' }) // guard passes → 'loggedIn'
auth.send('LOGIN', { token: '' }) // guard fails → stays 'loggedOut'
// Use in JSX — reactive:
const Status = () => (
<div>
{() => machine.matches('loading') && <Spinner />}
{() => machine.matches('error') && <ErrorMessage />}
{() => machine.matches('done') && <Results />}
</div>
)Exports
| Symbol | Kind | Summary |
|---|---|---|
createMachine | function | Create a reactive state machine. |
Eventless (always) transitions | function | A state may declare always transitions that fire SYNCHRONOUSLY the moment the state is entered (and for the initial st |
Machine.onExit / onEnter / onTransition / onDone | function | Lifecycle listeners. |
Final states (final / isFinal / onDone) | function | Mark a terminal state with final: true. |
Machine.matches / nextEvents / reset / dispose | function | The instance query + control surface (all reactive where noted). |
StateOf | type | The STATE union of a machine — accepts BOTH the machine INSTANCE (createMachine(...) return) and a raw config object ( |
EventOf | type | The EVENT union of a machine — instance or raw config (delegates to InferEvents, which unions every state's on keys; |
API
createMachine function
<S extends string, E extends string>(config: MachineConfig<S, E>) => Machine<S, E>Create a reactive state machine. The returned machine reads like a signal (machine() returns the current state string) and transitions via machine.send(event, payload?). States and events are type-safe — TypeScript infers the union from the config object. Guards enable conditional transitions with typed payloads. Beyond named events, states support eventless always transitions (transient/condition states that resolve synchronously), final: true terminal states (isFinal() + onDone()), and full lifecycle listeners (onEnter / onExit / onTransition / onDone). send(event, payload?) returns the settled state (after any always cascade), and can(event, payload?) predicts send exactly — both evaluate guards throw-safely (a guard that throws denies the transition rather than crashing). No built-in context or effects — use Pyreon signals and effect() alongside the machine for data and side effects.
Example
const traffic = createMachine({
initial: 'red',
states: {
red: { on: { NEXT: 'green' } },
green: { on: { NEXT: 'yellow' } },
yellow: { on: { NEXT: 'red' } },
},
})
traffic() // 'red' (reactive)
traffic.send('NEXT') // 'green'
traffic.matches('green') // true
traffic.can('NEXT') // trueCommon mistakes
Treating
machine.can(event)(no payload) as "is this event declared?" — it now PREDICTSsendexactly by evaluating the guard (throw-safe → denied), so a guarded event with no/invalid payload reportsfalseCalling
machine.set()— machines are constrained signals, they do not expose.set(). State changes only happen throughmachine.send(event)Using a machine for data storage — machines only hold the current state string. Use regular signals alongside the machine for associated data
Forgetting guard payloads —
machine.send("LOGIN")without the required payload silently fails the guard
See also: Machine · MachineConfig
Eventless (always) transitions function
states.<state>.always?: TransitionConfig | TransitionConfig[]A state may declare always transitions that fire SYNCHRONOUSLY the moment the state is entered (and for the initial state at creation / on reset()). The first unguarded entry — or the first whose guard passes — wins, then the new state's always is re-evaluated, cascading until none fire. Guards receive NO payload (the transition is eventless), so they read external signals instead. Use for transient / condition states — e.g. enter check, then branch to pass or fail based on a computed value, without an intermediate visible state. An always-loop (a state that always re-targets itself) throws after 1000 steps instead of hanging.
Example
const score = signal(0)
const m = createMachine({
initial: 'check',
states: {
// transient: resolves immediately to pass/fail based on the signal
check: { always: [{ target: 'pass', guard: () => score() >= 50 }, 'fail'] },
pass: {},
fail: {},
},
})
m() // 'pass' or 'fail' — 'check' is never observedCommon mistakes
Expecting an
alwaysguard to receive a payload — eventless transitions have none; read external signals in the guardA self-targeting unconditional
always({ always: "self" }) — infinite loop; throws after 1000 stepsPutting a catch-all FIRST in an
alwaysarray — order matters; the first matching entry wins, so list specific guarded targets before an unguarded fallback
See also: createMachine
Machine.onExit / onEnter / onTransition / onDone function
onExit(state, cb) | onEnter(state, cb) | onTransition(cb) | onDone(cb) => () => voidLifecycle listeners. On each transition they fire in state-chart order: onExit(from) (while the machine still reads from) → onTransition(from, to, event) → onEnter(to) (machine now reads to) → onDone(event) if to is a final state. Each returns an unsubscribe function; dispose() removes all. onExit pairs with onEnter for setup/teardown per state — e.g. start a timer on enter, clear it on exit (the idiomatic alternative to a built-in delayed transition).
Example
const m = createMachine({
initial: 'idle',
states: { idle: { on: { GO: 'busy' } }, busy: { on: { STOP: 'idle' } } },
})
m.onEnter('busy', () => { const id = setInterval(poll, 1000); cleanup = () => clearInterval(id) })
m.onExit('busy', () => cleanup())Common mistakes
Assuming
onExitfires AFTER the state changed — it fires while the machine still reads the state being left (state-chart exit-before-enter order)Using
onEnter/onExitfor derived data — listeners are for side effects; for data derived from state use acomputed()readingmachine()
See also: createMachine
Final states (final / isFinal / onDone) function
states.<state>.final?: boolean — machine.isFinal(): boolean — machine.onDone(cb)Mark a terminal state with final: true. machine.isFinal() reads reactively true while the machine is in any final state (use it in JSX / effects), and machine.onDone(cb) listeners fire whenever a final state is entered (by event OR by an always cascade), receiving the triggering event. Final states model "the machine is done" — e.g. a wizard's complete state or a fetch's terminal success/failure.
Example
const m = createMachine({
initial: 'active',
states: { active: { on: { FINISH: 'done' } }, done: { final: true } },
})
m.onDone((e) => console.log('finished via', e.type))
m.isFinal() // false
m.send('FINISH')
m.isFinal() // true → onDone firedCommon mistakes
Expecting a final state to block further
send()— Pyreon does not freeze final states; if a final state declaresontransitions they still fire. Omitonfor true terminals
See also: createMachine
Machine.matches / nextEvents / reset / dispose function
matches(...states: S[]) => boolean — nextEvents() => E[] — reset() => void — dispose() => voidThe instance query + control surface (all reactive where noted). matches(...states) — reactive; true when the current state is ANY of the given (a variadic OR: matches("loading", "error")). nextEvents() — reactive; the current state's DECLARED on event keys (does NOT evaluate guards and does NOT include eventless always). reset() — set the state back to initial and re-run the initial always cascade. dispose() — remove ALL lifecycle listeners (onEnter/onExit/onTransition/onDone) and clean up.
Example
m.matches('loading', 'error') // in loading OR error (reactive)
m.nextEvents() // ['FETCH', 'CANCEL'] — declared events from here
m.reset() // back to initial (+ its always cascade)
m.dispose() // drop all listenersCommon mistakes
matches("a", "b")is an OR, not an AND — it is true when the current state isaORb. A machine is in exactly one state, so an AND across two states is never true.Reading
nextEvents()as "events that would currently SUCCEED" — it returns the current state's DECLAREDonkeys WITHOUT evaluating guards, and excludes eventlessalways. Usecan(event, payload?)to test whether a specific event would actually transition.Expecting
reset()to land on the LITERALinitialwhen that state has analways— reset re-runs the initial cascade, so a transient initial resolves to its cascade target (never the transient state itself).Expecting
dispose()to stop or freeze the machine — it only removes listeners;send()still transitions the state afterward (now silently). Drop your references to let it GC.
See also: createMachine
StateOf type
type StateOf<M> // Machine<S, E> → S; raw config → InferStatesThe STATE union of a machine — accepts BOTH the machine INSTANCE (createMachine(...) return) and a raw config object (delegates to InferStates for configs), so you derive from whichever you hold. Type-only, zero runtime bytes.
Example
const light = createMachine({
initial: 'green',
states: { green: { on: { NEXT: 'yellow' } }, yellow: { on: { NEXT: 'red' } }, red: {} },
})
type LightState = StateOf<typeof light> // 'green' | 'yellow' | 'red'Common mistakes
Passing a config NOT declared
as const(or throughcreateMachine, which uses a const generic) — state names widen tostringand the union is lostStateOf<ReturnType<typeof createMachine>>gymnastics on a concrete machine —typeof lightis enough
See also: EventOf · InferStates · createMachine
EventOf type
type EventOf<M> // Machine<S, E> → E; raw config → InferEventsThe EVENT union of a machine — instance or raw config (delegates to InferEvents, which unions every state's on keys; states without on contribute nothing). Useful for typing event-dispatching wrappers: send(e: EventOf<typeof m>). Type-only, zero runtime bytes.
Example
type LightEvent = EventOf<typeof light> // 'NEXT'
function dispatch(e: EventOf<typeof light>) { light.send(e) }Common mistakes
Expecting per-STATE narrowing — the union covers ALL states' events;
machine.can(event)is the runtime per-state check
See also: StateOf · InferEvents · createMachine
Package-level notes
No context: Unlike XState, Pyreon machines have no built-in context. Use regular signals alongside the machine for associated data — the machine handles the state transitions, signals handle the data.
Guard failures are silent: If a guard returns false, the transition simply does not happen — no error is thrown, no event is emitted. Check
machine.can(event)before sending if you need to handle the rejection.
Signal compatibility: The machine reads like a signal (
machine()) and subscribes like one — it works ineffect(),computed(), and JSX expression thunks without special handling.
Eventless transitions resolve synchronously:
alwaystransitions fire during the SAMEsend()call (and at creation /reset()), cascading until none apply — a transient state is never observed bymachine()or by reactive readers. Guards receive no payload; read external signals. A self-loopingalwaysthrows after 1000 steps.
send returns state; can predicts it; guards are throw-safe:
send(event, payload?)returns the SETTLED state (after thealwayscascade), or the unchanged state for an unhandled event / rejected guard.can(event, payload?)predictssendexactly — it evaluates the guard. Guards are throw-safe: a guard that throws (e.g. reading a property of a missing payload) DENIES the transition rather than crashingsend/can.