Skip to content

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.

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.

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.

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.

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.
  • onAny handlers run alongside specific state handlers, not replacing them.
  • No runtime indirection. Parts write to the same transition/effect maps.

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.