@effectstream/sm
Package:
@effectstream/sm· Source
The state-machine DSL inside an EffectStream node. Define a typed grammar
of commands, register one generator per command, and Stm parses each
incoming batcher input, dispatches it to the right handler, and yields
SQL updates through the runtime.
- State-machine DSL: define a typed grammar, register one generator per command.
- Parses each batcher input, dispatches to the handler, yields SQL through the runtime.
@effectstream/sm/builtinships common on-chain event primitives (ERC-20/721/1155, Cardano, Midnight, ...).- DSL is directly testable in a pure-TS unit test, without a database.
Install
bun add @effectstream/sm
# or
npm install @effectstream/sm
Usage
This package pairs with @effectstream/runtime,
which drives a per-block loop that calls stm.processInput(...) for every
batcher subunit, collects the SQL yielded by your generators, and commits
it inside a per-block postgres transaction. You author the DSL here;
the runtime executes it.
The DSL is also directly testable in a pure-TS unit test (parse + dispatch
without a database) - see
primitives/src/evm-erc20/erc20-primitive.test.ts.
import { Stm } from "@effectstream/sm";
import { World } from "@effectstream/coroutine";
import { Type } from "@sinclair/typebox";
import { join, leave } from "./queries.ts"; // pgtyped queries
const grammar = {
join: [["user", Type.String()]] as const,
leave: [["user", Type.String()]] as const,
} as const;
const stm = new Stm(grammar);
stm.addStateTransition("join", function* ({ parsedInput, msTimestamp }) {
yield* World.resolve(join, { user: parsedInput.user, ts: msTimestamp });
});
stm.addStateTransition("leave", function* ({ parsedInput }) {
yield* World.resolve(leave, { user: parsedInput.user });
});
The runtime calls stm.processInput(input) for every batcher subunit; the
DSL parses against grammar, finds the right handler, and the generator
yields World.resolve(...) so the runtime can execute the pgtyped queries.
Inside EffectStream
Stm is the central piece a node author writes. The runtime's per-block
loop wires each user input through the corresponding Stm instance,
collects yielded SQL, and commits it inside the per-block transaction.
The built-in primitives package (@effectstream/sm/builtin) covers
common on-chain events - ERC-20/721/1155 transfers, Cardano transfers,
Midnight events, etc. - so you don't re-implement them.
Key exports
Stm<Grammar, Events>: the state machine..addStateTransition(prefix, handler),.processInput(input),.grammar,.fullJsonGrammar,.keyedJsonGrammar.ParamToData<Params>derives the typed argument shape from a grammar entry.BaseStfInput: input shape passed to every handler. IncludesmsTimestamp,blockHeight, etc.delegate-wallethelpers - account delegation primitives reused by built-ins.
MessageListener<Events, Params> is exported as the handler type but is
inferred at call sites rather than imported directly.
Subpath exports:
@effectstream/sm/builtin:PrimitiveTypeERC20,PrimitiveTypeERC721,PrimitiveTypeERC1155,PrimitiveTypeCardanoTransfer,PrimitiveTypeMidnightGeneric, and 20+ more chain-specific event tags.@effectstream/sm/grammar: the underlying grammar/parsing utilities, also re-exported from@effectstream/concise.
Examples
primitives/src/evm-erc20/erc20-primitive.test.ts- a real primitive's behavior unit-tested.- Game logic in
templates/dice/packages/node/shows the fullnew Stm(...).addStateTransition(...)pattern.
Runnable: test/examples.test.ts.