Skip to main content

Act Framework

The event-sourcing framework for TypeScript.

Most business apps can be modeled with just three primitives: Actions β†’ {State} ← Reactions. Act wires them together with Zod schemas, an immutable event log, and a built-in pipeline that turns reactions into observable workflows. Drop in Postgres for production, SQLite for embedded, or run in-memory for tests; no external message broker required.

What you get​

  • Simplicity β€” focus on state, actions, and reactions without boilerplate or code generation
  • Type safety β€” TypeScript and Zod for compile-time guarantees and runtime validation
  • Composability β€” build complex workflows by composing small, testable state machines
  • Auditability β€” every state change is an event, enabling time-travel, debugging, and compliance
  • Adaptability β€” swap storage backends, integrate external systems, scale from in-memory to production

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 }, state) => ({ count: state.count + data.amount }),
})
.on({ increment: z.object({ by: z.number() }) })
.emit((action) => ["Incremented", { amount: action.by }])
.build();

const app = act().withState(Counter).build();

const actor = { id: "user1", name: "User" };
await app.do("increment", { stream: "counter1", actor }, { by: 5 });
const snapshot = await app.load(Counter, "counter1");
console.log(snapshot.state.count); // 5

Core Concepts​

Actions β†’ State ← Reactions​

  1. Actions β€” commands that represent intent to change state
  2. State β€” domain entities modeled as immutable data with event-driven transitions
  3. Reactions β€” asynchronous responses to state changes that trigger workflows and integrations

Builders​

  • state() β€” define state machines with actions, events, invariants, and snapshots
  • projection() β€” read-model updaters that react to events (with optional .batch() for high-throughput replay)
  • slice() β€” vertical feature modules grouping states, projections, and scoped reactions
  • act() β€” orchestrator that composes states, slices, and projections into an application

Port/Adapter Pattern​

Infrastructure uses swappable adapters injected via log(), store(), and cache() port functions:

  • Logger β€” ConsoleLogger (default) or PinoLogger (@rotorsoft/act-pino)
  • Store β€” InMemoryStore (default), PostgresStore (@rotorsoft/act-pg), or SqliteStore (@rotorsoft/act-sqlite)
  • Cache β€” InMemoryCache (default, LRU) or custom adapters (e.g., Redis)
  • Disposal β€” dispose()() cleans up all registered adapters on shutdown

Event Processing​

  • Correlation β€” dynamic stream discovery via reaction resolvers
  • Drain β€” leasing-based reaction processing with dual-frontier strategy
  • Settle β€” debounced, non-blocking correlateβ†’drain loop for production
  • Time-travel β€” load() accepts an asOf filter to reconstruct historical state
  • Close the books β€” app.close() archives, tombstones, or restarts streams

Packages​

Core​

PackageDescription
@rotorsoft/actThe framework
@rotorsoft/act-pgPostgreSQL store adapter
@rotorsoft/act-sqliteSQLite (libSQL) store adapter
@rotorsoft/act-patchImmutable deep-merge patch utility

Supporting​

PackageDescription
@rotorsoft/act-sseServer-Sent Events for incremental state broadcast
@rotorsoft/act-pinoPino logger adapter
@rotorsoft/act-diagramSVG diagram generator

Requirements​

  • Node.js >= 22.18.0
  • pnpm >= 10.32.1

FAQ​

Q: Do I need to use Postgres? No. Start with the in-memory store and switch to Postgres or another backend when needed.

Q: Is Act only for DDD experts? No. Act is designed to be approachable for all TypeScript developers, with a focus on simplicity and strong typing.

License​

MIT