@rotorsoft/act
v1.25.0
Published
act core library
Maintainers
Readme
@rotorsoft/act
Event-sourcing framework for TypeScript — three primitives, Zod end to end, no broker required.
Why this package
This is the framework core: the builders (state, slice, projection, act), the port interfaces (Store, Cache, Logger) with bundled in-memory implementations, the orchestrator that runs the correlate → drain loop, and the snapshot/cache layer that keeps load() fast on long streams. Around three primitives — actions (the changes you want to make), state (the data you care about), and reactions (what happens as a result) — it provides input validation against Zod schemas, optimistic-concurrency commit, derived state via patch reducers, fan-out reactions with backoff and dead-lettering, blocked-stream recovery, time-travel queries against the same log.
Your domain stays in TypeScript; your schemas stay in Zod. Pick a store at bootstrap — Postgres (@rotorsoft/act-pg), SQLite (@rotorsoft/act-sqlite), or the bundled in-memory default — and the application code stays the same. The published surface is stable under SemVer as of 1.0.0.
For the project-level overview, see the root README.
Installation
pnpm add @rotorsoft/actFor production, also install one of the durable stores: @rotorsoft/act-pg (Postgres) or @rotorsoft/act-sqlite (SQLite). The bundled InMemoryStore is used by default and is intended for development and tests.
Quick start
import { act, state } from "@rotorsoft/act";
import { z } from "zod";
const Counter = state({ Counter: z.object({ count: z.number() }) })
.init(() => ({ count: 0 }))
.emits({ Incremented: z.object({ amount: z.number() }) })
.patch({ Incremented: ({ data }, s) => ({ count: s.count + data.amount }) })
.on({ increment: z.object({ by: z.number() }) })
.emit((action) => ["Incremented", { amount: action.by }])
.build();
const app = act().withState(Counter).build();
await app.do("increment", { stream: "counter1", actor: { id: "1", name: "u" } }, { by: 5 });
const snap = await app.load(Counter, "counter1");
console.log(snap.state); // { count: 5 }Define state, declare actions, dispatch, load. Everything else — projections, reactions, slices, cross-process drain, time-travel — is more of the same builder calls.
API
Top-level exports:
- Builders —
state(),slice(),projection(),act()build the domain.withState,withSlice,withProjectioncompose them. - Ports —
store(),cache(),log()are first-call-wins singletons; pass an adapter on first call to override the default.dispose()registers shutdown callbacks. Actorchestrator —do,load,query,query_array,query_streams,query_stats,drain,settle,correlate,reset,unblock,blocked_streams,closeplus lifecycle events (committed,notified,settled,blocked,closed, …).- Errors —
ValidationError,InvariantError,ConcurrencyError,StreamClosedError,NonRetryableErrorplus theErrorsconstants for string-matching. - In-memory adapters —
InMemoryStore,InMemoryCache,ConsoleLogger. - Constants —
SNAP_EVENT,TOMBSTONE_EVENT. - Types — full re-export of port interfaces, builder result types, lifecycle event payloads.
Full type reference: typedoc.
Common patterns
Slices and projections
slice() groups partial state with scoped reactions (vertical-slice architecture); projection() builds read-model updaters. Compose with .withSlice() / .withProjection():
import { projection, slice } from "@rotorsoft/act";
// Projection — read-model updater. Handlers receive (event, stream).
const CounterProjection = projection("counters")
.on({ Incremented: z.object({ amount: z.number() }) })
.do(async ({ stream, data }) => { /* update read model */ })
.build();
// Slice — partial state + scoped reactions. Handlers receive (event, stream, app).
const CounterSlice = slice()
.withState(Counter)
.withProjection(CounterProjection)
.on("Incremented")
.do(async (event, _stream, app) => { /* dispatch via app */ })
.to("counter-target")
.build();
const app = act().withSlice(CounterSlice).build();Standalone projections (cross-slice events) work at the act() level via .withProjection().
Lifecycle wiring at bootstrap
import { dispose, log, store } from "@rotorsoft/act";
import { PostgresStore } from "@rotorsoft/act-pg";
store(new PostgresStore({ /* … */ }));
await store().seed();
app.on("committed", () => app.settle()); // drain reactions on every commit
app.on("blocked", (xs) => log().error({ xs })); // page on blocked streams
dispose(async () => { /* your cleanup */ }); // wired into SIGINT/SIGTERMSee the production checklist for the full pre-deploy walkthrough.
Time-travel
await app.load(Counter, "counter1", undefined, { before: 5000 }); // state at event id
await app.load(Counter, "counter1", undefined, { created_before: someDate }); // state at timestampSame load() as everything else. The third parameter is a step-through callback that receives each intermediate snapshot during replay.
Recovery loop (operating Act)
When a reaction handler fails past its retry budget (or throws NonRetryableError), the stream is blocked and stays out of claim() results. Operators:
const blocked = await app.blocked_streams();
// Inspect, fix the underlying cause, then:
await app.unblock(["webhooks-out-customer-42"]);
await app.unblock({ stream: "^webhooks-out-" }); // bulkunblock resumes from where the stream stopped — it does not replay history. Use app.reset(...) only for projection rebuilds.
Compatibility
- Node: >=22.18.0
- Peer:
zod^4.4.3 - Bundled deps:
@rotorsoft/act-patch(state reducer) - Module formats: ESM + CJS
- TypeScript: strict mode recommended for full inference
Stability
Public API governed by the Act Stability Charter. The charter names exactly which surfaces are protected by SemVer (builders, Act interface, port interfaces, lifecycle event shapes, public type exports) and what's free to evolve (internal modules, performance characteristics, log formats). Breaking changes require a BREAKING CHANGE: commit footer and a written migration note. Charter is in effect as of 1.0.0; the milestone tracker is milestone 1.0.
Related packages
- @rotorsoft/act-pg — PostgreSQL store. Production default.
- @rotorsoft/act-sqlite — SQLite store. Single-node / edge.
- @rotorsoft/act-http —
webhookfor outbound POST from reactions;/ssesubpath for incremental state broadcast. - @rotorsoft/act-pino — pino logger adapter.
- @rotorsoft/act-patch — immutable deep-merge patch utility used by state reducers.
- @rotorsoft/act-tck — conformance suite for
Store/Cache/Loggeradapters. - @rotorsoft/act-diagram — interactive SVG diagram of the domain model +
actCLI.
Documentation
- Get started — 5-minute walkthrough.
- Concepts — state management, event sourcing, error handling, real-time, testing, configuration.
- Architecture — concurrency model, cache + snapshots, correlation + drain, cross-process reactions, priority lanes, close-cycle, schema evolution, extension points.
- Guides — production checklist, projections to database, external integration, writing a custom store/cache/logger, contributing a new package, contracts CLI.
- PERFORMANCE.md — measured throughput numbers, optimization history, and the reaction-latency benchmark answering "how long from
do()to reaction firing?" - BENCH.md — index of every benchmark in the workspace with run commands.
License
MIT
