first commit
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
# state-machine
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## 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
|
||||
|
||||
```ts
|
||||
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:
|
||||
|
||||
```ts
|
||||
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.
|
||||
Reference in New Issue
Block a user