first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,113 @@
# `events`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## 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
```ts
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:
```ts
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.