Skip to main content

@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/builtin ships 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. Includes msTimestamp, blockHeight, etc.
  • delegate-wallet helpers - 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

Runnable: test/examples.test.ts.