Initial Commit
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user