@nestjs-redisx/idempotency 1.5.1 → 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.
- package/dist/idempotency/api/filters/idempotency-exception.filter.d.ts +1 -0
- package/dist/idempotency/api/filters/idempotency-exception.filter.d.ts.map +1 -1
- package/dist/idempotency/api/interceptors/idempotency.interceptor.d.ts.map +1 -1
- package/dist/idempotency/application/ports/idempotency-service.port.d.ts +1 -1
- package/dist/idempotency/application/ports/idempotency-service.port.d.ts.map +1 -1
- package/dist/idempotency/application/ports/idempotency-store.port.d.ts +4 -2
- package/dist/idempotency/application/ports/idempotency-store.port.d.ts.map +1 -1
- package/dist/idempotency/application/services/idempotency.service.d.ts +15 -1
- package/dist/idempotency/application/services/idempotency.service.d.ts.map +1 -1
- package/dist/idempotency/infrastructure/adapters/redis-idempotency-store.adapter.d.ts +2 -2
- package/dist/idempotency/infrastructure/adapters/redis-idempotency-store.adapter.d.ts.map +1 -1
- package/dist/idempotency/infrastructure/scripts/lua-scripts.d.ts +1 -0
- package/dist/idempotency/infrastructure/scripts/lua-scripts.d.ts.map +1 -1
- package/dist/idempotency.plugin.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +106 -34
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +106 -35
- package/dist/index.mjs.map +1 -1
- package/dist/shared/errors/index.d.ts +7 -0
- package/dist/shared/errors/index.d.ts.map +1 -1
- package/dist/shared/types/index.d.ts +13 -1
- package/dist/shared/types/index.d.ts.map +1 -1
- package/package.json +7 -4
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.5.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 \"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 \"@vitest/coverage-v8\": \"^1.6.0\"\n },\n \"publishConfig\": {\n \"access\": \"public\"\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.
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
321
|
-
|
|
322
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|