# `idempotency-middleware` _(no submodules)_ ## Purpose Provides a framework-agnostic idempotency middleware factory that wraps request/response handlers with deduplication logic. `createIdempotencyMiddleware` reads an idempotency key from the request, checks the store, and returns the cached response or executes the handler and caches the result. Works with Express, Fastify, Hono, Fetch API, or any custom request/response abstraction. ## Features - Generic `TReq` / `TRes` type parameters for full type safety - Configurable key extraction from any request type - Per-operation TTL overrides for granular cache expiration - Fingerprint mismatch detection before returning cached responses - `transformResponse` hook for adapter-specific response transformations - Bypass behaviour: if no key is extracted, the handler runs without caching - State machine write (`pending` → `completed | failed`) before executing handler ## Usage: Basic Example ```ts import { createIdempotencyMiddleware } from "@suaveplan/idempotency/middleware"; import { MemoryIdempotencyStore } from "@suaveplan/idempotency/store"; interface MyRequest { headers: Record; body: unknown } interface MyResponse { status: number; data: unknown } const middleware = createIdempotencyMiddleware({ store: new MemoryIdempotencyStore(), extractKey: (req) => req.headers["idempotency-key"], }); const result = await middleware(req, async (r) => ({ status: 200, data: await process(r.body) })); console.log(result.cached); // true on second call with same key ``` ## Usage: Advanced Example ```ts const middleware = createIdempotencyMiddleware({ store, extractKey: (req) => req.headers["idempotency-key"], extractInput: (req) => req.body, extractOperation: (req) => req.headers["x-operation"], ttlOverrides: { "create-payment": 86_400_000, // 24h "send-email": 3_600_000, // 1h }, transformResponse: (res) => ({ ...res, fromCache: true }), }); ``` ## API Reference ### `createIdempotencyMiddleware(config): Handler` Returns `async (request: TReq, handler: (req: TReq) => Promise) => Promise>`. **Config options:** | Option | Type | Default | Description | |--------|------|---------|-------------| | `store` | `IdempotencyStoreAdapter` | required | Record persistence | | `extractKey` | `(req) => string \| undefined` | required | Key extraction | | `extractInput` | `(req) => unknown` | — | Input for fingerprinting | | `ttlMs` | `number` | `86_400_000` | Default TTL | | `ttlOverrides` | `Record` | — | Per-operation TTL | | `extractOperation` | `(req) => string \| undefined` | — | Operation name for TTL lookup | | `fingerprintFn` | `fn` | `fingerprintPayload` | Custom hasher | | `transformResponse` | `(res: TRes) => TRes` | — | Response transformer | ### `MiddlewareResult` ```ts interface MiddlewareResult { response: TRes; cached: boolean; key: string; } ``` **Throws:** `IdempotencyMismatchError` when the request fingerprint differs from the cached fingerprint. ## Implementation Notes - The middleware always writes a `pending` record before calling the handler, ensuring crash recovery on retry. - `failed` records do not block retries — if the handler throws, subsequent requests with the same key re-execute the handler. - Bypassed requests (no key extracted) run the handler directly without touching the store. - `transformResponse` is applied both on cache hit and on fresh execution, so the response shape is always consistent.