3.8 KiB
events
(no submodules)
Purpose
The saga orchestrator publishes every step + instance transition onto
the host's EventBus<SagaEventMap>. Sibling plugins use those events
to drive UI updates, audit, and observability. This module pins both
the type-level SagaEventMap shape (so a misspelt event name is a
compile error at every subscribe site) and the Zod schemas every emit
goes through (so an internal bug in the orchestrator cannot produce a
wire-corrupt payload).
The SagaEventEmitter bundles those validations into eight typed
methods — one per event — that the orchestrator calls instead of
hand-writing bus.emit(...) calls scattered across modules.
Features
- Typed
SagaEventMapcovering every step lifecycle event (saga.step.requested,.completed,.failed,.compensated) and every instance lifecycle event (.started,.completed,.compensated,.compensation_failed). - Strict Zod schemas (
z.object(...).strict()) for every payload — extra fields are rejected, missing fields are rejected. SagaEventEmitterwith one method per event so callers can't mix the wrong payload with the wrong event name.- Schemas exported so external consumers (audit sinks, web hooks, log shippers) can re-validate inbound payloads.
Usage: Basic
import { EventBus } from "@suaveplan/events";
import {
SagaEventEmitter,
type SagaEventMap,
} from "@suaveplan/api-saga/events/events";
const bus = new EventBus<SagaEventMap>();
const emitter = new SagaEventEmitter(bus);
bus.on("saga.step.completed", async (payload) => {
// payload.output is typed as Readonly<Record<string, unknown>>
console.log(`step ${payload.stepName} produced`, payload.output);
});
emitter.emitStepCompleted({
sagaId: "s_1",
sagaName: "checkout",
stepName: "charge-card",
stepIndex: 0,
attempt: 1,
tenantId: "acme",
stepState: "completed",
at: "2026-05-26T00:00:00.000Z",
output: { chargeId: "ch_1" },
});
Usage: Advanced
The schemas are publicly exported so a downstream component (e.g. a webhook plugin that forwards saga events outside the host) can apply the same validation to inbound payloads it receives from network:
import {
sagaStepFailedEventSchema,
sagaCompensationFailedEventSchema,
} from "@suaveplan/api-saga/events/events";
router.post("/webhook/saga", async (req) => {
const json = await req.json();
if ("failedStep" in json) {
const parsed = sagaCompensationFailedEventSchema.parse(json);
alertOnCall(parsed.sagaId, parsed.failedStep);
} else {
const parsed = sagaStepFailedEventSchema.parse(json);
log.warn("step failed", parsed);
}
});
API Reference
SagaEventMap—Record<saga.event.name, payload>type pinned to the eight documented event names.SagaEventEmitter— wrapper aroundEventBus<SagaEventMap>:emitStepRequested(payload),emitStepCompleted(payload),emitStepFailed(payload),emitStepCompensated(payload),emitInstanceStarted(payload),emitInstanceCompleted(payload),emitInstanceCompensated(payload),emitInstanceCompensationFailed(payload).
- Zod schemas:
sagaStepEventSchema,sagaStepCompletedEventSchema,sagaStepFailedEventSchema,sagaStepCompensatedEventSchema,sagaInstanceEventSchema,sagaCompensationFailedEventSchema.
Implementation Notes
The emitter validates outbound payloads before pushing them onto the
bus. This is deliberate — bus.emit is fire-and-forget; if the
emitter trusted the caller and pushed a bad payload, every listener
would see corrupt data with no callback to recover. By failing at the
emit site the bug surfaces inside the orchestrator path that produced
the payload, not in a downstream consumer that has no visibility into
which path produced what.