Minimal actor runtime for TypeScript with mailbox semantics, deferred effects, and inspection.
- Deno 1.42+
deno add jsr:@blazes/superstatepnpm add jsr:@blazes/superstate- Processes events sequentially (mailbox)
- Produces immutable snapshots
- Supports deferred effects after transitions
- Exposes inspection hooks for runtime events
- Define event types.
- Create transition logic.
- Create the actor.
- Subscribe to snapshots.
- Start the actor.
- Send events.
import { createActor, createTransition } from "jsr:@blazes/superstate";
import { assertEquals } from "jsr:@std/assert";
type CounterEvent =
| { type: "inc"; by?: number }
| { type: "reset" };
const logic = createTransition(
{ count: 0 },
(context, event: CounterEvent) => {
switch (event.type) {
case "inc":
return { count: context.count + (event.by ?? 1) };
case "reset":
return { count: 0 };
}
},
);
const actor = createActor(logic);
actor.start();
actor.send({ type: "inc" });
actor.send({ type: "inc", by: 2 });
assertEquals(actor.getSnapshot().context.count, 3);createTransition(initialContext, (context, event, scope) => nextContext)
- Updates context in response to events
- Returns logic compatible with
createActor
createActor(logic, options?)
- Runs logic and manages lifecycle
| Field | Type | Description |
|---|---|---|
| inspect | function | Receives runtime events |
A snapshot is the current actor state.
{
status: "active",
context: { count: 3 }
}
- Events are processed in order
- No reentrancy during transitions
- Deferred tasks run after transitions
- Transition errors set status to
"error" - Listener errors are isolated
- Subscription delivers current snapshot immediately
import { createActor, createTransition } from "jsr:@blazes/superstate";
import { assertEquals } from "jsr:@std/assert";
const logic = createTransition(
{ count: 0 },
(context, event: { type: "inc" }, scope) => {
const next = { count: context.count + 1 };
if (event.type === "inc" && next.count === 1) {
scope.defer(() => {
scope.self.send({ type: "inc" });
});
}
return next;
},
);
const actor = createActor(logic);
actor.start();
actor.send({ type: "inc" });
assertEquals(actor.getSnapshot().context.count, 2);Events are processed in order. Nested sends are queued and run after the current transition completes.
import { createActor, createTransition } from "jsr:@blazes/superstate";
import { assertEquals } from "jsr:@std/assert";
const seen: number[] = [];
const logic = createTransition(
{ count: 0 },
(context, event: { type: "inc" }, scope) => {
const next = { count: context.count + 1 };
seen.push(next.count);
if (next.count === 1) {
scope.self.send({ type: "inc" });
scope.self.send({ type: "inc" });
}
return next;
},
);
const actor = createActor(logic);
actor.start();
actor.send({ type: "inc" });
assertEquals(seen, [1, 2, 3]);import { createActor, createTransition } from "jsr:@blazes/superstate";
import { assertEquals } from "jsr:@std/assert";
const events: string[] = [];
const actor = createActor(
createTransition(
{ count: 0 },
(context, event: { type: "inc" }) => {
return { count: context.count + 1 };
},
),
{
inspect(event) {
events.push(event.type);
},
},
);
actor.start();
actor.send({ type: "inc" });
assertEquals(events, [
"@actor.start",
"@actor.event",
"@actor.transition",
]);Events:
@actor.start@actor.stop@actor.event@actor.transition@actor.error@actor.listener.error
Included:
createActorcreateTransition- mailbox processing
- deferred effects
- inspection
Not included:
- async actors
- child actors
- machine DSL
deno testpnpm dlx deno testsend()is ignored when status is not"active"- Errors during
subscribe()initial call are thrown - Errors during notification are reported via
inspect
MIT