Composition
actorSpec, definePart, and withParts slice one actor’s setup into
parts. Each part registers its own transitions and effects, in its own file,
under its own name. The actor composes them with a single call.
Parts are not child actors. They run in the same actor, share the same
context, and dispatch through the same event loop. They reorganize setup,
not behavior.
1. Declare the actor spec
Section titled “1. Declare the actor spec”Options move out of new Actor into an actorSpec value. This becomes the
type anchor every part references.
import { state, event } from "@mantaq/core";import { actorSpec } from "@mantaq/sugar";
export const basicInfo = state("basicInfo")();export const payment = state("payment")();export const submitting = state("submitting")();export const success = state("success")().final();export const error = state("error")();
export const submitBasicInfo = event("submitBasicInfo")<{ email: string; name: string }>();export const submitPayment = event("submitPayment")<{ cardNumber: string }>();export const paymentOk = event("paymentOk")<{ orderId: string }>();export const paymentFail = event("paymentFail")<{ reason: string }>();export const submittingDone = event("submittingDone")();
export const checkout = actorSpec({ inputs: [submitBasicInfo, submitPayment], internal: [paymentOk, paymentFail, submittingDone], states: [basicInfo, payment, submitting, success, error], initial: basicInfo, context: {} as CheckoutContext,});actorSpec narrows state and event arrays to their literals, no as const
needed. Context stays mutable, part handlers read and write with no casts.
2. Define parts
Section titled “2. Define parts”definePart wraps a setup slice. The builder is the actor’s own. Import
the spec type-only as anchor, refs as values.
import { definePart } from "@mantaq/sugar";import type { checkout } from "../checkout.ts";import { basicInfo, shippingAddress, submitBasicInfo, chargeCard } from "../checkout.ts";
export const basicInfoPart = definePart<typeof checkout>((m) => { m.on(basicInfo, submitBasicInfo, (event, opts) => { const cur = opts.context.get(); cur.basicInfo = event.payload; opts.context.set(cur); return { state: shippingAddress }; });});Effects stay with their state:
export const submittingPart = definePart<typeof checkout>((m) => { m.effect(submitting, { name: "chargeCard", fn: (input) => { const s = input.context.get(); withPromise(chargeCard(s.paymentInfo!.cardNumber), input.signal, input.emit, { success: (orderId) => paymentOk.create({ orderId }), error: (reason) => paymentFail.create({ reason: String(reason) }), }); withTimeout(800, input, () => submittingDone.create()); }, }); m.on(submitting, paymentOk, (event, opts) => { /* ... */ }); m.on(submitting, paymentFail, () => ({ state: error })); m.on(submitting, submittingDone, () => ({ state: success }));});Cross-state handlers go in their own part too. Reach the actor through
opts.actor, no closure needed.
3. Compose
Section titled “3. Compose”import { withParts } from "@mantaq/sugar";import { checkout } from "../checkout.ts";import { basicInfoPart } from "./parts/basic-info.ts";import { submittingPart } from "./parts/submitting.ts";
export const checkoutActor = withParts(checkout, [basicInfoPart, submittingPart]);Pass clock, regions, or internalBudget through the spec the same way
as new Actor.
4. Mix with inline code
Section titled “4. Mix with inline code”use registers a part inside a hand-written setup:
const actor = new Actor({ ...checkout, clock: new VirtualClock(), setup: (m) => { use(m, basicInfoPart); use(m, submittingPart); m.effect(basicInfo, { name: "trackBasicInfoVisit", fn: (input) => { // an extra inline effect; parts and inline code share the same builder }, }); },});- All parts share one
typeof <spec>anchor. Wrong anchor = compile error. - Last handler wins for (state, event) pairs, regardless of registration source.
- Effects append. Multiple effects run on state entry.
onAnyhandlers run alongside specific state handlers, not replacing them.- No runtime indirection. Parts write to the same transition/effect maps.
When to use
Section titled “When to use”Few states, a few transitions: keep inline. When the setup body grows past one screen, split by concern: state files for state-local handlers, a file for cross-state handlers, a file for effects.