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/builders/state-builder.ts:488

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โ€‹