44 lines
1.6 KiB
TypeScript
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);
|
|
}
|
|
}
|