@yaebal/state-machine
v0.0.2
Published
yaebal state-machine plugin — declarative finite-state machines: typed events, guarded transitions, onEnter/onLeave hooks.
Maintainers
Readme
@yaebal/state-machine
a declarative finite-state machine backed by @yaebal/sklad storage: typed events,
guarded transitions, onEnter/onLeave hooks. unlike @yaebal/scenes, there are
no steps and no explicit enter() — a key's machine is always active, starting at initial the
first time it's seen, and moves only when a typed event you send matches a transition declared
for the current state.
install
pnpm add @yaebal/state-machineusage
import { defineMachine, stateMachine } from "@yaebal/state-machine";
import { createBot, type Context } from "yaebal";
type OrderEvent = { type: "PAY" } | { type: "SHIP" } | { type: "CANCEL" };
const order = defineMachine<Context, OrderEvent, { paidAt?: number }>({
initial: "created",
states: {
created: {
on: {
PAY: {
target: "paid",
actions: (ctx) => {
ctx.machine.context.paidAt = Date.now();
},
},
CANCEL: { target: "cancelled" },
},
},
paid: {
onEnter: (ctx) => ctx.send("payment received — shipping soon"),
on: {
SHIP: { target: "shipped" },
CANCEL: { target: "cancelled", guard: (ctx) => ctx.machine.context.paidAt !== undefined },
},
},
shipped: {
onEnter: (ctx) => ctx.send("your order shipped 📦"),
},
cancelled: {
onEnter: (ctx) => ctx.send("order cancelled"),
},
},
});
const bot = createBot(token).install(stateMachine(order));
bot.command("pay", (ctx) => ctx.machine.send({ type: "PAY" }));
bot.command("ship", (ctx) => ctx.machine.send({ type: "SHIP" }));
bot.command("status", (ctx) => ctx.reply(`order is ${ctx.machine.state}`));behavior
- always active — every key starts in
initialthe moment it's first seen; there is noenter()/leave()lifecycle like@yaebal/scenes.onEnterfires on that first activation too, withinfo.from === undefined. - typed events —
onis keyed by your event union'stypefield, soctx.machine.send(...)only accepts events the current state's type signature allows, and guards/actions receive the narrowed event. - guarded transitions — a state can declare several transitions for the same event as an
array; they're tried in order, and a
guardreturningfalseskips to the next candidate.send()resolvestrueonly if a transition actually fired. - extended state —
ctx.machine.contextis a plain, mutable, json-serializable bag (built bydef.context(ctx)), persisted after every transition alongside the current state name. - durable snapshots — state + context persist as one json snapshot per
chat:userkey in anyStorageAdapter(see@yaebal/sklad); restarts resume a key in the same state.ttlresets an inactive machine toinitiallazily; snapshots pointing at a state a deploy removed self-heal instead of shadowing the key. - hooks —
onEnter(ctx, { from, event })/onLeave(ctx, { to, event })fire on every transition into/out of the state they're declared on.
options
stateMachine(order, {
storage: myStorageAdapter, // defaults to in-memory (lost on restart)
getKey: (ctx) => ctx.chat?.id?.toString(), // defaults to `chat.id:from.id`
ttl: 24 * 60 * 60 * 1000, // reset an inactive machine after a day
});part of yaebal — a type-safe, runtime-agnostic Telegram Bot API framework. MIT.
