Files
skills/docs/communication/emails/campaigns/src/state-machine/state-machine.md
2026-05-29 08:20:56 +01:00

2.9 KiB

state-machine

(no submodules)

Purpose

Strictly typed lifecycle state machine for campaigns. Owns the set of nine legal transitions between the seven CampaignStatus values and rejects every other combination with a typed CampaignsError. The state machine is consumed by CampaignEngine for every status-changing operation (schedule, send, pause, resume, cancel, complete, fail) and by UI layers that need to enumerate the available next-step actions for the user.

By centralising the transition rules here we keep the rest of the package free of conditional if (status === ...) ladders; new lifecycle events are added by appending to the TRANSITIONS table.

Features

  • Nine canonical transitions covering draft, scheduled, sending, paused, plus the three terminal states (completed, cancelled, failed).
  • transition(currentStatus, event) returns the next status or throws CampaignsError(INVALID_TRANSITION) with the offending {from, event} in context.
  • canTransition(currentStatus, event) boolean predicate for UI affordances.
  • getAvailableEvents(currentStatus) lists every legal event for the current status — terminal states return [].
  • Records a campaigns_transitions_total counter point per successful transition tagged with from, to, and event labels.

Usage: Basic

import { CampaignStateMachine } from "@suaveplan/campaigns";

const sm = new CampaignStateMachine();
const next = sm.transition("draft", "send"); // "sending"

Usage: Advanced

Drive UI buttons from the available-events list:

import { CampaignStateMachine, type CampaignEvent } from "@suaveplan/campaigns";

const sm = new CampaignStateMachine();
const available: CampaignEvent[] = sm.getAvailableEvents(campaign.status);
return available.map((event) => (
    <Button
        key={event}
        disabled={!sm.canTransition(campaign.status, event)}
        onClick={() => onEvent(event)}
    >
        {event}
    </Button>
));

API Reference

  • CampaignEvent — "schedule" | "send" | "start" | "cancel" | "pause" | "complete" | "fail" | "resume".
  • CampaignStateMachine.transition(currentStatus, event): CampaignStatus — throws CampaignsError with code CAMPAIGNS.INVALID_TRANSITION if the combination is illegal.
  • CampaignStateMachine.canTransition(currentStatus, event): boolean.
  • CampaignStateMachine.getAvailableEvents(currentStatus): CampaignEvent[].

Implementation Notes

The TRANSITIONS table is a readonly Transition[] — frozen at module load. Lookups are linear (n=9), which is faster in practice than a nested Record<Status, Record<Event, Status>> lookup for this size. Adding a new event means appending to the array; removing one is a public API change and requires an OpenSpec proposal. The telemetry record is fired only on success, so the counter never double-counts a rejected transition.