Files
skills/docs/api-stack/api/api-saga/src/events/events.md
2026-05-29 08:20:56 +01:00

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 SagaEventMap covering 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.
  • SagaEventEmitter with 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 around EventBus<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.