Skip to main content

Calculator Example

A simple calculator demonstrating core Act patterns: state machines, multiple event types, conditional emit logic, invariants, and snapshotting.

Source: packages/calculator/src/

Patterns Demonstrated

Single State Machine

The calculator is a single state with left/right operands, an operator, and a result:

const State = z.object({
left: z.string().optional(),
right: z.string().optional(),
operator: z.enum(["+", "-", "*", "/"]).optional(),
result: z.number(),
});

const Calculator = state({ Calculator: State })
.init(() => ({ result: 0 }))
// ...

Multiple Event Types from One Action

A single PressKey action emits different events based on the key pressed:

.on({ PressKey: z.object({ key: z.enum(KEYS) }) })
.emit(({ key }, { state }) => {
if (key === ".") return ["DotPressed", {}];
if (key === "=") return [["EqualsPressed", {}]]; // array of tuples
return DIGITS.includes(key)
? ["DigitPressed", { digit: key }]
: ["OperatorPressed", { operator: key }];
})

Custom Patch Reducers

Each event type has its own reducer logic:

.patch({
DigitPressed: ({ data }, state) => append(state, data.digit),
OperatorPressed: ({ data }, state) => compute(state, data.operator),
DotPressed: (_, state) => {
const current = state.operator ? state.right : state.left;
if (current?.includes(".")) return {}; // no-op
return append(state, ".");
},
EqualsPressed: (_, state) => compute(state),
Cleared: () => ({ result: 0, left: undefined, right: undefined, operator: undefined }),
})

Invariants

The Clear action enforces that the calculator has state to clear:

.on({ Clear: ZodEmpty })
.given([{
description: "Must be dirty",
valid: (state) => !!state.left || !!state.right || !!state.result || !!state.operator,
}])
.emit(() => ["Cleared", {}])

Snapshotting

Snapshots are taken every 12 events for cold-start performance:

.snap((s) => s.patches > 12)

ZodEmpty

Events and actions with no payload use ZodEmpty:

import { ZodEmpty } from "@rotorsoft/act";

const Events = {
DotPressed: ZodEmpty,
EqualsPressed: ZodEmpty,
Cleared: ZodEmpty,
};

Running

pnpm dev:calculator
pnpm -F calculator test