Skip to main content

state

@rotorsoft/act-root


@rotorsoft/act-root / act/src / state

Function: state()

state<TName, TState>(entry): StateBuilder<TState, TName>

Defined in: libs/act/src/state-builder.ts:456

Creates a new state definition with event sourcing capabilities.

States are the core building blocks of Act. Each state represents a consistency boundary (aggregate) that processes actions, emits events, and maintains its own state through event patches (reducers). States use event sourcing to maintain a complete audit trail and enable time-travel capabilities.

The state builder provides a fluent API for defining:

  1. Initial state via .init()
  2. Event types via .emits() — all events default to passthrough (({ data }) => data)
  3. Custom event reducers via .patch() (optional — only for events that need custom logic)
  4. Actions (commands) via .on().emit() — pass an event name string for passthrough
  5. Business rules (invariants) via .given()
  6. Snapshotting strategy via .snap()

Type Parameters

TName

TName extends string

TState

TState extends Schema

Zod schema type defining the shape of the state

Parameters

entry

StateEntry<TName, TState>

Single-key record mapping state name to Zod schema (e.g., { Counter: z.object({ count: z.number() }) })

Returns

StateBuilder<TState, TName>

A StateBuilder instance for fluent API configuration

Examples

import { 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({ // optional — only for events needing custom reducers
Incremented: ({ data }, state) => ({ count: state.count + data.amount })
})
.on({ increment: z.object({ by: z.number() }) })
.emit((action) => ["Incremented", { amount: action.by }])
.build();
const DigitBoard = state({ DigitBoard: z.object({ digit: z.string() }) })
.init(() => ({ digit: "" }))
.emits({ DigitCounted: z.object({ digit: z.string() }) })
// no .patch() — passthrough is the default (event data merges into state)
.on({ CountDigit: z.object({ digit: z.string() }) })
.emit("DigitCounted") // string passthrough — action payload becomes event data
.build();
const BankAccount = state({ BankAccount: z.object({
balance: z.number(),
currency: z.string(),
status: z.enum(["open", "closed"])
}) })
.init(() => ({ balance: 0, currency: "USD", status: "open" }))
.emits({
Deposited: z.object({ amount: z.number() }),
Withdrawn: z.object({ amount: z.number() }),
Closed: z.object({})
})
.patch({ // only override events needing custom logic
Deposited: ({ data }, state) => ({ balance: state.balance + data.amount }),
Withdrawn: ({ data }, state) => ({ balance: state.balance - data.amount }),
Closed: () => ({ status: "closed", balance: 0 })
})
.on({ deposit: z.object({ amount: z.number() }) })
.given([
(_, snap) => snap.state.status === "open" || "Account must be open"
])
.emit("Deposited") // passthrough — action payload { amount } becomes event data
.on({ withdraw: z.object({ amount: z.number() }) })
.given([
(_, snap) => snap.state.status === "open" || "Account must be open",
(_, snap, action) =>
snap.state.balance >= action.amount || "Insufficient funds"
])
.emit("Withdrawn")
.on({ close: z.object({}) })
.given([
(_, snap) => snap.state.status === "open" || "Already closed",
(_, snap) => snap.state.balance === 0 || "Balance must be zero"
])
.emit("Closed")
.build();
const User = state({ User: z.object({
name: z.string(),
email: z.string(),
loginCount: z.number()
}) })
.init((data) => ({ ...data, loginCount: 0 }))
.emits({
UserCreated: z.object({ name: z.string(), email: z.string() }),
UserLoggedIn: z.object({})
})
.patch({ // only override events needing custom logic
UserLoggedIn: (_, state) => ({ loginCount: state.loginCount + 1 })
})
// UserCreated uses passthrough — event data merges into state
.on({ createUser: z.object({ name: z.string(), email: z.string() }) })
.emit("UserCreated") // passthrough
.on({ login: z.object({}) })
.emit("UserLoggedIn")
.snap((snap) => snap.patches >= 10) // Snapshot every 10 events
.build();

See