@nestjs-redisx/idempotency 1.6.0 → 1.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (25) hide show
  1. package/dist/idempotency/api/filters/idempotency-exception.filter.d.ts +1 -0
  2. package/dist/idempotency/api/filters/idempotency-exception.filter.d.ts.map +1 -1
  3. package/dist/idempotency/api/interceptors/idempotency.interceptor.d.ts.map +1 -1
  4. package/dist/idempotency/application/ports/idempotency-service.port.d.ts +1 -1
  5. package/dist/idempotency/application/ports/idempotency-service.port.d.ts.map +1 -1
  6. package/dist/idempotency/application/ports/idempotency-store.port.d.ts +4 -2
  7. package/dist/idempotency/application/ports/idempotency-store.port.d.ts.map +1 -1
  8. package/dist/idempotency/application/services/idempotency.service.d.ts +15 -1
  9. package/dist/idempotency/application/services/idempotency.service.d.ts.map +1 -1
  10. package/dist/idempotency/infrastructure/adapters/redis-idempotency-store.adapter.d.ts +2 -2
  11. package/dist/idempotency/infrastructure/adapters/redis-idempotency-store.adapter.d.ts.map +1 -1
  12. package/dist/idempotency/infrastructure/scripts/lua-scripts.d.ts +1 -0
  13. package/dist/idempotency/infrastructure/scripts/lua-scripts.d.ts.map +1 -1
  14. package/dist/idempotency.plugin.d.ts.map +1 -1
  15. package/dist/index.d.ts +1 -1
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +106 -34
  18. package/dist/index.js.map +1 -1
  19. package/dist/index.mjs +106 -35
  20. package/dist/index.mjs.map +1 -1
  21. package/dist/shared/errors/index.d.ts +7 -0
  22. package/dist/shared/errors/index.d.ts.map +1 -1
  23. package/dist/shared/types/index.d.ts +13 -1
  24. package/dist/shared/types/index.d.ts.map +1 -1
  25. package/package.json +2 -2
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../package.json","../src/shared/errors/index.ts","../src/idempotency/api/filters/idempotency-exception.filter.ts","../src/shared/constants/index.ts","../src/idempotency/api/decorators/idempotent.decorator.ts","../src/idempotency/api/interceptors/idempotency.interceptor.ts","../src/idempotency/application/services/idempotency.service.ts","../src/idempotency/infrastructure/scripts/lua-scripts.ts","../src/idempotency/infrastructure/adapters/redis-idempotency-store.adapter.ts","../src/idempotency.plugin.ts"],"names":["RedisXError","ErrorCode","HttpStatus","Catch","HttpAdapterHost","applyDecorators","SetMetadata","UseInterceptors","IdempotencyInterceptor","Logger","tap","createHash","of","Injectable","Inject","Reflector","IdempotencyService","Optional","CLIENT_MANAGER","REDIS_CLIENTS_INITIALIZATION","APP_FILTER"],"mappings":"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","file":"index.js","sourcesContent":["{\n \"name\": \"@nestjs-redisx/idempotency\",\n \"version\": \"1.6.0\",\n \"description\": \"Idempotency plugin for NestJS RedisX with request deduplication and response replay\",\n \"author\": \"NestJS RedisX Team\",\n \"license\": \"MIT\",\n \"main\": \"dist/index.js\",\n \"types\": \"dist/index.d.ts\",\n \"exports\": {\n \".\": {\n \"types\": \"./dist/index.d.ts\",\n \"require\": \"./dist/index.js\",\n \"import\": \"./dist/index.js\",\n \"default\": \"./dist/index.js\"\n }\n },\n \"files\": [\n \"dist\",\n \"README.md\",\n \"LICENSE\"\n ],\n \"scripts\": {\n \"build\": \"tsup\",\n \"test\": \"SKIP_INTEGRATION=true vitest run\",\n \"test:watch\": \"SKIP_INTEGRATION=true vitest\",\n \"test:cov\": \"SKIP_INTEGRATION=true vitest run --coverage\",\n \"test:integration\": \"SKIP_INTEGRATION=false vitest run test/integration/idempotency.integration.spec.ts\",\n \"test:integration:watch\": \"SKIP_INTEGRATION=false vitest watch test/integration/idempotency.integration.spec.ts\",\n \"docker:up\": \"cd ../.. && docker-compose up -d redis\",\n \"docker:down\": \"cd ../.. && docker-compose down\",\n \"docker:logs\": \"cd ../.. && docker-compose logs -f redis\",\n \"test:docker\": \"npm run docker:up && sleep 3 && npm run test:integration; TEST_EXIT=$?; npm run docker:down; exit $TEST_EXIT\",\n \"test:all\": \"npm run test:cov && npm run test:integration\",\n \"lint\": \"eslint \\\"{src,test}/**/*.ts\\\"\",\n \"format\": \"prettier --write \\\"{src,test}/**/*.ts\\\"\"\n },\n \"keywords\": [\n \"nestjs\",\n \"redis\",\n \"idempotency\",\n \"idempotent\",\n \"deduplication\",\n \"request-replay\",\n \"http-idempotency\"\n ],\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"https://github.com/nestjs-redisx/nestjs-redisx.git\",\n \"directory\": \"packages/idempotency\"\n },\n \"homepage\": \"https://nestjs-redisx.dev/en/reference/idempotency/\",\n \"bugs\": {\n \"url\": \"https://github.com/nestjs-redisx/nestjs-redisx/issues\"\n },\n \"peerDependencies\": {\n \"@nestjs-redisx/core\": \"^1.0.0\",\n \"@nestjs/common\": \"^10.0.0 || ^11.0.0\",\n \"@nestjs/core\": \"^10.0.0 || ^11.0.0\",\n \"reflect-metadata\": \"^0.2.0\",\n \"rxjs\": \"^7.8.0\"\n },\n \"devDependencies\": {\n \"@nestjs/testing\": \"^9.0.0 || ^10.0.0 || ^11.0.0\",\n \"@types/node\": \"^20.0.0\",\n \"@typescript-eslint/eslint-plugin\": \"^6.0.0\",\n \"@typescript-eslint/parser\": \"^6.0.0\",\n \"@vitest/coverage-v8\": \"^1.6.0\",\n \"eslint\": \"^8.0.0\",\n \"prettier\": \"^3.0.0\",\n \"tsup\": \"^8.0.0\",\n \"typescript\": \"^5.3.0\",\n \"vitest\": \"^1.6.0\"\n },\n \"publishConfig\": {\n \"access\": \"public\"\n },\n \"engines\": {\n \"node\": \">=18.0.0\"\n }\n}\n","import { RedisXError, ErrorCode } from '@nestjs-redisx/core';\n\n/**\n * Base class for all idempotency-related errors\n */\nexport class IdempotencyError extends RedisXError {\n constructor(\n message: string,\n code: ErrorCode,\n public readonly idempotencyKey: string,\n cause?: Error,\n ) {\n super(message, code, cause, { idempotencyKey });\n }\n}\n\n/**\n * Thrown when Idempotency-Key header is required but not provided\n */\nexport class IdempotencyKeyRequiredError extends IdempotencyError {\n constructor() {\n super('Idempotency-Key header is required', ErrorCode.IDEMPOTENCY_KEY_INVALID, '');\n }\n}\n\n/**\n * Thrown when request fingerprint doesn't match the stored one\n */\nexport class IdempotencyFingerprintMismatchError extends IdempotencyError {\n constructor(key: string) {\n super(`Request body does not match previous request with idempotency key \"${key}\"`, ErrorCode.IDEMPOTENCY_KEY_INVALID, key);\n }\n}\n\n/**\n * Thrown when timeout waiting for concurrent request to complete\n */\nexport class IdempotencyTimeoutError extends IdempotencyError {\n constructor(key: string) {\n super(`Timeout waiting for concurrent request with idempotency key \"${key}\"`, ErrorCode.OP_TIMEOUT, key);\n }\n}\n\n/**\n * Thrown when previous request with same key failed\n */\nexport class IdempotencyFailedError extends IdempotencyError {\n constructor(key: string, error?: string) {\n super(`Previous request with idempotency key \"${key}\" failed${error ? `: ${error}` : ''}`, ErrorCode.IDEMPOTENCY_PREVIOUS_FAILED, key);\n }\n}\n\n/**\n * Thrown when idempotency record not found in Redis\n */\nexport class IdempotencyRecordNotFoundError extends IdempotencyError {\n constructor(key: string) {\n super(`Idempotency record not found for key \"${key}\"`, ErrorCode.OP_KEY_NOT_FOUND, key);\n }\n}\n","import { Catch, ExceptionFilter, ArgumentsHost, HttpStatus, Inject } from '@nestjs/common';\nimport { HttpAdapterHost } from '@nestjs/core';\n\nimport { IdempotencyError, IdempotencyKeyRequiredError, IdempotencyFingerprintMismatchError, IdempotencyTimeoutError, IdempotencyFailedError } from '../../../shared/errors';\n\n/**\n * Exception filter for idempotency errors.\n *\n * Without this filter the idempotency errors (which extend `RedisXError`, not\n * `HttpException`) would surface as `500 Internal Server Error`. That is wrong\n * for situations that are part of normal idempotent-request handling — a reused\n * key with a different body, a still-in-progress concurrent request, or a key\n * whose previous attempt failed. This filter maps each error to an appropriate\n * 4xx status while leaving genuinely unexpected errors as 500.\n */\n@Catch(IdempotencyError)\nexport class IdempotencyExceptionFilter implements ExceptionFilter {\n constructor(@Inject(HttpAdapterHost) private readonly adapterHost: HttpAdapterHost) {}\n\n catch(exception: IdempotencyError, host: ArgumentsHost): void {\n const httpAdapter = this.adapterHost.httpAdapter;\n if (!httpAdapter) {\n throw new Error('IdempotencyExceptionFilter: HttpAdapterHost is not initialized. Ensure the NestJS application has bootstrapped with an HTTP adapter before handling requests.');\n }\n\n const response = host.switchToHttp().getResponse();\n const status = this.resolveStatus(exception);\n\n const body = {\n statusCode: status,\n message: exception.message,\n error: this.reasonPhrase(status),\n idempotencyKey: exception.idempotencyKey || undefined,\n };\n\n httpAdapter.reply(response, body, status);\n }\n\n /**\n * Maps an idempotency error to its HTTP status code.\n *\n * - Missing/invalid key -> 400 Bad Request\n * - Reused key with a different request -> 422 Unprocessable Entity\n * - Concurrent request still in progress (timeout) -> 409 Conflict\n * - Previous request with the same key failed -> 409 Conflict\n * - Anything else (unexpected) -> 500 Internal Server Error\n */\n private resolveStatus(exception: IdempotencyError): HttpStatus {\n if (exception instanceof IdempotencyKeyRequiredError) {\n return HttpStatus.BAD_REQUEST;\n }\n if (exception instanceof IdempotencyFingerprintMismatchError) {\n return HttpStatus.UNPROCESSABLE_ENTITY;\n }\n if (exception instanceof IdempotencyTimeoutError) {\n return HttpStatus.CONFLICT;\n }\n if (exception instanceof IdempotencyFailedError) {\n return HttpStatus.CONFLICT;\n }\n return HttpStatus.INTERNAL_SERVER_ERROR;\n }\n\n private reasonPhrase(status: HttpStatus): string {\n switch (status) {\n case HttpStatus.BAD_REQUEST:\n return 'Bad Request';\n case HttpStatus.CONFLICT:\n return 'Conflict';\n case HttpStatus.UNPROCESSABLE_ENTITY:\n return 'Unprocessable Entity';\n default:\n return 'Internal Server Error';\n }\n }\n}\n","/**\n * Injection tokens for idempotency plugin.\n */\nexport const IDEMPOTENCY_PLUGIN_OPTIONS = Symbol.for('IDEMPOTENCY_PLUGIN_OPTIONS');\nexport const IDEMPOTENCY_SERVICE = Symbol.for('IDEMPOTENCY_SERVICE');\nexport const IDEMPOTENCY_STORE = Symbol.for('IDEMPOTENCY_STORE');\n\n/**\n * Plugin-specific Redis driver token.\n * Resolves to the named client specified in plugin options.\n */\nexport const IDEMPOTENCY_REDIS_DRIVER = Symbol.for('IDEMPOTENCY_REDIS_DRIVER');\n","import { applyDecorators, SetMetadata, UseInterceptors, ExecutionContext } from '@nestjs/common';\n\nimport { IdempotencyInterceptor } from '../interceptors/idempotency.interceptor';\n\nexport const IDEMPOTENT_OPTIONS = Symbol.for('IDEMPOTENT_OPTIONS');\n\nexport interface IIdempotentOptions {\n ttl?: number;\n keyExtractor?: (context: ExecutionContext) => string | Promise<string>;\n fingerprintFields?: ('method' | 'path' | 'body' | 'query')[];\n validateFingerprint?: boolean;\n cacheHeaders?: string[];\n skip?: (context: ExecutionContext) => boolean | Promise<boolean>;\n}\n\nexport function Idempotent(options: IIdempotentOptions = {}): MethodDecorator {\n return applyDecorators(SetMetadata(IDEMPOTENT_OPTIONS, options), UseInterceptors(IdempotencyInterceptor));\n}\n","import { createHash } from 'crypto';\n\nimport { Injectable, NestInterceptor, ExecutionContext, CallHandler, Inject, Logger } from '@nestjs/common';\nimport { HttpAdapterHost, Reflector } from '@nestjs/core';\nimport { Observable, of } from 'rxjs';\nimport { tap } from 'rxjs/operators';\n\nimport { IDEMPOTENCY_SERVICE, IDEMPOTENCY_PLUGIN_OPTIONS } from '../../../shared/constants';\nimport { IdempotencyFingerprintMismatchError, IdempotencyFailedError } from '../../../shared/errors';\nimport { IIdempotencyPluginOptions, IIdempotencyRecord } from '../../../shared/types';\nimport { IIdempotencyService } from '../../application/ports/idempotency-service.port';\nimport { IDEMPOTENT_OPTIONS, IIdempotentOptions } from '../decorators/idempotent.decorator';\n\n@Injectable()\nexport class IdempotencyInterceptor implements NestInterceptor {\n private readonly logger = new Logger(IdempotencyInterceptor.name);\n\n constructor(\n @Inject(IDEMPOTENCY_SERVICE) private readonly idempotencyService: IIdempotencyService,\n @Inject(IDEMPOTENCY_PLUGIN_OPTIONS) private readonly config: IIdempotencyPluginOptions,\n @Inject(Reflector) private readonly reflector: Reflector,\n @Inject(HttpAdapterHost) private readonly adapterHost: HttpAdapterHost,\n ) {}\n\n async intercept(context: ExecutionContext, next: CallHandler): Promise<Observable<unknown>> {\n const options = this.getOptions(context);\n const response = context.switchToHttp().getResponse();\n\n const key = await this.extractKey(context, options);\n\n if (!key) {\n return next.handle();\n }\n\n if (options.skip && (await options.skip(context))) {\n return next.handle();\n }\n\n const fingerprint = await this.generateFingerprint(context, options);\n\n let checkResult: Awaited<ReturnType<typeof this.idempotencyService.checkAndLock>>;\n try {\n checkResult = await this.idempotencyService.checkAndLock(key, fingerprint, {\n ttl: options.ttl,\n });\n } catch (error) {\n // The idempotency store is unavailable (e.g. Redis is down). Honor the\n // configured errorPolicy: 'fail-open' proceeds without idempotency\n // protection, 'fail-closed' (default) rejects by propagating the error.\n if ((this.config.errorPolicy ?? 'fail-closed') === 'fail-open') {\n this.logger.warn(`Idempotency store unavailable for key \"${key}\"; proceeding without idempotency (fail-open): ${(error as Error).message}`);\n return next.handle();\n }\n throw error;\n }\n\n if (!checkResult.isNew) {\n if (checkResult.fingerprintMismatch) {\n throw new IdempotencyFingerprintMismatchError(key);\n }\n\n const record = checkResult.record!;\n\n if (record.status === 'failed') {\n throw new IdempotencyFailedError(key, record.error);\n }\n\n return this.replayResponse(response, record);\n }\n\n return next.handle().pipe(\n tap({\n next: (data) => {\n void this.idempotencyService.complete(\n key,\n {\n statusCode: response.statusCode,\n body: data,\n headers: this.extractHeaders(response, options),\n },\n { ttl: options.ttl },\n );\n },\n error: (error) => {\n void this.idempotencyService.fail(key, error.message);\n },\n }),\n );\n }\n\n private getOptions(context: ExecutionContext): IIdempotentOptions {\n return this.reflector.get<IIdempotentOptions>(IDEMPOTENT_OPTIONS, context.getHandler()) ?? {};\n }\n\n private async extractKey(context: ExecutionContext, options: IIdempotentOptions): Promise<string | null> {\n if (options.keyExtractor) {\n return options.keyExtractor(context);\n }\n\n const request = context.switchToHttp().getRequest();\n const headerName = this.config.headerName ?? 'Idempotency-Key';\n\n return request.headers[headerName.toLowerCase()] ?? null;\n }\n\n private async generateFingerprint(context: ExecutionContext, options: IIdempotentOptions): Promise<string> {\n if (this.config.fingerprintGenerator) {\n return this.config.fingerprintGenerator(context);\n }\n\n const request = context.switchToHttp().getRequest();\n const fields = options.fingerprintFields ?? this.config.fingerprintFields ?? ['method', 'path', 'body'];\n\n const parts: string[] = [];\n\n if (fields.includes('method')) parts.push(request.method);\n if (fields.includes('path')) parts.push(this.getRequestPath(request));\n if (fields.includes('body')) parts.push(JSON.stringify(request.body ?? {}));\n if (fields.includes('query')) parts.push(JSON.stringify(request.query ?? {}));\n\n const data = parts.join('|');\n return this.hash(data);\n }\n\n private getRequestPath(request: unknown): string {\n const httpAdapter = this.adapterHost.httpAdapter;\n const req = request as { path?: string; url?: string };\n return httpAdapter?.getRequestUrl?.(request) ?? req.url ?? req.path ?? '';\n }\n\n private hash(data: string): string {\n return createHash('sha256').update(data).digest('hex');\n }\n\n private replayResponse(response: unknown, record: IIdempotencyRecord): Observable<unknown> {\n const httpAdapter = this.adapterHost.httpAdapter;\n if (!httpAdapter) {\n throw new Error('IdempotencyInterceptor: HttpAdapterHost is not initialized. Ensure the NestJS application has bootstrapped with an HTTP adapter before handling requests.');\n }\n\n httpAdapter.status(response, record.statusCode ?? 200);\n\n if (record.headers) {\n const headers = JSON.parse(record.headers);\n for (const [key, value] of Object.entries(headers)) {\n httpAdapter.setHeader(response, key, value as string);\n }\n }\n\n const body = record.response ? JSON.parse(record.response) : null;\n return of(body);\n }\n\n private extractHeaders(response: { getHeader?: (name: string) => string | number | string[] | undefined }, options: IIdempotentOptions): Record<string, string> | undefined {\n if (!options.cacheHeaders?.length) return undefined;\n\n const headers: Record<string, string> = {};\n for (const name of options.cacheHeaders) {\n const value = response.getHeader?.(name);\n if (value) headers[name] = String(value);\n }\n\n return Object.keys(headers).length > 0 ? headers : undefined;\n }\n}\n","import { Injectable, Inject, Optional } from '@nestjs/common';\n\nimport { IDEMPOTENCY_PLUGIN_OPTIONS, IDEMPOTENCY_STORE } from '../../../shared/constants';\n\n/** Polling interval when waiting for an in-flight idempotent request to complete. */\nconst POLL_INTERVAL_MS = 100;\nimport { IdempotencyRecordNotFoundError, IdempotencyTimeoutError } from '../../../shared/errors';\nimport { IIdempotencyPluginOptions, IIdempotencyRecord, IIdempotencyCheckResult, IIdempotencyResponse, IIdempotencyOptions } from '../../../shared/types';\nimport { IIdempotencyService } from '../ports/idempotency-service.port';\nimport { IIdempotencyStore } from '../ports/idempotency-store.port';\n\n// Optional metrics integration\nconst METRICS_SERVICE = Symbol.for('METRICS_SERVICE');\n\ninterface IMetricsService {\n incrementCounter(name: string, labels?: Record<string, string>, value?: number): void;\n observeHistogram(name: string, value: number, labels?: Record<string, string>): void;\n}\n\n/**\n * Idempotency service implementation\n */\n@Injectable()\nexport class IdempotencyService implements IIdempotencyService {\n constructor(\n @Inject(IDEMPOTENCY_PLUGIN_OPTIONS)\n private readonly config: IIdempotencyPluginOptions,\n @Inject(IDEMPOTENCY_STORE)\n private readonly store: IIdempotencyStore,\n @Optional() @Inject(METRICS_SERVICE) private readonly metrics?: IMetricsService,\n ) {}\n\n async checkAndLock(key: string, fingerprint: string, options: IIdempotencyOptions = {}): Promise<IIdempotencyCheckResult> {\n const startTime = Date.now();\n const fullKey = this.buildKey(key);\n const lockTimeout = options.lockTimeout ?? this.config.lockTimeout ?? 30000;\n\n const result = await this.store.checkAndLock(fullKey, fingerprint, lockTimeout);\n\n if (result.status === 'new') {\n this.metrics?.incrementCounter('redisx_idempotency_requests_total', { status: 'new' });\n this.recordDuration(startTime);\n return { isNew: true };\n }\n\n if (result.status === 'fingerprint_mismatch') {\n this.metrics?.incrementCounter('redisx_idempotency_requests_total', { status: 'mismatch' });\n this.recordDuration(startTime);\n return { isNew: false, fingerprintMismatch: true };\n }\n\n if (result.status === 'processing') {\n // Wait for completion\n const record = await this.waitForCompletion(fullKey);\n this.metrics?.incrementCounter('redisx_idempotency_requests_total', { status: 'replay' });\n this.recordDuration(startTime);\n return { isNew: false, record };\n }\n\n // completed or failed - replay from cache\n this.metrics?.incrementCounter('redisx_idempotency_requests_total', { status: 'replay' });\n this.recordDuration(startTime);\n return { isNew: false, record: result.record };\n }\n\n private recordDuration(startTime: number): void {\n const duration = (Date.now() - startTime) / 1000;\n this.metrics?.observeHistogram('redisx_idempotency_duration_seconds', duration);\n }\n\n async complete(key: string, response: IIdempotencyResponse, options: IIdempotencyOptions = {}): Promise<void> {\n const fullKey = this.buildKey(key);\n const ttl = options.ttl ?? this.config.defaultTtl ?? 86400;\n\n await this.store.complete(\n fullKey,\n {\n statusCode: response.statusCode,\n response: JSON.stringify(response.body),\n headers: response.headers ? JSON.stringify(response.headers) : undefined,\n completedAt: Date.now(),\n },\n ttl,\n );\n }\n\n async fail(key: string, error: string): Promise<void> {\n const fullKey = this.buildKey(key);\n // Failed records are short-lived: keep them only for the lock window so a\n // retry returns the failure briefly, then a fresh attempt is allowed.\n const ttlSeconds = Math.ceil((this.config.lockTimeout ?? 30000) / 1000);\n await this.store.fail(fullKey, error, ttlSeconds);\n }\n\n async get(key: string): Promise<IIdempotencyRecord | null> {\n const fullKey = this.buildKey(key);\n return this.store.get(fullKey);\n }\n\n async delete(key: string): Promise<boolean> {\n const fullKey = this.buildKey(key);\n return this.store.delete(fullKey);\n }\n\n private async waitForCompletion(key: string): Promise<IIdempotencyRecord> {\n const waitTimeout = this.config.waitTimeout ?? 60000;\n const startTime = Date.now();\n while (Date.now() - startTime < waitTimeout) {\n const record = await this.store.get(key);\n\n if (!record) {\n throw new IdempotencyRecordNotFoundError(key);\n }\n\n if (record.status === 'completed' || record.status === 'failed') {\n return record;\n }\n\n await this.sleep(POLL_INTERVAL_MS);\n }\n\n throw new IdempotencyTimeoutError(key);\n }\n\n private buildKey(key: string): string {\n const prefix = this.config.keyPrefix ?? 'idempotency:';\n return `${prefix}${key}`;\n }\n\n private sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n }\n}\n","/**\n * Inline Lua scripts for idempotency operations.\n *\n * Scripts are stored as inline strings to avoid issues with file reading\n * after build (dist directory doesn't contain .lua files).\n */\n\n/**\n * Check and Lock Lua script for idempotency\n *\n * This script atomically checks if an idempotency key exists and locks it if new.\n *\n * KEYS[1] = idempotency key\n * ARGV[1] = fingerprint\n * ARGV[2] = lock timeout (ms)\n * ARGV[3] = current timestamp (ms)\n *\n * Returns:\n * - ['new'] - new request, lock acquired\n * - ['fingerprint_mismatch'] - same key, different fingerprint\n * - ['processing'] - another request is processing\n * - [status, statusCode, response, headers, error] - completed/failed record\n */\nexport const CHECK_AND_LOCK_SCRIPT = `\nlocal key = KEYS[1]\nlocal fingerprint = ARGV[1]\nlocal lock_timeout = tonumber(ARGV[2])\nlocal now = tonumber(ARGV[3])\n\n-- Check if key exists\nlocal existing = redis.call('HGETALL', key)\n\nif #existing == 0 then\n -- New request - create lock\n redis.call('HMSET', key,\n 'fingerprint', fingerprint,\n 'status', 'processing',\n 'startedAt', now\n )\n redis.call('PEXPIRE', key, lock_timeout)\n return {'new'}\nend\n\n-- Convert to table\nlocal record = {}\nfor i = 1, #existing, 2 do\n record[existing[i]] = existing[i + 1]\nend\n\n-- Check fingerprint\nif record.fingerprint ~= fingerprint then\n return {'fingerprint_mismatch'}\nend\n\n-- Check status\nif record.status == 'processing' then\n -- Check if lock expired (stale)\n local started = tonumber(record.startedAt)\n if now - started > lock_timeout then\n -- Stale lock - take over\n redis.call('HMSET', key,\n 'status', 'processing',\n 'startedAt', now\n )\n redis.call('PEXPIRE', key, lock_timeout)\n return {'new'}\n end\n return {'processing'}\nend\n\n-- Completed or failed - return record\nreturn {\n record.status,\n record.statusCode or '',\n record.response or '',\n record.headers or '',\n record.error or ''\n}\n`.trim();\n","import { Injectable, Inject, OnModuleInit } from '@nestjs/common';\nimport { IRedisDriver } from '@nestjs-redisx/core';\n\nimport { IDEMPOTENCY_REDIS_DRIVER } from '../../../shared/constants';\nimport { IIdempotencyRecord } from '../../../shared/types';\nimport { IIdempotencyStore, ICheckAndLockResult, ICompleteData } from '../../application/ports/idempotency-store.port';\nimport { CHECK_AND_LOCK_SCRIPT } from '../scripts/lua-scripts';\n\n/**\n * Redis-based idempotency store implementation\n */\n@Injectable()\nexport class RedisIdempotencyStoreAdapter implements IIdempotencyStore, OnModuleInit {\n private checkAndLockSha: string | null = null;\n\n constructor(@Inject(IDEMPOTENCY_REDIS_DRIVER) private readonly driver: IRedisDriver) {}\n\n /**\n * Pre-load Lua script on module initialization\n */\n async onModuleInit(): Promise<void> {\n this.checkAndLockSha = await this.driver.scriptLoad(CHECK_AND_LOCK_SCRIPT);\n }\n\n async checkAndLock(key: string, fingerprint: string, lockTimeoutMs: number): Promise<ICheckAndLockResult> {\n const now = Date.now();\n const rawResult = await this.driver.evalsha(this.checkAndLockSha!, [key], [fingerprint, lockTimeoutMs, now]);\n\n // Normalize result: node-redis may return Buffer/null elements\n const result = (rawResult as unknown[]).map((v) => (v === null || v === undefined ? '' : String(v)));\n\n const status = result[0];\n\n if (status === 'new') {\n return { status: 'new' };\n }\n\n if (status === 'fingerprint_mismatch') {\n return { status: 'fingerprint_mismatch' };\n }\n\n if (status === 'processing') {\n return { status: 'processing' };\n }\n\n // completed or failed\n return {\n status: status as 'completed' | 'failed',\n record: {\n key,\n fingerprint,\n status: status as 'completed' | 'failed',\n statusCode: result[1] ? parseInt(result[1], 10) : undefined,\n response: result[2] || undefined,\n headers: result[3] || undefined,\n error: result[4] || undefined,\n startedAt: 0, // Not returned from Lua\n },\n };\n }\n\n async complete(key: string, data: ICompleteData, ttlSeconds: number): Promise<void> {\n await this.driver.hmset(key, {\n status: 'completed',\n statusCode: String(data.statusCode),\n response: data.response,\n headers: data.headers || '',\n completedAt: String(data.completedAt),\n });\n await this.driver.expire(key, ttlSeconds);\n }\n\n async fail(key: string, error: string, ttlSeconds: number): Promise<void> {\n await this.driver.hmset(key, {\n status: 'failed',\n error,\n completedAt: String(Date.now()),\n });\n // Set an explicit expiry so the failed record does not rely on the leftover\n // lock TTL; once it expires a fresh attempt with the same key is allowed.\n await this.driver.expire(key, ttlSeconds);\n }\n\n async get(key: string): Promise<IIdempotencyRecord | null> {\n const data = await this.driver.hgetall(key);\n if (!data || Object.keys(data).length === 0) {\n return null;\n }\n\n return {\n key,\n fingerprint: data.fingerprint!,\n status: data.status! as 'processing' | 'completed' | 'failed',\n statusCode: data.statusCode ? parseInt(data.statusCode, 10) : undefined,\n response: data.response || undefined,\n headers: data.headers || undefined,\n startedAt: parseInt(data.startedAt!, 10),\n completedAt: data.completedAt ? parseInt(data.completedAt, 10) : undefined,\n error: data.error || undefined,\n };\n }\n\n async delete(key: string): Promise<boolean> {\n const result = await this.driver.del(key);\n return result > 0;\n }\n}\n","/**\n * Idempotency plugin for NestJS RedisX.\n * Provides request deduplication with response replay for idempotent operations.\n */\n\nimport { DynamicModule, ForwardReference, Provider, Type } from '@nestjs/common';\nimport { APP_FILTER, Reflector } from '@nestjs/core';\nimport { IRedisXPlugin, IPluginAsyncOptions, CLIENT_MANAGER, REDIS_CLIENTS_INITIALIZATION, RedisClientManager } from '@nestjs-redisx/core';\n\nimport { version } from '../package.json';\nimport { IdempotencyExceptionFilter } from './idempotency/api/filters/idempotency-exception.filter';\nimport { IdempotencyInterceptor } from './idempotency/api/interceptors/idempotency.interceptor';\nimport { IdempotencyService } from './idempotency/application/services/idempotency.service';\nimport { RedisIdempotencyStoreAdapter } from './idempotency/infrastructure/adapters/redis-idempotency-store.adapter';\nimport { IDEMPOTENCY_PLUGIN_OPTIONS, IDEMPOTENCY_REDIS_DRIVER, IDEMPOTENCY_SERVICE, IDEMPOTENCY_STORE } from './shared/constants';\nimport { IIdempotencyPluginOptions } from './shared/types';\n\nconst DEFAULT_IDEMPOTENCY_CONFIG: Required<Omit<IIdempotencyPluginOptions, 'isGlobal' | 'client' | 'fingerprintGenerator'>> = {\n defaultTtl: 86400,\n keyPrefix: 'idempotency:',\n headerName: 'Idempotency-Key',\n lockTimeout: 30000,\n waitTimeout: 60000,\n validateFingerprint: true,\n fingerprintFields: ['method', 'path', 'body'],\n errorPolicy: 'fail-closed',\n};\n\n/**\n * Idempotency plugin for NestJS RedisX.\n *\n * Provides request deduplication with response replay:\n * - Prevents duplicate processing of same request\n * - Replays successful responses\n * - Handles concurrent requests\n * - Validates request fingerprints\n *\n * @example\n * ```typescript\n * @Module({\n * imports: [\n * RedisModule.forRoot({\n * clients: { host: 'localhost', port: 6379 },\n * plugins: [\n * new IdempotencyPlugin({\n * defaultTtl: 86400,\n * headerName: 'Idempotency-Key',\n * validateFingerprint: true,\n * }),\n * ],\n * }),\n * ],\n * })\n * export class AppModule {}\n * ```\n */\nexport class IdempotencyPlugin implements IRedisXPlugin {\n readonly name = 'idempotency';\n readonly version: string = version;\n readonly description = 'Request deduplication with response replay for idempotent operations';\n\n private asyncOptions?: IPluginAsyncOptions<IIdempotencyPluginOptions>;\n\n constructor(private readonly options: IIdempotencyPluginOptions = {}) {}\n\n static registerAsync(asyncOptions: IPluginAsyncOptions<IIdempotencyPluginOptions>): IdempotencyPlugin {\n const plugin = new IdempotencyPlugin();\n plugin.asyncOptions = asyncOptions;\n return plugin;\n }\n\n private static mergeDefaults(options: IIdempotencyPluginOptions): IIdempotencyPluginOptions {\n return {\n client: options.client,\n defaultTtl: options.defaultTtl ?? DEFAULT_IDEMPOTENCY_CONFIG.defaultTtl,\n keyPrefix: options.keyPrefix ?? DEFAULT_IDEMPOTENCY_CONFIG.keyPrefix,\n headerName: options.headerName ?? DEFAULT_IDEMPOTENCY_CONFIG.headerName,\n lockTimeout: options.lockTimeout ?? DEFAULT_IDEMPOTENCY_CONFIG.lockTimeout,\n waitTimeout: options.waitTimeout ?? DEFAULT_IDEMPOTENCY_CONFIG.waitTimeout,\n validateFingerprint: options.validateFingerprint ?? DEFAULT_IDEMPOTENCY_CONFIG.validateFingerprint,\n fingerprintFields: options.fingerprintFields ?? DEFAULT_IDEMPOTENCY_CONFIG.fingerprintFields,\n errorPolicy: options.errorPolicy ?? DEFAULT_IDEMPOTENCY_CONFIG.errorPolicy,\n fingerprintGenerator: options.fingerprintGenerator,\n };\n }\n\n getImports(): Array<Type<unknown> | DynamicModule | ForwardReference> {\n return this.asyncOptions?.imports ?? [];\n }\n\n getProviders(): Provider[] {\n const optionsProvider: Provider = this.asyncOptions\n ? {\n provide: IDEMPOTENCY_PLUGIN_OPTIONS,\n useFactory: async (...args: unknown[]) => {\n const userOptions = await this.asyncOptions!.useFactory(...args);\n return IdempotencyPlugin.mergeDefaults(userOptions);\n },\n inject: this.asyncOptions.inject || [],\n }\n : {\n provide: IDEMPOTENCY_PLUGIN_OPTIONS,\n useValue: IdempotencyPlugin.mergeDefaults(this.options),\n };\n\n return [\n optionsProvider,\n // Plugin-specific Redis driver (resolves named client)\n {\n provide: IDEMPOTENCY_REDIS_DRIVER,\n useFactory: async (manager: RedisClientManager, _init: void, options: IIdempotencyPluginOptions) => {\n const clientName = options.client ?? 'default';\n try {\n return await manager.getClient(clientName);\n } catch {\n throw new Error(`IdempotencyPlugin: Redis client \"${clientName}\" not found. ` + `Available clients are configured in RedisModule.forRoot({ clients: { ... } }). ` + `Either add a \"${clientName}\" client or remove the \"client\" option to use the default connection.`);\n }\n },\n inject: [CLIENT_MANAGER, REDIS_CLIENTS_INITIALIZATION, IDEMPOTENCY_PLUGIN_OPTIONS],\n },\n { provide: IDEMPOTENCY_STORE, useClass: RedisIdempotencyStoreAdapter },\n { provide: IDEMPOTENCY_SERVICE, useClass: IdempotencyService },\n // Reflector is needed for @Idempotent decorator metadata\n Reflector,\n IdempotencyInterceptor,\n // Global exception filter to map idempotency errors to proper HTTP codes\n // (otherwise they surface as 500 since they are not HttpExceptions)\n { provide: APP_FILTER, useClass: IdempotencyExceptionFilter },\n ];\n }\n\n getExports(): Array<string | symbol | Provider> {\n return [IDEMPOTENCY_PLUGIN_OPTIONS, IDEMPOTENCY_SERVICE, IdempotencyInterceptor];\n }\n}\n"]}
1
+ {"version":3,"sources":["../package.json","../src/shared/errors/index.ts","../src/idempotency/api/filters/idempotency-exception.filter.ts","../src/shared/constants/index.ts","../src/idempotency/api/decorators/idempotent.decorator.ts","../src/idempotency/api/interceptors/idempotency.interceptor.ts","../src/idempotency/application/services/idempotency.service.ts","../src/idempotency/infrastructure/scripts/lua-scripts.ts","../src/idempotency/infrastructure/adapters/redis-idempotency-store.adapter.ts","../src/idempotency.plugin.ts"],"names":["RedisXError","ErrorCode","HttpStatus","Catch","HttpAdapterHost","applyDecorators","SetMetadata","UseInterceptors","IdempotencyInterceptor","Logger","tap","createHash","of","Injectable","Inject","Reflector","IdempotencyService","Optional","CLIENT_MANAGER","REDIS_CLIENTS_INITIALIZATION","APP_FILTER"],"mappings":"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","file":"index.js","sourcesContent":["{\n \"name\": \"@nestjs-redisx/idempotency\",\n \"version\": \"1.6.1\",\n \"description\": \"Idempotency plugin for NestJS RedisX with request deduplication and response replay\",\n \"author\": \"NestJS RedisX Team\",\n \"license\": \"MIT\",\n \"main\": \"dist/index.js\",\n \"types\": \"dist/index.d.ts\",\n \"exports\": {\n \".\": {\n \"types\": \"./dist/index.d.ts\",\n \"require\": \"./dist/index.js\",\n \"import\": \"./dist/index.js\",\n \"default\": \"./dist/index.js\"\n }\n },\n \"files\": [\n \"dist\",\n \"README.md\",\n \"LICENSE\"\n ],\n \"scripts\": {\n \"build\": \"tsup\",\n \"test\": \"SKIP_INTEGRATION=true vitest run\",\n \"test:watch\": \"SKIP_INTEGRATION=true vitest\",\n \"test:cov\": \"SKIP_INTEGRATION=true vitest run --coverage\",\n \"test:integration\": \"SKIP_INTEGRATION=false vitest run test/integration --pool=forks --poolOptions.forks.singleFork=true\",\n \"test:integration:watch\": \"SKIP_INTEGRATION=false vitest watch test/integration/idempotency.integration.spec.ts\",\n \"docker:up\": \"cd ../.. && docker-compose up -d redis\",\n \"docker:down\": \"cd ../.. && docker-compose down\",\n \"docker:logs\": \"cd ../.. && docker-compose logs -f redis\",\n \"test:docker\": \"npm run docker:up && sleep 3 && npm run test:integration; TEST_EXIT=$?; npm run docker:down; exit $TEST_EXIT\",\n \"test:all\": \"npm run test:cov && npm run test:integration\",\n \"lint\": \"eslint \\\"{src,test}/**/*.ts\\\"\",\n \"format\": \"prettier --write \\\"{src,test}/**/*.ts\\\"\"\n },\n \"keywords\": [\n \"nestjs\",\n \"redis\",\n \"idempotency\",\n \"idempotent\",\n \"deduplication\",\n \"request-replay\",\n \"http-idempotency\"\n ],\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"https://github.com/nestjs-redisx/nestjs-redisx.git\",\n \"directory\": \"packages/idempotency\"\n },\n \"homepage\": \"https://nestjs-redisx.dev/en/reference/idempotency/\",\n \"bugs\": {\n \"url\": \"https://github.com/nestjs-redisx/nestjs-redisx/issues\"\n },\n \"peerDependencies\": {\n \"@nestjs-redisx/core\": \"^1.0.0\",\n \"@nestjs/common\": \"^10.0.0 || ^11.0.0\",\n \"@nestjs/core\": \"^10.0.0 || ^11.0.0\",\n \"reflect-metadata\": \"^0.2.0\",\n \"rxjs\": \"^7.8.0\"\n },\n \"devDependencies\": {\n \"@nestjs/testing\": \"^9.0.0 || ^10.0.0 || ^11.0.0\",\n \"@types/node\": \"^20.0.0\",\n \"@typescript-eslint/eslint-plugin\": \"^6.0.0\",\n \"@typescript-eslint/parser\": \"^6.0.0\",\n \"@vitest/coverage-v8\": \"^1.6.0\",\n \"eslint\": \"^8.0.0\",\n \"prettier\": \"^3.0.0\",\n \"tsup\": \"^8.0.0\",\n \"typescript\": \"^5.3.0\",\n \"vitest\": \"^1.6.0\"\n },\n \"publishConfig\": {\n \"access\": \"public\"\n },\n \"engines\": {\n \"node\": \">=18.0.0\"\n }\n}\n","import { RedisXError, ErrorCode } from '@nestjs-redisx/core';\n\n/**\n * Base class for all idempotency-related errors\n */\nexport class IdempotencyError extends RedisXError {\n constructor(\n message: string,\n code: ErrorCode,\n public readonly idempotencyKey: string,\n cause?: Error,\n ) {\n super(message, code, cause, { idempotencyKey });\n }\n}\n\n/**\n * Thrown when Idempotency-Key header is required but not provided\n */\nexport class IdempotencyKeyRequiredError extends IdempotencyError {\n constructor() {\n super('Idempotency-Key header is required', ErrorCode.IDEMPOTENCY_KEY_INVALID, '');\n }\n}\n\n/**\n * Thrown when request fingerprint doesn't match the stored one\n */\nexport class IdempotencyFingerprintMismatchError extends IdempotencyError {\n constructor(key: string) {\n super(`Request body does not match previous request with idempotency key \"${key}\"`, ErrorCode.IDEMPOTENCY_KEY_INVALID, key);\n }\n}\n\n/**\n * Thrown when timeout waiting for concurrent request to complete\n */\nexport class IdempotencyTimeoutError extends IdempotencyError {\n constructor(key: string) {\n super(`Timeout waiting for concurrent request with idempotency key \"${key}\"`, ErrorCode.OP_TIMEOUT, key);\n }\n}\n\n/**\n * Thrown when previous request with same key failed\n */\nexport class IdempotencyFailedError extends IdempotencyError {\n constructor(key: string, error?: string) {\n super(`Previous request with idempotency key \"${key}\" failed${error ? `: ${error}` : ''}`, ErrorCode.IDEMPOTENCY_PREVIOUS_FAILED, key);\n }\n}\n\n/**\n * Thrown when idempotency record not found in Redis\n */\nexport class IdempotencyRecordNotFoundError extends IdempotencyError {\n constructor(key: string) {\n super(`Idempotency record not found for key \"${key}\"`, ErrorCode.OP_KEY_NOT_FOUND, key);\n }\n}\n\n/**\n * Thrown when the plugin is configured with invalid values (e.g. non-positive\n * timeouts, which would silently break record TTL semantics).\n */\nexport class IdempotencyConfigError extends IdempotencyError {\n constructor(message: string) {\n super(message, ErrorCode.CFG_INVALID, '');\n }\n}\n","import { Catch, ExceptionFilter, ArgumentsHost, HttpStatus, Inject } from '@nestjs/common';\nimport { HttpAdapterHost } from '@nestjs/core';\n\nimport { IdempotencyError, IdempotencyKeyRequiredError, IdempotencyFingerprintMismatchError, IdempotencyTimeoutError, IdempotencyFailedError, IdempotencyRecordNotFoundError } from '../../../shared/errors';\n\n/**\n * Exception filter for idempotency errors.\n *\n * Without this filter the idempotency errors (which extend `RedisXError`, not\n * `HttpException`) would surface as `500 Internal Server Error`. That is wrong\n * for situations that are part of normal idempotent-request handling — a reused\n * key with a different body, a still-in-progress concurrent request, or a key\n * whose previous attempt failed. This filter maps each error to an appropriate\n * 4xx status while leaving genuinely unexpected errors as 500.\n */\n@Catch(IdempotencyError)\nexport class IdempotencyExceptionFilter implements ExceptionFilter {\n constructor(@Inject(HttpAdapterHost) private readonly adapterHost: HttpAdapterHost) {}\n\n catch(exception: IdempotencyError, host: ArgumentsHost): void {\n const httpAdapter = this.adapterHost.httpAdapter;\n if (!httpAdapter) {\n throw new Error('IdempotencyExceptionFilter: HttpAdapterHost is not initialized. Ensure the NestJS application has bootstrapped with an HTTP adapter before handling requests.');\n }\n\n const response = host.switchToHttp().getResponse();\n const status = this.resolveStatus(exception);\n\n const body = {\n statusCode: status,\n message: exception.message,\n error: this.reasonPhrase(status),\n idempotencyKey: exception.idempotencyKey || undefined,\n };\n\n httpAdapter.reply(response, body, status);\n }\n\n /**\n * Maps an idempotency error to its HTTP status code.\n *\n * - Missing/invalid key -> 400 Bad Request\n * - Reused key with a different request -> 422 Unprocessable Entity\n * - Concurrent request still in progress (timeout) -> 409 Conflict\n * - Previous request with the same key failed -> 409 Conflict\n * - In-flight record vanished mid-wait -> 409 Conflict (retryable)\n * - Anything else (unexpected) -> 500 Internal Server Error\n */\n private resolveStatus(exception: IdempotencyError): HttpStatus {\n if (exception instanceof IdempotencyKeyRequiredError) {\n return HttpStatus.BAD_REQUEST;\n }\n if (exception instanceof IdempotencyFingerprintMismatchError) {\n return HttpStatus.UNPROCESSABLE_ENTITY;\n }\n if (exception instanceof IdempotencyTimeoutError) {\n return HttpStatus.CONFLICT;\n }\n if (exception instanceof IdempotencyFailedError) {\n return HttpStatus.CONFLICT;\n }\n if (exception instanceof IdempotencyRecordNotFoundError) {\n // Should be unreachable since waiters take over vanished records, but if\n // it ever surfaces the client should get a retryable 409, not a 500.\n return HttpStatus.CONFLICT;\n }\n return HttpStatus.INTERNAL_SERVER_ERROR;\n }\n\n private reasonPhrase(status: HttpStatus): string {\n switch (status) {\n case HttpStatus.BAD_REQUEST:\n return 'Bad Request';\n case HttpStatus.CONFLICT:\n return 'Conflict';\n case HttpStatus.UNPROCESSABLE_ENTITY:\n return 'Unprocessable Entity';\n default:\n return 'Internal Server Error';\n }\n }\n}\n","/**\n * Injection tokens for idempotency plugin.\n */\nexport const IDEMPOTENCY_PLUGIN_OPTIONS = Symbol.for('IDEMPOTENCY_PLUGIN_OPTIONS');\nexport const IDEMPOTENCY_SERVICE = Symbol.for('IDEMPOTENCY_SERVICE');\nexport const IDEMPOTENCY_STORE = Symbol.for('IDEMPOTENCY_STORE');\n\n/**\n * Plugin-specific Redis driver token.\n * Resolves to the named client specified in plugin options.\n */\nexport const IDEMPOTENCY_REDIS_DRIVER = Symbol.for('IDEMPOTENCY_REDIS_DRIVER');\n","import { applyDecorators, SetMetadata, UseInterceptors, ExecutionContext } from '@nestjs/common';\n\nimport { IdempotencyInterceptor } from '../interceptors/idempotency.interceptor';\n\nexport const IDEMPOTENT_OPTIONS = Symbol.for('IDEMPOTENT_OPTIONS');\n\nexport interface IIdempotentOptions {\n ttl?: number;\n keyExtractor?: (context: ExecutionContext) => string | Promise<string>;\n fingerprintFields?: ('method' | 'path' | 'body' | 'query')[];\n validateFingerprint?: boolean;\n cacheHeaders?: string[];\n skip?: (context: ExecutionContext) => boolean | Promise<boolean>;\n}\n\nexport function Idempotent(options: IIdempotentOptions = {}): MethodDecorator {\n return applyDecorators(SetMetadata(IDEMPOTENT_OPTIONS, options), UseInterceptors(IdempotencyInterceptor));\n}\n","import { createHash } from 'crypto';\n\nimport { Injectable, NestInterceptor, ExecutionContext, CallHandler, Inject, Logger } from '@nestjs/common';\nimport { HttpAdapterHost, Reflector } from '@nestjs/core';\nimport { Observable, of } from 'rxjs';\nimport { tap } from 'rxjs/operators';\n\nimport { IDEMPOTENCY_SERVICE, IDEMPOTENCY_PLUGIN_OPTIONS } from '../../../shared/constants';\nimport { IdempotencyFingerprintMismatchError, IdempotencyFailedError } from '../../../shared/errors';\nimport { IIdempotencyPluginOptions, IIdempotencyRecord } from '../../../shared/types';\nimport { IIdempotencyService } from '../../application/ports/idempotency-service.port';\nimport { IDEMPOTENT_OPTIONS, IIdempotentOptions } from '../decorators/idempotent.decorator';\n\n/**\n * Per-request marker recording that idempotency has already been handled for\n * this request. The interceptor can legitimately be bound more than once\n * (global APP_INTERCEPTOR + @Idempotent on the method, controller-level\n * @UseInterceptors + method decorator, controller inheritance). Without the\n * marker the inner pass would see its own 'processing' record and wait for\n * itself until the lock TTL expires — a self-deadlock. Mirrors\n * RATE_LIMIT_CONSUMED in the rate-limit guard.\n */\nconst IDEMPOTENCY_HANDLED = Symbol('redisx.idempotencyHandled');\n\n@Injectable()\nexport class IdempotencyInterceptor implements NestInterceptor {\n private readonly logger = new Logger(IdempotencyInterceptor.name);\n\n constructor(\n @Inject(IDEMPOTENCY_SERVICE) private readonly idempotencyService: IIdempotencyService,\n @Inject(IDEMPOTENCY_PLUGIN_OPTIONS) private readonly config: IIdempotencyPluginOptions,\n @Inject(Reflector) private readonly reflector: Reflector,\n @Inject(HttpAdapterHost) private readonly adapterHost: HttpAdapterHost,\n ) {}\n\n async intercept(context: ExecutionContext, next: CallHandler): Promise<Observable<unknown>> {\n // Idempotency must engage at most once per request even when the\n // interceptor is bound multiple times; a second pass is a plain\n // passthrough (see IDEMPOTENCY_HANDLED).\n const request = context.switchToHttp().getRequest<Record<symbol, unknown>>();\n if (request[IDEMPOTENCY_HANDLED]) {\n return next.handle();\n }\n\n const options = this.getOptions(context);\n const response = context.switchToHttp().getResponse();\n\n const key = await this.extractKey(context, options);\n\n if (!key) {\n return next.handle();\n }\n\n if (options.skip && (await options.skip(context))) {\n return next.handle();\n }\n\n request[IDEMPOTENCY_HANDLED] = true;\n\n const fingerprint = await this.generateFingerprint(context, options);\n\n let checkResult: Awaited<ReturnType<typeof this.idempotencyService.checkAndLock>>;\n try {\n checkResult = await this.idempotencyService.checkAndLock(key, fingerprint, {\n ttl: options.ttl,\n validateFingerprint: options.validateFingerprint,\n });\n } catch (error) {\n // The idempotency store is unavailable (e.g. Redis is down). Honor the\n // configured errorPolicy: 'fail-open' proceeds without idempotency\n // protection, 'fail-closed' (default) rejects by propagating the error.\n if ((this.config.errorPolicy ?? 'fail-closed') === 'fail-open') {\n this.logger.warn(`Idempotency store unavailable for key \"${key}\"; proceeding without idempotency (fail-open): ${(error as Error).message}`);\n return next.handle();\n }\n throw error;\n }\n\n if (!checkResult.isNew) {\n if (checkResult.fingerprintMismatch) {\n throw new IdempotencyFingerprintMismatchError(key);\n }\n\n const record = checkResult.record!;\n\n if (record.status === 'failed') {\n throw new IdempotencyFailedError(key, record.error);\n }\n\n return this.replayResponse(response, record);\n }\n\n return next.handle().pipe(\n tap({\n next: (data) => {\n Promise.resolve(\n this.idempotencyService.complete(\n key,\n {\n statusCode: response.statusCode,\n body: data,\n headers: this.extractHeaders(response, options),\n },\n { ttl: options.ttl, fingerprint },\n ),\n ).catch((err: Error) => {\n // The client already got its response; never let a store failure\n // become an unhandled rejection. The processing record will\n // expire after lockTimeout and a waiter/retry takes over.\n this.logger.error(`Failed to persist idempotency completion for key \"${key}\": ${err.message}`);\n });\n },\n error: (error) => {\n Promise.resolve(this.idempotencyService.fail(key, error.message, { fingerprint })).catch((err: Error) => {\n this.logger.error(`Failed to persist idempotency failure for key \"${key}\": ${err.message}`);\n });\n },\n }),\n );\n }\n\n private getOptions(context: ExecutionContext): IIdempotentOptions {\n return this.reflector.get<IIdempotentOptions>(IDEMPOTENT_OPTIONS, context.getHandler()) ?? {};\n }\n\n private async extractKey(context: ExecutionContext, options: IIdempotentOptions): Promise<string | null> {\n if (options.keyExtractor) {\n return options.keyExtractor(context);\n }\n\n const request = context.switchToHttp().getRequest();\n const headerName = this.config.headerName ?? 'Idempotency-Key';\n const value = request.headers[headerName.toLowerCase()];\n\n // A duplicated header arrives as an array; use the first occurrence.\n if (Array.isArray(value)) {\n return value[0] ?? null;\n }\n return value ?? null;\n }\n\n private async generateFingerprint(context: ExecutionContext, options: IIdempotentOptions): Promise<string> {\n if (this.config.fingerprintGenerator) {\n return this.config.fingerprintGenerator(context);\n }\n\n const request = context.switchToHttp().getRequest();\n const fields = options.fingerprintFields ?? this.config.fingerprintFields ?? ['method', 'path', 'body'];\n\n const parts: string[] = [];\n\n if (fields.includes('method')) parts.push(request.method);\n if (fields.includes('path')) parts.push(this.getRequestPath(request));\n if (fields.includes('body')) parts.push(JSON.stringify(request.body ?? {}));\n if (fields.includes('query')) parts.push(JSON.stringify(request.query ?? {}));\n\n const data = parts.join('|');\n return this.hash(data);\n }\n\n private getRequestPath(request: unknown): string {\n const httpAdapter = this.adapterHost.httpAdapter;\n const req = request as { path?: string; url?: string };\n return httpAdapter?.getRequestUrl?.(request) ?? req.url ?? req.path ?? '';\n }\n\n private hash(data: string): string {\n return createHash('sha256').update(data).digest('hex');\n }\n\n private replayResponse(response: unknown, record: IIdempotencyRecord): Observable<unknown> {\n const httpAdapter = this.adapterHost.httpAdapter;\n if (!httpAdapter) {\n throw new Error('IdempotencyInterceptor: HttpAdapterHost is not initialized. Ensure the NestJS application has bootstrapped with an HTTP adapter before handling requests.');\n }\n\n httpAdapter.status(response, record.statusCode ?? 200);\n\n if (record.headers) {\n const headers = JSON.parse(record.headers);\n for (const [key, value] of Object.entries(headers)) {\n httpAdapter.setHeader(response, key, value as string);\n }\n }\n\n const body = record.response ? JSON.parse(record.response) : null;\n return of(body);\n }\n\n private extractHeaders(response: { getHeader?: (name: string) => string | number | string[] | undefined }, options: IIdempotentOptions): Record<string, string> | undefined {\n if (!options.cacheHeaders?.length) return undefined;\n\n const headers: Record<string, string> = {};\n for (const name of options.cacheHeaders) {\n const value = response.getHeader?.(name);\n if (value) headers[name] = String(value);\n }\n\n return Object.keys(headers).length > 0 ? headers : undefined;\n }\n}\n","import { Injectable, Inject, Optional } from '@nestjs/common';\n\nimport { IDEMPOTENCY_PLUGIN_OPTIONS, IDEMPOTENCY_STORE } from '../../../shared/constants';\n\n/** Polling interval when waiting for an in-flight idempotent request to complete. */\nconst POLL_INTERVAL_MS = 100;\nimport { IdempotencyFingerprintMismatchError, IdempotencyTimeoutError } from '../../../shared/errors';\nimport { IIdempotencyPluginOptions, IIdempotencyRecord, IIdempotencyCheckResult, IIdempotencyResponse, IIdempotencyOptions } from '../../../shared/types';\nimport { IIdempotencyService } from '../ports/idempotency-service.port';\nimport { IIdempotencyStore } from '../ports/idempotency-store.port';\n\n// Optional metrics integration\nconst METRICS_SERVICE = Symbol.for('METRICS_SERVICE');\n\ninterface IMetricsService {\n incrementCounter(name: string, labels?: Record<string, string>, value?: number): void;\n observeHistogram(name: string, value: number, labels?: Record<string, string>): void;\n}\n\n/**\n * Idempotency service implementation\n */\n@Injectable()\nexport class IdempotencyService implements IIdempotencyService {\n constructor(\n @Inject(IDEMPOTENCY_PLUGIN_OPTIONS)\n private readonly config: IIdempotencyPluginOptions,\n @Inject(IDEMPOTENCY_STORE)\n private readonly store: IIdempotencyStore,\n @Optional() @Inject(METRICS_SERVICE) private readonly metrics?: IMetricsService,\n ) {}\n\n async checkAndLock(key: string, fingerprint: string, options: IIdempotencyOptions = {}): Promise<IIdempotencyCheckResult> {\n const startTime = Date.now();\n const fullKey = this.buildKey(key);\n const lockTimeout = options.lockTimeout ?? this.config.lockTimeout ?? 30000;\n const validateFingerprint = options.validateFingerprint ?? this.config.validateFingerprint ?? true;\n\n const result = await this.store.checkAndLock(fullKey, fingerprint, lockTimeout, validateFingerprint);\n\n if (result.status === 'new') {\n this.metrics?.incrementCounter('redisx_idempotency_requests_total', { status: 'new' });\n this.recordDuration(startTime);\n return { isNew: true };\n }\n\n if (result.status === 'fingerprint_mismatch') {\n this.metrics?.incrementCounter('redisx_idempotency_requests_total', { status: 'mismatch' });\n this.recordDuration(startTime);\n return { isNew: false, fingerprintMismatch: true };\n }\n\n if (result.status === 'processing') {\n // Wait for the in-flight attempt; if its record vanishes (the process\n // died between checkAndLock and complete, so the lock TTL expired),\n // atomically take over instead of failing the caller.\n const outcome = await this.waitForCompletion(fullKey, fingerprint, lockTimeout, validateFingerprint);\n\n if (outcome === 'takeover') {\n this.metrics?.incrementCounter('redisx_idempotency_requests_total', { status: 'takeover' });\n this.recordDuration(startTime);\n return { isNew: true };\n }\n\n this.metrics?.incrementCounter('redisx_idempotency_requests_total', { status: 'replay' });\n this.recordDuration(startTime);\n return { isNew: false, record: outcome };\n }\n\n // completed or failed - replay from cache\n this.metrics?.incrementCounter('redisx_idempotency_requests_total', { status: 'replay' });\n this.recordDuration(startTime);\n return { isNew: false, record: result.record };\n }\n\n private recordDuration(startTime: number): void {\n const duration = (Date.now() - startTime) / 1000;\n this.metrics?.observeHistogram('redisx_idempotency_duration_seconds', duration);\n }\n\n async complete(key: string, response: IIdempotencyResponse, options: IIdempotencyOptions = {}): Promise<void> {\n const fullKey = this.buildKey(key);\n const ttl = options.ttl ?? this.config.defaultTtl ?? 86400;\n\n await this.store.complete(\n fullKey,\n {\n fingerprint: options.fingerprint,\n statusCode: response.statusCode,\n response: JSON.stringify(response.body),\n headers: response.headers ? JSON.stringify(response.headers) : undefined,\n completedAt: Date.now(),\n },\n ttl,\n );\n }\n\n async fail(key: string, error: string, options: IIdempotencyOptions = {}): Promise<void> {\n const fullKey = this.buildKey(key);\n // Failed records are short-lived: keep them only for the lock window so a\n // retry returns the failure briefly, then a fresh attempt is allowed.\n const ttlSeconds = Math.ceil((this.config.lockTimeout ?? 30000) / 1000);\n await this.store.fail(fullKey, error, ttlSeconds, options.fingerprint);\n }\n\n async get(key: string): Promise<IIdempotencyRecord | null> {\n const fullKey = this.buildKey(key);\n return this.store.get(fullKey);\n }\n\n async delete(key: string): Promise<boolean> {\n const fullKey = this.buildKey(key);\n return this.store.delete(fullKey);\n }\n\n /**\n * Polls until the in-flight attempt completes or fails, bounded by\n * waitTimeout.\n *\n * If the processing record VANISHES (the first attempt died between\n * checkAndLock and complete, so its lock TTL expired), the waiter attempts\n * an atomic takeover via checkAndLock: the winner returns 'takeover' (the\n * caller executes the handler itself), losers keep waiting on the new\n * owner's record. This turns a dead first attempt into a self-healing\n * retry instead of surfacing an unexplained error to every waiter.\n *\n * @returns the completed/failed record to replay, or 'takeover' when this\n * caller now owns the key and must execute the request\n */\n private async waitForCompletion(key: string, fingerprint: string, lockTimeoutMs: number, validateFingerprint: boolean): Promise<IIdempotencyRecord | 'takeover'> {\n const waitTimeout = this.config.waitTimeout ?? 60000;\n const startTime = Date.now();\n while (Date.now() - startTime < waitTimeout) {\n const record = await this.store.get(key);\n\n if (!record) {\n // First attempt died — race for the lock (atomic Lua SET-if-absent).\n const retry = await this.store.checkAndLock(key, fingerprint, lockTimeoutMs, validateFingerprint);\n\n if (retry.status === 'new') {\n return 'takeover';\n }\n if (retry.status === 'completed' || retry.status === 'failed') {\n return retry.record!;\n }\n if (retry.status === 'fingerprint_mismatch') {\n // A different request re-claimed the key while we waited.\n throw new IdempotencyFingerprintMismatchError(key);\n }\n // 'processing' — another waiter won the takeover; keep waiting.\n } else if (record.status === 'completed' || record.status === 'failed') {\n return record;\n }\n\n await this.sleep(POLL_INTERVAL_MS);\n }\n\n throw new IdempotencyTimeoutError(key);\n }\n\n private buildKey(key: string): string {\n const prefix = this.config.keyPrefix ?? 'idempotency:';\n return `${prefix}${key}`;\n }\n\n private sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n }\n}\n","/**\n * Inline Lua scripts for idempotency operations.\n *\n * Scripts are stored as inline strings to avoid issues with file reading\n * after build (dist directory doesn't contain .lua files).\n */\n\n/**\n * Check and Lock Lua script for idempotency\n *\n * This script atomically checks if an idempotency key exists and locks it if new.\n *\n * KEYS[1] = idempotency key\n * ARGV[1] = fingerprint\n * ARGV[2] = lock timeout (ms)\n * ARGV[3] = current timestamp (ms)\n * ARGV[4] = validate fingerprint flag ('1' = compare fingerprints, '0' = skip)\n *\n * Returns:\n * - ['new'] - new request, lock acquired\n * - ['fingerprint_mismatch'] - same key, different fingerprint\n * - ['processing'] - another request is processing\n * - [status, statusCode, response, headers, error] - completed/failed record\n */\nexport const CHECK_AND_LOCK_SCRIPT = `\nlocal key = KEYS[1]\nlocal fingerprint = ARGV[1]\nlocal lock_timeout = tonumber(ARGV[2])\nlocal now = tonumber(ARGV[3])\nlocal validate = ARGV[4] ~= '0'\n\n-- Check if key exists\nlocal existing = redis.call('HGETALL', key)\n\nif #existing == 0 then\n -- New request - create lock\n redis.call('HMSET', key,\n 'fingerprint', fingerprint,\n 'status', 'processing',\n 'startedAt', now\n )\n redis.call('PEXPIRE', key, lock_timeout)\n return {'new'}\nend\n\n-- Convert to table\nlocal record = {}\nfor i = 1, #existing, 2 do\n record[existing[i]] = existing[i + 1]\nend\n\n-- Check fingerprint (skipped when validateFingerprint is disabled)\nif validate and record.fingerprint ~= fingerprint then\n return {'fingerprint_mismatch'}\nend\n\n-- Check status\nif record.status == 'processing' then\n -- Check if lock expired (stale)\n local started = tonumber(record.startedAt)\n if now - started > lock_timeout then\n -- Stale lock - take over\n redis.call('HMSET', key,\n 'status', 'processing',\n 'startedAt', now\n )\n redis.call('PEXPIRE', key, lock_timeout)\n return {'new'}\n end\n return {'processing'}\nend\n\n-- Completed or failed - return record\nreturn {\n record.status,\n record.statusCode or '',\n record.response or '',\n record.headers or '',\n record.error or ''\n}\n`.trim();\n","import { Injectable, Inject, OnModuleInit } from '@nestjs/common';\nimport { IRedisDriver } from '@nestjs-redisx/core';\n\nimport { IDEMPOTENCY_REDIS_DRIVER } from '../../../shared/constants';\nimport { IIdempotencyRecord } from '../../../shared/types';\nimport { IIdempotencyStore, ICheckAndLockResult, ICompleteData } from '../../application/ports/idempotency-store.port';\nimport { CHECK_AND_LOCK_SCRIPT } from '../scripts/lua-scripts';\n\n/**\n * Redis-based idempotency store implementation\n */\n@Injectable()\nexport class RedisIdempotencyStoreAdapter implements IIdempotencyStore, OnModuleInit {\n private checkAndLockSha: string | null = null;\n\n constructor(@Inject(IDEMPOTENCY_REDIS_DRIVER) private readonly driver: IRedisDriver) {}\n\n /**\n * Pre-load Lua script on module initialization\n */\n async onModuleInit(): Promise<void> {\n this.checkAndLockSha = await this.driver.scriptLoad(CHECK_AND_LOCK_SCRIPT);\n }\n\n async checkAndLock(key: string, fingerprint: string, lockTimeoutMs: number, validateFingerprint = true): Promise<ICheckAndLockResult> {\n const now = Date.now();\n const rawResult = await this.driver.evalsha(this.checkAndLockSha!, [key], [fingerprint, lockTimeoutMs, now, validateFingerprint ? '1' : '0']);\n\n // Normalize result: node-redis may return Buffer/null elements\n const result = (rawResult as unknown[]).map((v) => (v === null || v === undefined ? '' : String(v)));\n\n const status = result[0];\n\n if (status === 'new') {\n return { status: 'new' };\n }\n\n if (status === 'fingerprint_mismatch') {\n return { status: 'fingerprint_mismatch' };\n }\n\n if (status === 'processing') {\n return { status: 'processing' };\n }\n\n // completed or failed\n return {\n status: status as 'completed' | 'failed',\n record: {\n key,\n fingerprint,\n status: status as 'completed' | 'failed',\n statusCode: result[1] ? parseInt(result[1], 10) : undefined,\n response: result[2] || undefined,\n headers: result[3] || undefined,\n error: result[4] || undefined,\n startedAt: 0, // Not returned from Lua\n },\n };\n }\n\n async complete(key: string, data: ICompleteData, ttlSeconds: number): Promise<void> {\n const fields: Record<string, string> = {\n status: 'completed',\n statusCode: String(data.statusCode),\n response: data.response,\n headers: data.headers || '',\n completedAt: String(data.completedAt),\n };\n // Persist the fingerprint too: if the record expired mid-handler (handler\n // ran longer than lockTimeout), HMSET re-creates the key — without the\n // fingerprint every later replay would be misread as a mismatch (422).\n if (data.fingerprint) {\n fields.fingerprint = data.fingerprint;\n }\n await this.driver.hmset(key, fields);\n await this.driver.expire(key, ttlSeconds);\n }\n\n async fail(key: string, error: string, ttlSeconds: number, fingerprint?: string): Promise<void> {\n const fields: Record<string, string> = {\n status: 'failed',\n error,\n completedAt: String(Date.now()),\n };\n if (fingerprint) {\n fields.fingerprint = fingerprint;\n }\n await this.driver.hmset(key, fields);\n // Set an explicit expiry so the failed record does not rely on the leftover\n // lock TTL; once it expires a fresh attempt with the same key is allowed.\n await this.driver.expire(key, ttlSeconds);\n }\n\n async get(key: string): Promise<IIdempotencyRecord | null> {\n const data = await this.driver.hgetall(key);\n if (!data || Object.keys(data).length === 0) {\n return null;\n }\n\n return {\n key,\n fingerprint: data.fingerprint ?? '',\n status: data.status! as 'processing' | 'completed' | 'failed',\n statusCode: data.statusCode ? parseInt(data.statusCode, 10) : undefined,\n response: data.response || undefined,\n headers: data.headers || undefined,\n startedAt: data.startedAt ? parseInt(data.startedAt, 10) : 0,\n completedAt: data.completedAt ? parseInt(data.completedAt, 10) : undefined,\n error: data.error || undefined,\n };\n }\n\n async delete(key: string): Promise<boolean> {\n const result = await this.driver.del(key);\n return result > 0;\n }\n}\n","/**\n * Idempotency plugin for NestJS RedisX.\n * Provides request deduplication with response replay for idempotent operations.\n */\n\nimport { DynamicModule, ForwardReference, Provider, Type } from '@nestjs/common';\nimport { APP_FILTER, Reflector } from '@nestjs/core';\nimport { IRedisXPlugin, IPluginAsyncOptions, CLIENT_MANAGER, REDIS_CLIENTS_INITIALIZATION, RedisClientManager } from '@nestjs-redisx/core';\n\nimport { version } from '../package.json';\nimport { IdempotencyExceptionFilter } from './idempotency/api/filters/idempotency-exception.filter';\nimport { IdempotencyInterceptor } from './idempotency/api/interceptors/idempotency.interceptor';\nimport { IdempotencyService } from './idempotency/application/services/idempotency.service';\nimport { RedisIdempotencyStoreAdapter } from './idempotency/infrastructure/adapters/redis-idempotency-store.adapter';\nimport { IDEMPOTENCY_PLUGIN_OPTIONS, IDEMPOTENCY_REDIS_DRIVER, IDEMPOTENCY_SERVICE, IDEMPOTENCY_STORE } from './shared/constants';\nimport { IdempotencyConfigError } from './shared/errors';\nimport { IIdempotencyPluginOptions } from './shared/types';\n\nconst DEFAULT_IDEMPOTENCY_CONFIG: Required<Omit<IIdempotencyPluginOptions, 'isGlobal' | 'client' | 'fingerprintGenerator'>> = {\n defaultTtl: 86400,\n keyPrefix: 'idempotency:',\n headerName: 'Idempotency-Key',\n lockTimeout: 30000,\n waitTimeout: 60000,\n validateFingerprint: true,\n fingerprintFields: ['method', 'path', 'body'],\n errorPolicy: 'fail-closed',\n};\n\n/**\n * Idempotency plugin for NestJS RedisX.\n *\n * Provides request deduplication with response replay:\n * - Prevents duplicate processing of same request\n * - Replays successful responses\n * - Handles concurrent requests\n * - Validates request fingerprints\n *\n * @example\n * ```typescript\n * @Module({\n * imports: [\n * RedisModule.forRoot({\n * clients: { host: 'localhost', port: 6379 },\n * plugins: [\n * new IdempotencyPlugin({\n * defaultTtl: 86400,\n * headerName: 'Idempotency-Key',\n * validateFingerprint: true,\n * }),\n * ],\n * }),\n * ],\n * })\n * export class AppModule {}\n * ```\n */\nexport class IdempotencyPlugin implements IRedisXPlugin {\n readonly name = 'idempotency';\n readonly version: string = version;\n readonly description = 'Request deduplication with response replay for idempotent operations';\n\n private asyncOptions?: IPluginAsyncOptions<IIdempotencyPluginOptions>;\n\n constructor(private readonly options: IIdempotencyPluginOptions = {}) {}\n\n static registerAsync(asyncOptions: IPluginAsyncOptions<IIdempotencyPluginOptions>): IdempotencyPlugin {\n const plugin = new IdempotencyPlugin();\n plugin.asyncOptions = asyncOptions;\n return plugin;\n }\n\n private static mergeDefaults(options: IIdempotencyPluginOptions): IIdempotencyPluginOptions {\n const merged: IIdempotencyPluginOptions = {\n client: options.client,\n defaultTtl: options.defaultTtl ?? DEFAULT_IDEMPOTENCY_CONFIG.defaultTtl,\n keyPrefix: options.keyPrefix ?? DEFAULT_IDEMPOTENCY_CONFIG.keyPrefix,\n headerName: options.headerName ?? DEFAULT_IDEMPOTENCY_CONFIG.headerName,\n lockTimeout: options.lockTimeout ?? DEFAULT_IDEMPOTENCY_CONFIG.lockTimeout,\n waitTimeout: options.waitTimeout ?? DEFAULT_IDEMPOTENCY_CONFIG.waitTimeout,\n validateFingerprint: options.validateFingerprint ?? DEFAULT_IDEMPOTENCY_CONFIG.validateFingerprint,\n fingerprintFields: options.fingerprintFields ?? DEFAULT_IDEMPOTENCY_CONFIG.fingerprintFields,\n errorPolicy: options.errorPolicy ?? DEFAULT_IDEMPOTENCY_CONFIG.errorPolicy,\n fingerprintGenerator: options.fingerprintGenerator,\n };\n\n // Fail fast: non-positive timing values silently break TTL semantics\n // (e.g. lockTimeout 0 -> the processing record is deleted immediately).\n for (const knob of ['defaultTtl', 'lockTimeout', 'waitTimeout'] as const) {\n const value = merged[knob]!;\n if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) {\n throw new IdempotencyConfigError(`${knob} must be a positive number (got ${String(value)})`);\n }\n }\n\n return merged;\n }\n\n getImports(): Array<Type<unknown> | DynamicModule | ForwardReference> {\n return this.asyncOptions?.imports ?? [];\n }\n\n getProviders(): Provider[] {\n const optionsProvider: Provider = this.asyncOptions\n ? {\n provide: IDEMPOTENCY_PLUGIN_OPTIONS,\n useFactory: async (...args: unknown[]) => {\n const userOptions = await this.asyncOptions!.useFactory(...args);\n return IdempotencyPlugin.mergeDefaults(userOptions);\n },\n inject: this.asyncOptions.inject || [],\n }\n : {\n provide: IDEMPOTENCY_PLUGIN_OPTIONS,\n useValue: IdempotencyPlugin.mergeDefaults(this.options),\n };\n\n return [\n optionsProvider,\n // Plugin-specific Redis driver (resolves named client)\n {\n provide: IDEMPOTENCY_REDIS_DRIVER,\n useFactory: async (manager: RedisClientManager, _init: void, options: IIdempotencyPluginOptions) => {\n const clientName = options.client ?? 'default';\n try {\n return await manager.getClient(clientName);\n } catch {\n throw new Error(`IdempotencyPlugin: Redis client \"${clientName}\" not found. ` + `Available clients are configured in RedisModule.forRoot({ clients: { ... } }). ` + `Either add a \"${clientName}\" client or remove the \"client\" option to use the default connection.`);\n }\n },\n inject: [CLIENT_MANAGER, REDIS_CLIENTS_INITIALIZATION, IDEMPOTENCY_PLUGIN_OPTIONS],\n },\n { provide: IDEMPOTENCY_STORE, useClass: RedisIdempotencyStoreAdapter },\n { provide: IDEMPOTENCY_SERVICE, useClass: IdempotencyService },\n // Reflector is needed for @Idempotent decorator metadata\n Reflector,\n IdempotencyInterceptor,\n // Global exception filter to map idempotency errors to proper HTTP codes\n // (otherwise they surface as 500 since they are not HttpExceptions)\n { provide: APP_FILTER, useClass: IdempotencyExceptionFilter },\n ];\n }\n\n getExports(): Array<string | symbol | Provider> {\n return [IDEMPOTENCY_PLUGIN_OPTIONS, IDEMPOTENCY_SERVICE, IdempotencyInterceptor];\n }\n}\n"]}
package/dist/index.mjs CHANGED
@@ -16,7 +16,7 @@ var __decorateClass = (decorators, target, key, kind) => {
16
16
  var __decorateParam = (index, decorator) => (target, key) => decorator(target, key, index);
17
17
 
18
18
  // package.json
19
- var version = "1.6.0";
19
+ var version = "1.6.1";
20
20
  var IdempotencyError = class extends RedisXError {
21
21
  constructor(message, code, idempotencyKey, cause) {
22
22
  super(message, code, cause, { idempotencyKey });
@@ -48,6 +48,11 @@ var IdempotencyRecordNotFoundError = class extends IdempotencyError {
48
48
  super(`Idempotency record not found for key "${key}"`, ErrorCode.OP_KEY_NOT_FOUND, key);
49
49
  }
50
50
  };
51
+ var IdempotencyConfigError = class extends IdempotencyError {
52
+ constructor(message) {
53
+ super(message, ErrorCode.CFG_INVALID, "");
54
+ }
55
+ };
51
56
 
52
57
  // src/idempotency/api/filters/idempotency-exception.filter.ts
53
58
  var IdempotencyExceptionFilter = class {
@@ -76,6 +81,7 @@ var IdempotencyExceptionFilter = class {
76
81
  * - Reused key with a different request -> 422 Unprocessable Entity
77
82
  * - Concurrent request still in progress (timeout) -> 409 Conflict
78
83
  * - Previous request with the same key failed -> 409 Conflict
84
+ * - In-flight record vanished mid-wait -> 409 Conflict (retryable)
79
85
  * - Anything else (unexpected) -> 500 Internal Server Error
80
86
  */
81
87
  resolveStatus(exception) {
@@ -91,6 +97,9 @@ var IdempotencyExceptionFilter = class {
91
97
  if (exception instanceof IdempotencyFailedError) {
92
98
  return HttpStatus.CONFLICT;
93
99
  }
100
+ if (exception instanceof IdempotencyRecordNotFoundError) {
101
+ return HttpStatus.CONFLICT;
102
+ }
94
103
  return HttpStatus.INTERNAL_SERVER_ERROR;
95
104
  }
96
105
  reasonPhrase(status) {
@@ -122,6 +131,7 @@ function Idempotent(options = {}) {
122
131
  }
123
132
 
124
133
  // src/idempotency/api/interceptors/idempotency.interceptor.ts
134
+ var IDEMPOTENCY_HANDLED = /* @__PURE__ */ Symbol("redisx.idempotencyHandled");
125
135
  var IdempotencyInterceptor = class {
126
136
  constructor(idempotencyService, config, reflector, adapterHost) {
127
137
  this.idempotencyService = idempotencyService;
@@ -131,6 +141,10 @@ var IdempotencyInterceptor = class {
131
141
  }
132
142
  logger = new Logger(IdempotencyInterceptor.name);
133
143
  async intercept(context, next) {
144
+ const request = context.switchToHttp().getRequest();
145
+ if (request[IDEMPOTENCY_HANDLED]) {
146
+ return next.handle();
147
+ }
134
148
  const options = this.getOptions(context);
135
149
  const response = context.switchToHttp().getResponse();
136
150
  const key = await this.extractKey(context, options);
@@ -140,11 +154,13 @@ var IdempotencyInterceptor = class {
140
154
  if (options.skip && await options.skip(context)) {
141
155
  return next.handle();
142
156
  }
157
+ request[IDEMPOTENCY_HANDLED] = true;
143
158
  const fingerprint = await this.generateFingerprint(context, options);
144
159
  let checkResult;
145
160
  try {
146
161
  checkResult = await this.idempotencyService.checkAndLock(key, fingerprint, {
147
- ttl: options.ttl
162
+ ttl: options.ttl,
163
+ validateFingerprint: options.validateFingerprint
148
164
  });
149
165
  } catch (error) {
150
166
  if ((this.config.errorPolicy ?? "fail-closed") === "fail-open") {
@@ -166,18 +182,24 @@ var IdempotencyInterceptor = class {
166
182
  return next.handle().pipe(
167
183
  tap({
168
184
  next: (data) => {
169
- void this.idempotencyService.complete(
170
- key,
171
- {
172
- statusCode: response.statusCode,
173
- body: data,
174
- headers: this.extractHeaders(response, options)
175
- },
176
- { ttl: options.ttl }
177
- );
185
+ Promise.resolve(
186
+ this.idempotencyService.complete(
187
+ key,
188
+ {
189
+ statusCode: response.statusCode,
190
+ body: data,
191
+ headers: this.extractHeaders(response, options)
192
+ },
193
+ { ttl: options.ttl, fingerprint }
194
+ )
195
+ ).catch((err) => {
196
+ this.logger.error(`Failed to persist idempotency completion for key "${key}": ${err.message}`);
197
+ });
178
198
  },
179
199
  error: (error) => {
180
- void this.idempotencyService.fail(key, error.message);
200
+ Promise.resolve(this.idempotencyService.fail(key, error.message, { fingerprint })).catch((err) => {
201
+ this.logger.error(`Failed to persist idempotency failure for key "${key}": ${err.message}`);
202
+ });
181
203
  }
182
204
  })
183
205
  );
@@ -191,7 +213,11 @@ var IdempotencyInterceptor = class {
191
213
  }
192
214
  const request = context.switchToHttp().getRequest();
193
215
  const headerName = this.config.headerName ?? "Idempotency-Key";
194
- return request.headers[headerName.toLowerCase()] ?? null;
216
+ const value = request.headers[headerName.toLowerCase()];
217
+ if (Array.isArray(value)) {
218
+ return value[0] ?? null;
219
+ }
220
+ return value ?? null;
195
221
  }
196
222
  async generateFingerprint(context, options) {
197
223
  if (this.config.fingerprintGenerator) {
@@ -259,7 +285,8 @@ var IdempotencyService = class {
259
285
  const startTime = Date.now();
260
286
  const fullKey = this.buildKey(key);
261
287
  const lockTimeout = options.lockTimeout ?? this.config.lockTimeout ?? 3e4;
262
- const result = await this.store.checkAndLock(fullKey, fingerprint, lockTimeout);
288
+ const validateFingerprint = options.validateFingerprint ?? this.config.validateFingerprint ?? true;
289
+ const result = await this.store.checkAndLock(fullKey, fingerprint, lockTimeout, validateFingerprint);
263
290
  if (result.status === "new") {
264
291
  this.metrics?.incrementCounter("redisx_idempotency_requests_total", { status: "new" });
265
292
  this.recordDuration(startTime);
@@ -271,10 +298,15 @@ var IdempotencyService = class {
271
298
  return { isNew: false, fingerprintMismatch: true };
272
299
  }
273
300
  if (result.status === "processing") {
274
- const record = await this.waitForCompletion(fullKey);
301
+ const outcome = await this.waitForCompletion(fullKey, fingerprint, lockTimeout, validateFingerprint);
302
+ if (outcome === "takeover") {
303
+ this.metrics?.incrementCounter("redisx_idempotency_requests_total", { status: "takeover" });
304
+ this.recordDuration(startTime);
305
+ return { isNew: true };
306
+ }
275
307
  this.metrics?.incrementCounter("redisx_idempotency_requests_total", { status: "replay" });
276
308
  this.recordDuration(startTime);
277
- return { isNew: false, record };
309
+ return { isNew: false, record: outcome };
278
310
  }
279
311
  this.metrics?.incrementCounter("redisx_idempotency_requests_total", { status: "replay" });
280
312
  this.recordDuration(startTime);
@@ -290,6 +322,7 @@ var IdempotencyService = class {
290
322
  await this.store.complete(
291
323
  fullKey,
292
324
  {
325
+ fingerprint: options.fingerprint,
293
326
  statusCode: response.statusCode,
294
327
  response: JSON.stringify(response.body),
295
328
  headers: response.headers ? JSON.stringify(response.headers) : void 0,
@@ -298,10 +331,10 @@ var IdempotencyService = class {
298
331
  ttl
299
332
  );
300
333
  }
301
- async fail(key, error) {
334
+ async fail(key, error, options = {}) {
302
335
  const fullKey = this.buildKey(key);
303
336
  const ttlSeconds = Math.ceil((this.config.lockTimeout ?? 3e4) / 1e3);
304
- await this.store.fail(fullKey, error, ttlSeconds);
337
+ await this.store.fail(fullKey, error, ttlSeconds, options.fingerprint);
305
338
  }
306
339
  async get(key) {
307
340
  const fullKey = this.buildKey(key);
@@ -311,15 +344,37 @@ var IdempotencyService = class {
311
344
  const fullKey = this.buildKey(key);
312
345
  return this.store.delete(fullKey);
313
346
  }
314
- async waitForCompletion(key) {
347
+ /**
348
+ * Polls until the in-flight attempt completes or fails, bounded by
349
+ * waitTimeout.
350
+ *
351
+ * If the processing record VANISHES (the first attempt died between
352
+ * checkAndLock and complete, so its lock TTL expired), the waiter attempts
353
+ * an atomic takeover via checkAndLock: the winner returns 'takeover' (the
354
+ * caller executes the handler itself), losers keep waiting on the new
355
+ * owner's record. This turns a dead first attempt into a self-healing
356
+ * retry instead of surfacing an unexplained error to every waiter.
357
+ *
358
+ * @returns the completed/failed record to replay, or 'takeover' when this
359
+ * caller now owns the key and must execute the request
360
+ */
361
+ async waitForCompletion(key, fingerprint, lockTimeoutMs, validateFingerprint) {
315
362
  const waitTimeout = this.config.waitTimeout ?? 6e4;
316
363
  const startTime = Date.now();
317
364
  while (Date.now() - startTime < waitTimeout) {
318
365
  const record = await this.store.get(key);
319
366
  if (!record) {
320
- throw new IdempotencyRecordNotFoundError(key);
321
- }
322
- if (record.status === "completed" || record.status === "failed") {
367
+ const retry = await this.store.checkAndLock(key, fingerprint, lockTimeoutMs, validateFingerprint);
368
+ if (retry.status === "new") {
369
+ return "takeover";
370
+ }
371
+ if (retry.status === "completed" || retry.status === "failed") {
372
+ return retry.record;
373
+ }
374
+ if (retry.status === "fingerprint_mismatch") {
375
+ throw new IdempotencyFingerprintMismatchError(key);
376
+ }
377
+ } else if (record.status === "completed" || record.status === "failed") {
323
378
  return record;
324
379
  }
325
380
  await this.sleep(POLL_INTERVAL_MS);
@@ -348,6 +403,7 @@ local key = KEYS[1]
348
403
  local fingerprint = ARGV[1]
349
404
  local lock_timeout = tonumber(ARGV[2])
350
405
  local now = tonumber(ARGV[3])
406
+ local validate = ARGV[4] ~= '0'
351
407
 
352
408
  -- Check if key exists
353
409
  local existing = redis.call('HGETALL', key)
@@ -369,8 +425,8 @@ for i = 1, #existing, 2 do
369
425
  record[existing[i]] = existing[i + 1]
370
426
  end
371
427
 
372
- -- Check fingerprint
373
- if record.fingerprint ~= fingerprint then
428
+ -- Check fingerprint (skipped when validateFingerprint is disabled)
429
+ if validate and record.fingerprint ~= fingerprint then
374
430
  return {'fingerprint_mismatch'}
375
431
  end
376
432
 
@@ -412,9 +468,9 @@ var RedisIdempotencyStoreAdapter = class {
412
468
  async onModuleInit() {
413
469
  this.checkAndLockSha = await this.driver.scriptLoad(CHECK_AND_LOCK_SCRIPT);
414
470
  }
415
- async checkAndLock(key, fingerprint, lockTimeoutMs) {
471
+ async checkAndLock(key, fingerprint, lockTimeoutMs, validateFingerprint = true) {
416
472
  const now = Date.now();
417
- const rawResult = await this.driver.evalsha(this.checkAndLockSha, [key], [fingerprint, lockTimeoutMs, now]);
473
+ const rawResult = await this.driver.evalsha(this.checkAndLockSha, [key], [fingerprint, lockTimeoutMs, now, validateFingerprint ? "1" : "0"]);
418
474
  const result = rawResult.map((v) => v === null || v === void 0 ? "" : String(v));
419
475
  const status = result[0];
420
476
  if (status === "new") {
@@ -442,21 +498,29 @@ var RedisIdempotencyStoreAdapter = class {
442
498
  };
443
499
  }
444
500
  async complete(key, data, ttlSeconds) {
445
- await this.driver.hmset(key, {
501
+ const fields = {
446
502
  status: "completed",
447
503
  statusCode: String(data.statusCode),
448
504
  response: data.response,
449
505
  headers: data.headers || "",
450
506
  completedAt: String(data.completedAt)
451
- });
507
+ };
508
+ if (data.fingerprint) {
509
+ fields.fingerprint = data.fingerprint;
510
+ }
511
+ await this.driver.hmset(key, fields);
452
512
  await this.driver.expire(key, ttlSeconds);
453
513
  }
454
- async fail(key, error, ttlSeconds) {
455
- await this.driver.hmset(key, {
514
+ async fail(key, error, ttlSeconds, fingerprint) {
515
+ const fields = {
456
516
  status: "failed",
457
517
  error,
458
518
  completedAt: String(Date.now())
459
- });
519
+ };
520
+ if (fingerprint) {
521
+ fields.fingerprint = fingerprint;
522
+ }
523
+ await this.driver.hmset(key, fields);
460
524
  await this.driver.expire(key, ttlSeconds);
461
525
  }
462
526
  async get(key) {
@@ -466,12 +530,12 @@ var RedisIdempotencyStoreAdapter = class {
466
530
  }
467
531
  return {
468
532
  key,
469
- fingerprint: data.fingerprint,
533
+ fingerprint: data.fingerprint ?? "",
470
534
  status: data.status,
471
535
  statusCode: data.statusCode ? parseInt(data.statusCode, 10) : void 0,
472
536
  response: data.response || void 0,
473
537
  headers: data.headers || void 0,
474
- startedAt: parseInt(data.startedAt, 10),
538
+ startedAt: data.startedAt ? parseInt(data.startedAt, 10) : 0,
475
539
  completedAt: data.completedAt ? parseInt(data.completedAt, 10) : void 0,
476
540
  error: data.error || void 0
477
541
  };
@@ -511,7 +575,7 @@ var IdempotencyPlugin = class _IdempotencyPlugin {
511
575
  return plugin;
512
576
  }
513
577
  static mergeDefaults(options) {
514
- return {
578
+ const merged = {
515
579
  client: options.client,
516
580
  defaultTtl: options.defaultTtl ?? DEFAULT_IDEMPOTENCY_CONFIG.defaultTtl,
517
581
  keyPrefix: options.keyPrefix ?? DEFAULT_IDEMPOTENCY_CONFIG.keyPrefix,
@@ -523,6 +587,13 @@ var IdempotencyPlugin = class _IdempotencyPlugin {
523
587
  errorPolicy: options.errorPolicy ?? DEFAULT_IDEMPOTENCY_CONFIG.errorPolicy,
524
588
  fingerprintGenerator: options.fingerprintGenerator
525
589
  };
590
+ for (const knob of ["defaultTtl", "lockTimeout", "waitTimeout"]) {
591
+ const value = merged[knob];
592
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
593
+ throw new IdempotencyConfigError(`${knob} must be a positive number (got ${String(value)})`);
594
+ }
595
+ }
596
+ return merged;
526
597
  }
527
598
  getImports() {
528
599
  return this.asyncOptions?.imports ?? [];
@@ -569,6 +640,6 @@ var IdempotencyPlugin = class _IdempotencyPlugin {
569
640
  }
570
641
  };
571
642
 
572
- export { IDEMPOTENCY_PLUGIN_OPTIONS, IDEMPOTENCY_REDIS_DRIVER, IDEMPOTENCY_SERVICE, IDEMPOTENCY_STORE, IDEMPOTENT_OPTIONS, IdempotencyError, IdempotencyFailedError, IdempotencyFingerprintMismatchError, IdempotencyInterceptor, IdempotencyKeyRequiredError, IdempotencyPlugin, IdempotencyRecordNotFoundError, IdempotencyService, IdempotencyTimeoutError, Idempotent };
643
+ export { IDEMPOTENCY_PLUGIN_OPTIONS, IDEMPOTENCY_REDIS_DRIVER, IDEMPOTENCY_SERVICE, IDEMPOTENCY_STORE, IDEMPOTENT_OPTIONS, IdempotencyConfigError, IdempotencyError, IdempotencyFailedError, IdempotencyFingerprintMismatchError, IdempotencyInterceptor, IdempotencyKeyRequiredError, IdempotencyPlugin, IdempotencyRecordNotFoundError, IdempotencyService, IdempotencyTimeoutError, Idempotent };
573
644
  //# sourceMappingURL=index.mjs.map
574
645
  //# sourceMappingURL=index.mjs.map