import type { IdempotencyGuard } from "@suaveplan/idempotency"; import type { SagaIdempotencyMode } from "../registry/participant-registry.types.js"; /** * Compose the per-step idempotency key from the saga id and step * name. Stable across attempts so a replayed step under the same key * returns the cached outcome rather than re-entering the handler. */ export function stepIdempotencyKey(sagaId: string, stepName: string): string { return `saga:${sagaId}:${stepName}`; } /** * Per-step wrapper around `IdempotencyGuard.execute`. The orchestrator * calls this around every action invocation; the guard is keyed by * `{sagaId, stepName}` so a replayed step under the same key returns * the cached outcome without re-entering the handler. * * When the registration declares `idempotency: "manual"` the wrapper * is bypassed and the handler is invoked directly. The handler is then * solely responsible for its own dedup. This opt-out is honoured per * design doc §9.1's "Idempotency contract" bullet. */ export class SagaIdempotencyWrapper { public constructor(private readonly guard: IdempotencyGuard) {} /** * Wrap one step invocation. Returns the handler's outcome — either * cached from a previous attempt or newly produced. */ public async run( sagaId: string, stepName: string, mode: SagaIdempotencyMode | undefined, fn: () => Promise ): Promise { if (mode === "manual") { return fn(); } const key = stepIdempotencyKey(sagaId, stepName); return this.guard.execute(key, fn); } }