Files
suaveplan/packages/api-stack/api/api-saga/src/idempotency/idempotency.ts
2026-05-27 23:29:53 +01:00

44 lines
1.6 KiB
TypeScript

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<T>(
sagaId: string,
stepName: string,
mode: SagaIdempotencyMode | undefined,
fn: () => Promise<T>
): Promise<T> {
if (mode === "manual") {
return fn();
}
const key = stepIdempotencyKey(sagaId, stepName);
return this.guard.execute<T>(key, fn);
}
}