@nestjs-redisx/idempotency 1.3.1 → 1.5.0

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.
@@ -0,0 +1,30 @@
1
+ import { ExceptionFilter, ArgumentsHost } from '@nestjs/common';
2
+ import { HttpAdapterHost } from '@nestjs/core';
3
+ import { IdempotencyError } from '../../../shared/errors';
4
+ /**
5
+ * Exception filter for idempotency errors.
6
+ *
7
+ * Without this filter the idempotency errors (which extend `RedisXError`, not
8
+ * `HttpException`) would surface as `500 Internal Server Error`. That is wrong
9
+ * for situations that are part of normal idempotent-request handling — a reused
10
+ * key with a different body, a still-in-progress concurrent request, or a key
11
+ * whose previous attempt failed. This filter maps each error to an appropriate
12
+ * 4xx status while leaving genuinely unexpected errors as 500.
13
+ */
14
+ export declare class IdempotencyExceptionFilter implements ExceptionFilter {
15
+ private readonly adapterHost;
16
+ constructor(adapterHost: HttpAdapterHost);
17
+ catch(exception: IdempotencyError, host: ArgumentsHost): void;
18
+ /**
19
+ * Maps an idempotency error to its HTTP status code.
20
+ *
21
+ * - Missing/invalid key -> 400 Bad Request
22
+ * - Reused key with a different request -> 422 Unprocessable Entity
23
+ * - Concurrent request still in progress (timeout) -> 409 Conflict
24
+ * - Previous request with the same key failed -> 409 Conflict
25
+ * - Anything else (unexpected) -> 500 Internal Server Error
26
+ */
27
+ private resolveStatus;
28
+ private reasonPhrase;
29
+ }
30
+ //# sourceMappingURL=idempotency-exception.filter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"idempotency-exception.filter.d.ts","sourceRoot":"","sources":["../../../../src/idempotency/api/filters/idempotency-exception.filter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAS,eAAe,EAAE,aAAa,EAAsB,MAAM,gBAAgB,CAAC;AAC3F,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAE/C,OAAO,EAAE,gBAAgB,EAAqH,MAAM,wBAAwB,CAAC;AAE7K;;;;;;;;;GASG;AACH,qBACa,0BAA2B,YAAW,eAAe;IAC3B,OAAO,CAAC,QAAQ,CAAC,WAAW;gBAAX,WAAW,EAAE,eAAe;IAElF,KAAK,CAAC,SAAS,EAAE,gBAAgB,EAAE,IAAI,EAAE,aAAa,GAAG,IAAI;IAmB7D;;;;;;;;OAQG;IACH,OAAO,CAAC,aAAa;IAgBrB,OAAO,CAAC,YAAY;CAYrB"}
@@ -8,6 +8,7 @@ export declare class IdempotencyInterceptor implements NestInterceptor {
8
8
  private readonly config;
9
9
  private readonly reflector;
10
10
  private readonly adapterHost;
11
+ private readonly logger;
11
12
  constructor(idempotencyService: IIdempotencyService, config: IIdempotencyPluginOptions, reflector: Reflector, adapterHost: HttpAdapterHost);
12
13
  intercept(context: ExecutionContext, next: CallHandler): Promise<Observable<unknown>>;
13
14
  private getOptions;
@@ -1 +1 @@
1
- {"version":3,"file":"idempotency.interceptor.d.ts","sourceRoot":"","sources":["../../../../src/idempotency/api/interceptors/idempotency.interceptor.ts"],"names":[],"mappings":"AAEA,OAAO,EAAc,eAAe,EAAE,gBAAgB,EAAE,WAAW,EAAU,MAAM,gBAAgB,CAAC;AACpG,OAAO,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC1D,OAAO,EAAE,UAAU,EAAM,MAAM,MAAM,CAAC;AAKtC,OAAO,EAAE,yBAAyB,EAAsB,MAAM,uBAAuB,CAAC;AACtF,OAAO,EAAE,mBAAmB,EAAE,MAAM,kDAAkD,CAAC;AAGvF,qBACa,sBAAuB,YAAW,eAAe;IAE7B,OAAO,CAAC,QAAQ,CAAC,kBAAkB;IAC5B,OAAO,CAAC,QAAQ,CAAC,MAAM;IACxC,OAAO,CAAC,QAAQ,CAAC,SAAS;IACpB,OAAO,CAAC,QAAQ,CAAC,WAAW;gBAHP,kBAAkB,EAAE,mBAAmB,EAChC,MAAM,EAAE,yBAAyB,EAClD,SAAS,EAAE,SAAS,EACd,WAAW,EAAE,eAAe;IAGlE,SAAS,CAAC,OAAO,EAAE,gBAAgB,EAAE,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC;IAsD3F,OAAO,CAAC,UAAU;YAIJ,UAAU;YAWV,mBAAmB;IAmBjC,OAAO,CAAC,cAAc;IAMtB,OAAO,CAAC,IAAI;IAIZ,OAAO,CAAC,cAAc;IAmBtB,OAAO,CAAC,cAAc;CAWvB"}
1
+ {"version":3,"file":"idempotency.interceptor.d.ts","sourceRoot":"","sources":["../../../../src/idempotency/api/interceptors/idempotency.interceptor.ts"],"names":[],"mappings":"AAEA,OAAO,EAAc,eAAe,EAAE,gBAAgB,EAAE,WAAW,EAAkB,MAAM,gBAAgB,CAAC;AAC5G,OAAO,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC1D,OAAO,EAAE,UAAU,EAAM,MAAM,MAAM,CAAC;AAKtC,OAAO,EAAE,yBAAyB,EAAsB,MAAM,uBAAuB,CAAC;AACtF,OAAO,EAAE,mBAAmB,EAAE,MAAM,kDAAkD,CAAC;AAGvF,qBACa,sBAAuB,YAAW,eAAe;IAI7B,OAAO,CAAC,QAAQ,CAAC,kBAAkB;IAC5B,OAAO,CAAC,QAAQ,CAAC,MAAM;IACxC,OAAO,CAAC,QAAQ,CAAC,SAAS;IACpB,OAAO,CAAC,QAAQ,CAAC,WAAW;IANvD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA2C;gBAGlB,kBAAkB,EAAE,mBAAmB,EAChC,MAAM,EAAE,yBAAyB,EAClD,SAAS,EAAE,SAAS,EACd,WAAW,EAAE,eAAe;IAGlE,SAAS,CAAC,OAAO,EAAE,gBAAgB,EAAE,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC;IAkE3F,OAAO,CAAC,UAAU;YAIJ,UAAU;YAWV,mBAAmB;IAmBjC,OAAO,CAAC,cAAc;IAMtB,OAAO,CAAC,IAAI;IAIZ,OAAO,CAAC,cAAc;IAmBtB,OAAO,CAAC,cAAc;CAWvB"}
@@ -47,8 +47,10 @@ export interface IIdempotencyStore {
47
47
  *
48
48
  * @param key - Redis key
49
49
  * @param error - Error message
50
+ * @param ttlSeconds - TTL in seconds after which the failed record expires,
51
+ * allowing a fresh attempt with the same key
50
52
  */
51
- fail(key: string, error: string): Promise<void>;
53
+ fail(key: string, error: string, ttlSeconds: number): Promise<void>;
52
54
  /**
53
55
  * Get record by key.
54
56
  *
@@ -1 +1 @@
1
- {"version":3,"file":"idempotency-store.port.d.ts","sourceRoot":"","sources":["../../../../src/idempotency/application/ports/idempotency-store.port.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAE3D;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,0BAA0B;IAC1B,MAAM,EAAE,KAAK,GAAG,YAAY,GAAG,WAAW,GAAG,QAAQ,GAAG,sBAAsB,CAAC;IAE/E,uBAAuB;IACvB,MAAM,CAAC,EAAE,kBAAkB,CAAC;CAC7B;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,uBAAuB;IACvB,UAAU,EAAE,MAAM,CAAC;IAEnB,kCAAkC;IAClC,QAAQ,EAAE,MAAM,CAAC;IAEjB,qCAAqC;IACrC,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB,gCAAgC;IAChC,WAAW,EAAE,MAAM,CAAC;CACrB;AAED;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC;;;;;;;OAOG;IACH,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAAC;IAEpG;;;;;;OAMG;IACH,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAE9E;;;;;OAKG;IACH,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAEhD;;;;;OAKG;IACH,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,CAAC;IAErD;;;;;OAKG;IACH,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACvC"}
1
+ {"version":3,"file":"idempotency-store.port.d.ts","sourceRoot":"","sources":["../../../../src/idempotency/application/ports/idempotency-store.port.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAE3D;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,0BAA0B;IAC1B,MAAM,EAAE,KAAK,GAAG,YAAY,GAAG,WAAW,GAAG,QAAQ,GAAG,sBAAsB,CAAC;IAE/E,uBAAuB;IACvB,MAAM,CAAC,EAAE,kBAAkB,CAAC;CAC7B;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,uBAAuB;IACvB,UAAU,EAAE,MAAM,CAAC;IAEnB,kCAAkC;IAClC,QAAQ,EAAE,MAAM,CAAC;IAEjB,qCAAqC;IACrC,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB,gCAAgC;IAChC,WAAW,EAAE,MAAM,CAAC;CACrB;AAED;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC;;;;;;;OAOG;IACH,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAAC;IAEpG;;;;;;OAMG;IACH,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAE9E;;;;;;;OAOG;IACH,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAEpE;;;;;OAKG;IACH,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,CAAC;IAErD;;;;;OAKG;IACH,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACvC"}
@@ -1 +1 @@
1
- {"version":3,"file":"idempotency.service.d.ts","sourceRoot":"","sources":["../../../../src/idempotency/application/services/idempotency.service.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,yBAAyB,EAAE,kBAAkB,EAAE,uBAAuB,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAC1J,OAAO,EAAE,mBAAmB,EAAE,MAAM,mCAAmC,CAAC;AACxE,OAAO,EAAE,iBAAiB,EAAE,MAAM,iCAAiC,CAAC;AAKpE,UAAU,eAAe;IACvB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtF,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CAAC;CACtF;AAED;;GAEG;AACH,qBACa,kBAAmB,YAAW,mBAAmB;IAG1D,OAAO,CAAC,QAAQ,CAAC,MAAM;IAEvB,OAAO,CAAC,QAAQ,CAAC,KAAK;IACe,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC;gBAH7C,MAAM,EAAE,yBAAyB,EAEjC,KAAK,EAAE,iBAAiB,EACa,OAAO,CAAC,EAAE,eAAe,YAAA;IAG3E,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,OAAO,GAAE,mBAAwB,GAAG,OAAO,CAAC,uBAAuB,CAAC;IAiCzH,OAAO,CAAC,cAAc;IAKhB,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,oBAAoB,EAAE,OAAO,GAAE,mBAAwB,GAAG,OAAO,CAAC,IAAI,CAAC;IAgBvG,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAK/C,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC;IAKpD,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;YAK7B,iBAAiB;IAoB/B,OAAO,CAAC,QAAQ;IAKhB,OAAO,CAAC,KAAK;CAGd"}
1
+ {"version":3,"file":"idempotency.service.d.ts","sourceRoot":"","sources":["../../../../src/idempotency/application/services/idempotency.service.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,yBAAyB,EAAE,kBAAkB,EAAE,uBAAuB,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAC1J,OAAO,EAAE,mBAAmB,EAAE,MAAM,mCAAmC,CAAC;AACxE,OAAO,EAAE,iBAAiB,EAAE,MAAM,iCAAiC,CAAC;AAKpE,UAAU,eAAe;IACvB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtF,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CAAC;CACtF;AAED;;GAEG;AACH,qBACa,kBAAmB,YAAW,mBAAmB;IAG1D,OAAO,CAAC,QAAQ,CAAC,MAAM;IAEvB,OAAO,CAAC,QAAQ,CAAC,KAAK;IACe,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC;gBAH7C,MAAM,EAAE,yBAAyB,EAEjC,KAAK,EAAE,iBAAiB,EACa,OAAO,CAAC,EAAE,eAAe,YAAA;IAG3E,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,OAAO,GAAE,mBAAwB,GAAG,OAAO,CAAC,uBAAuB,CAAC;IAiCzH,OAAO,CAAC,cAAc;IAKhB,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,oBAAoB,EAAE,OAAO,GAAE,mBAAwB,GAAG,OAAO,CAAC,IAAI,CAAC;IAgBvG,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAQ/C,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC;IAKpD,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;YAK7B,iBAAiB;IAoB/B,OAAO,CAAC,QAAQ;IAKhB,OAAO,CAAC,KAAK;CAGd"}
@@ -15,7 +15,7 @@ export declare class RedisIdempotencyStoreAdapter implements IIdempotencyStore,
15
15
  onModuleInit(): Promise<void>;
16
16
  checkAndLock(key: string, fingerprint: string, lockTimeoutMs: number): Promise<ICheckAndLockResult>;
17
17
  complete(key: string, data: ICompleteData, ttlSeconds: number): Promise<void>;
18
- fail(key: string, error: string): Promise<void>;
18
+ fail(key: string, error: string, ttlSeconds: number): Promise<void>;
19
19
  get(key: string): Promise<IIdempotencyRecord | null>;
20
20
  delete(key: string): Promise<boolean>;
21
21
  }
@@ -1 +1 @@
1
- {"version":3,"file":"redis-idempotency-store.adapter.d.ts","sourceRoot":"","sources":["../../../../src/idempotency/infrastructure/adapters/redis-idempotency-store.adapter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAsB,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAClE,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAGnD,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,aAAa,EAAE,MAAM,gDAAgD,CAAC;AAGvH;;GAEG;AACH,qBACa,4BAA6B,YAAW,iBAAiB,EAAE,YAAY;IAGpC,OAAO,CAAC,QAAQ,CAAC,MAAM;IAFrE,OAAO,CAAC,eAAe,CAAuB;gBAEiB,MAAM,EAAE,YAAY;IAEnF;;OAEG;IACG,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC;IAI7B,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,CAAC;IAqCnG,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAW7E,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAQ/C,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC;IAmBpD,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;CAI5C"}
1
+ {"version":3,"file":"redis-idempotency-store.adapter.d.ts","sourceRoot":"","sources":["../../../../src/idempotency/infrastructure/adapters/redis-idempotency-store.adapter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAsB,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAClE,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAGnD,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,aAAa,EAAE,MAAM,gDAAgD,CAAC;AAGvH;;GAEG;AACH,qBACa,4BAA6B,YAAW,iBAAiB,EAAE,YAAY;IAGpC,OAAO,CAAC,QAAQ,CAAC,MAAM;IAFrE,OAAO,CAAC,eAAe,CAAuB;gBAEiB,MAAM,EAAE,YAAY;IAEnF;;OAEG;IACG,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC;IAI7B,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,CAAC;IAqCnG,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAW7E,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAWnE,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC;IAmBpD,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;CAI5C"}
@@ -1 +1 @@
1
- {"version":3,"file":"idempotency.plugin.d.ts","sourceRoot":"","sources":["../src/idempotency.plugin.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;AAEjF,OAAO,EAAE,aAAa,EAAE,mBAAmB,EAAoE,MAAM,qBAAqB,CAAC;AAO3I,OAAO,EAAE,yBAAyB,EAAE,MAAM,gBAAgB,CAAC;AAa3D;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,qBAAa,iBAAkB,YAAW,aAAa;IAOzC,OAAO,CAAC,QAAQ,CAAC,OAAO;IANpC,QAAQ,CAAC,IAAI,iBAAiB;IAC9B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAW;IACnC,QAAQ,CAAC,WAAW,0EAA0E;IAE9F,OAAO,CAAC,YAAY,CAAC,CAAiD;gBAEzC,OAAO,GAAE,yBAA8B;IAEpE,MAAM,CAAC,aAAa,CAAC,YAAY,EAAE,mBAAmB,CAAC,yBAAyB,CAAC,GAAG,iBAAiB;IAMrG,OAAO,CAAC,MAAM,CAAC,aAAa;IAe5B,UAAU,IAAI,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,aAAa,GAAG,gBAAgB,CAAC;IAIrE,YAAY,IAAI,QAAQ,EAAE;IAsC1B,UAAU,IAAI,KAAK,CAAC,MAAM,GAAG,MAAM,GAAG,QAAQ,CAAC;CAGhD"}
1
+ {"version":3,"file":"idempotency.plugin.d.ts","sourceRoot":"","sources":["../src/idempotency.plugin.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;AAEjF,OAAO,EAAE,aAAa,EAAE,mBAAmB,EAAoE,MAAM,qBAAqB,CAAC;AAQ3I,OAAO,EAAE,yBAAyB,EAAE,MAAM,gBAAgB,CAAC;AAa3D;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,qBAAa,iBAAkB,YAAW,aAAa;IAOzC,OAAO,CAAC,QAAQ,CAAC,OAAO;IANpC,QAAQ,CAAC,IAAI,iBAAiB;IAC9B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAW;IACnC,QAAQ,CAAC,WAAW,0EAA0E;IAE9F,OAAO,CAAC,YAAY,CAAC,CAAiD;gBAEzC,OAAO,GAAE,yBAA8B;IAEpE,MAAM,CAAC,aAAa,CAAC,YAAY,EAAE,mBAAmB,CAAC,yBAAyB,CAAC,GAAG,iBAAiB;IAMrG,OAAO,CAAC,MAAM,CAAC,aAAa;IAe5B,UAAU,IAAI,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,aAAa,GAAG,gBAAgB,CAAC;IAIrE,YAAY,IAAI,QAAQ,EAAE;IAyC1B,UAAU,IAAI,KAAK,CAAC,MAAM,GAAG,MAAM,GAAG,QAAQ,CAAC;CAGhD"}
package/dist/index.js CHANGED
@@ -2,8 +2,8 @@
2
2
 
3
3
  var core = require('@nestjs/core');
4
4
  var core$1 = require('@nestjs-redisx/core');
5
- var crypto = require('crypto');
6
5
  var common = require('@nestjs/common');
6
+ var crypto = require('crypto');
7
7
  var rxjs = require('rxjs');
8
8
  var operators = require('rxjs/operators');
9
9
 
@@ -18,13 +18,7 @@ var __decorateClass = (decorators, target, key, kind) => {
18
18
  var __decorateParam = (index, decorator) => (target, key) => decorator(target, key, index);
19
19
 
20
20
  // package.json
21
- var version = "1.3.1";
22
-
23
- // src/shared/constants/index.ts
24
- var IDEMPOTENCY_PLUGIN_OPTIONS = /* @__PURE__ */ Symbol.for("IDEMPOTENCY_PLUGIN_OPTIONS");
25
- var IDEMPOTENCY_SERVICE = /* @__PURE__ */ Symbol.for("IDEMPOTENCY_SERVICE");
26
- var IDEMPOTENCY_STORE = /* @__PURE__ */ Symbol.for("IDEMPOTENCY_STORE");
27
- var IDEMPOTENCY_REDIS_DRIVER = /* @__PURE__ */ Symbol.for("IDEMPOTENCY_REDIS_DRIVER");
21
+ var version = "1.5.0";
28
22
  var IdempotencyError = class extends core$1.RedisXError {
29
23
  constructor(message, code, idempotencyKey, cause) {
30
24
  super(message, code, cause, { idempotencyKey });
@@ -56,6 +50,74 @@ var IdempotencyRecordNotFoundError = class extends IdempotencyError {
56
50
  super(`Idempotency record not found for key "${key}"`, core$1.ErrorCode.OP_KEY_NOT_FOUND, key);
57
51
  }
58
52
  };
53
+
54
+ // src/idempotency/api/filters/idempotency-exception.filter.ts
55
+ var IdempotencyExceptionFilter = class {
56
+ constructor(adapterHost) {
57
+ this.adapterHost = adapterHost;
58
+ }
59
+ catch(exception, host) {
60
+ const httpAdapter = this.adapterHost.httpAdapter;
61
+ if (!httpAdapter) {
62
+ throw new Error("IdempotencyExceptionFilter: HttpAdapterHost is not initialized. Ensure the NestJS application has bootstrapped with an HTTP adapter before handling requests.");
63
+ }
64
+ const response = host.switchToHttp().getResponse();
65
+ const status = this.resolveStatus(exception);
66
+ const body = {
67
+ statusCode: status,
68
+ message: exception.message,
69
+ error: this.reasonPhrase(status),
70
+ idempotencyKey: exception.idempotencyKey || void 0
71
+ };
72
+ httpAdapter.reply(response, body, status);
73
+ }
74
+ /**
75
+ * Maps an idempotency error to its HTTP status code.
76
+ *
77
+ * - Missing/invalid key -> 400 Bad Request
78
+ * - Reused key with a different request -> 422 Unprocessable Entity
79
+ * - Concurrent request still in progress (timeout) -> 409 Conflict
80
+ * - Previous request with the same key failed -> 409 Conflict
81
+ * - Anything else (unexpected) -> 500 Internal Server Error
82
+ */
83
+ resolveStatus(exception) {
84
+ if (exception instanceof IdempotencyKeyRequiredError) {
85
+ return common.HttpStatus.BAD_REQUEST;
86
+ }
87
+ if (exception instanceof IdempotencyFingerprintMismatchError) {
88
+ return common.HttpStatus.UNPROCESSABLE_ENTITY;
89
+ }
90
+ if (exception instanceof IdempotencyTimeoutError) {
91
+ return common.HttpStatus.CONFLICT;
92
+ }
93
+ if (exception instanceof IdempotencyFailedError) {
94
+ return common.HttpStatus.CONFLICT;
95
+ }
96
+ return common.HttpStatus.INTERNAL_SERVER_ERROR;
97
+ }
98
+ reasonPhrase(status) {
99
+ switch (status) {
100
+ case common.HttpStatus.BAD_REQUEST:
101
+ return "Bad Request";
102
+ case common.HttpStatus.CONFLICT:
103
+ return "Conflict";
104
+ case common.HttpStatus.UNPROCESSABLE_ENTITY:
105
+ return "Unprocessable Entity";
106
+ default:
107
+ return "Internal Server Error";
108
+ }
109
+ }
110
+ };
111
+ IdempotencyExceptionFilter = __decorateClass([
112
+ common.Catch(IdempotencyError),
113
+ __decorateParam(0, common.Inject(core.HttpAdapterHost))
114
+ ], IdempotencyExceptionFilter);
115
+
116
+ // src/shared/constants/index.ts
117
+ var IDEMPOTENCY_PLUGIN_OPTIONS = /* @__PURE__ */ Symbol.for("IDEMPOTENCY_PLUGIN_OPTIONS");
118
+ var IDEMPOTENCY_SERVICE = /* @__PURE__ */ Symbol.for("IDEMPOTENCY_SERVICE");
119
+ var IDEMPOTENCY_STORE = /* @__PURE__ */ Symbol.for("IDEMPOTENCY_STORE");
120
+ var IDEMPOTENCY_REDIS_DRIVER = /* @__PURE__ */ Symbol.for("IDEMPOTENCY_REDIS_DRIVER");
59
121
  var IDEMPOTENT_OPTIONS = /* @__PURE__ */ Symbol.for("IDEMPOTENT_OPTIONS");
60
122
  function Idempotent(options = {}) {
61
123
  return common.applyDecorators(common.SetMetadata(IDEMPOTENT_OPTIONS, options), common.UseInterceptors(exports.IdempotencyInterceptor));
@@ -69,6 +131,7 @@ exports.IdempotencyInterceptor = class IdempotencyInterceptor {
69
131
  this.reflector = reflector;
70
132
  this.adapterHost = adapterHost;
71
133
  }
134
+ logger = new common.Logger(exports.IdempotencyInterceptor.name);
72
135
  async intercept(context, next) {
73
136
  const options = this.getOptions(context);
74
137
  const response = context.switchToHttp().getResponse();
@@ -80,9 +143,18 @@ exports.IdempotencyInterceptor = class IdempotencyInterceptor {
80
143
  return next.handle();
81
144
  }
82
145
  const fingerprint = await this.generateFingerprint(context, options);
83
- const checkResult = await this.idempotencyService.checkAndLock(key, fingerprint, {
84
- ttl: options.ttl
85
- });
146
+ let checkResult;
147
+ try {
148
+ checkResult = await this.idempotencyService.checkAndLock(key, fingerprint, {
149
+ ttl: options.ttl
150
+ });
151
+ } catch (error) {
152
+ if ((this.config.errorPolicy ?? "fail-closed") === "fail-open") {
153
+ this.logger.warn(`Idempotency store unavailable for key "${key}"; proceeding without idempotency (fail-open): ${error.message}`);
154
+ return next.handle();
155
+ }
156
+ throw error;
157
+ }
86
158
  if (!checkResult.isNew) {
87
159
  if (checkResult.fingerprintMismatch) {
88
160
  throw new IdempotencyFingerprintMismatchError(key);
@@ -230,7 +302,8 @@ exports.IdempotencyService = class IdempotencyService {
230
302
  }
231
303
  async fail(key, error) {
232
304
  const fullKey = this.buildKey(key);
233
- await this.store.fail(fullKey, error);
305
+ const ttlSeconds = Math.ceil((this.config.lockTimeout ?? 3e4) / 1e3);
306
+ await this.store.fail(fullKey, error, ttlSeconds);
234
307
  }
235
308
  async get(key) {
236
309
  const fullKey = this.buildKey(key);
@@ -380,12 +453,13 @@ var RedisIdempotencyStoreAdapter = class {
380
453
  });
381
454
  await this.driver.expire(key, ttlSeconds);
382
455
  }
383
- async fail(key, error) {
456
+ async fail(key, error, ttlSeconds) {
384
457
  await this.driver.hmset(key, {
385
458
  status: "failed",
386
459
  error,
387
460
  completedAt: String(Date.now())
388
461
  });
462
+ await this.driver.expire(key, ttlSeconds);
389
463
  }
390
464
  async get(key) {
391
465
  const data = await this.driver.hgetall(key);
@@ -476,7 +550,7 @@ var IdempotencyPlugin = class _IdempotencyPlugin {
476
550
  const clientName = options.client ?? "default";
477
551
  try {
478
552
  return await manager.getClient(clientName);
479
- } catch (error) {
553
+ } catch {
480
554
  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.`);
481
555
  }
482
556
  },
@@ -486,7 +560,10 @@ var IdempotencyPlugin = class _IdempotencyPlugin {
486
560
  { provide: IDEMPOTENCY_SERVICE, useClass: exports.IdempotencyService },
487
561
  // Reflector is needed for @Idempotent decorator metadata
488
562
  core.Reflector,
489
- exports.IdempotencyInterceptor
563
+ exports.IdempotencyInterceptor,
564
+ // Global exception filter to map idempotency errors to proper HTTP codes
565
+ // (otherwise they surface as 500 since they are not HttpExceptions)
566
+ { provide: core.APP_FILTER, useClass: IdempotencyExceptionFilter }
490
567
  ];
491
568
  }
492
569
  getExports() {
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../package.json","../src/shared/constants/index.ts","../src/shared/errors/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","applyDecorators","SetMetadata","UseInterceptors","IdempotencyInterceptor","tap","createHash","of","Injectable","Reflector","HttpAdapterHost","IdempotencyService","Inject","Optional","CLIENT_MANAGER","REDIS_CLIENTS_INITIALIZATION"],"mappings":"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","file":"index.js","sourcesContent":["{\n \"name\": \"@nestjs-redisx/idempotency\",\n \"version\": \"1.3.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/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\": \"^10.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","/**\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 { 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 { 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 } 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 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 const checkResult = await this.idempotencyService.checkAndLock(key, fingerprint, {\n ttl: options.ttl,\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 await this.store.fail(fullKey, error);\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): Promise<void> {\n await this.driver.hmset(key, {\n status: 'failed',\n error,\n completedAt: String(Date.now()),\n });\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 { Reflector } from '@nestjs/core';\nimport { IRedisXPlugin, IPluginAsyncOptions, CLIENT_MANAGER, REDIS_CLIENTS_INITIALIZATION, RedisClientManager } from '@nestjs-redisx/core';\n\nimport { version } from '../package.json';\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 (error) {\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 ];\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.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"]}
package/dist/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
- import { Reflector, HttpAdapterHost } from '@nestjs/core';
1
+ import { HttpAdapterHost, Reflector, APP_FILTER } from '@nestjs/core';
2
2
  import { RedisXError, ErrorCode, CLIENT_MANAGER, REDIS_CLIENTS_INITIALIZATION } from '@nestjs-redisx/core';
3
+ import { Catch, Inject, Injectable, Optional, applyDecorators, SetMetadata, UseInterceptors, Logger, HttpStatus } from '@nestjs/common';
3
4
  import { createHash } from 'crypto';
4
- import { Injectable, Inject, Optional, applyDecorators, SetMetadata, UseInterceptors } from '@nestjs/common';
5
5
  import { of } from 'rxjs';
6
6
  import { tap } from 'rxjs/operators';
7
7
 
@@ -16,13 +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.3.1";
20
-
21
- // src/shared/constants/index.ts
22
- var IDEMPOTENCY_PLUGIN_OPTIONS = /* @__PURE__ */ Symbol.for("IDEMPOTENCY_PLUGIN_OPTIONS");
23
- var IDEMPOTENCY_SERVICE = /* @__PURE__ */ Symbol.for("IDEMPOTENCY_SERVICE");
24
- var IDEMPOTENCY_STORE = /* @__PURE__ */ Symbol.for("IDEMPOTENCY_STORE");
25
- var IDEMPOTENCY_REDIS_DRIVER = /* @__PURE__ */ Symbol.for("IDEMPOTENCY_REDIS_DRIVER");
19
+ var version = "1.5.0";
26
20
  var IdempotencyError = class extends RedisXError {
27
21
  constructor(message, code, idempotencyKey, cause) {
28
22
  super(message, code, cause, { idempotencyKey });
@@ -54,6 +48,74 @@ var IdempotencyRecordNotFoundError = class extends IdempotencyError {
54
48
  super(`Idempotency record not found for key "${key}"`, ErrorCode.OP_KEY_NOT_FOUND, key);
55
49
  }
56
50
  };
51
+
52
+ // src/idempotency/api/filters/idempotency-exception.filter.ts
53
+ var IdempotencyExceptionFilter = class {
54
+ constructor(adapterHost) {
55
+ this.adapterHost = adapterHost;
56
+ }
57
+ catch(exception, host) {
58
+ const httpAdapter = this.adapterHost.httpAdapter;
59
+ if (!httpAdapter) {
60
+ throw new Error("IdempotencyExceptionFilter: HttpAdapterHost is not initialized. Ensure the NestJS application has bootstrapped with an HTTP adapter before handling requests.");
61
+ }
62
+ const response = host.switchToHttp().getResponse();
63
+ const status = this.resolveStatus(exception);
64
+ const body = {
65
+ statusCode: status,
66
+ message: exception.message,
67
+ error: this.reasonPhrase(status),
68
+ idempotencyKey: exception.idempotencyKey || void 0
69
+ };
70
+ httpAdapter.reply(response, body, status);
71
+ }
72
+ /**
73
+ * Maps an idempotency error to its HTTP status code.
74
+ *
75
+ * - Missing/invalid key -> 400 Bad Request
76
+ * - Reused key with a different request -> 422 Unprocessable Entity
77
+ * - Concurrent request still in progress (timeout) -> 409 Conflict
78
+ * - Previous request with the same key failed -> 409 Conflict
79
+ * - Anything else (unexpected) -> 500 Internal Server Error
80
+ */
81
+ resolveStatus(exception) {
82
+ if (exception instanceof IdempotencyKeyRequiredError) {
83
+ return HttpStatus.BAD_REQUEST;
84
+ }
85
+ if (exception instanceof IdempotencyFingerprintMismatchError) {
86
+ return HttpStatus.UNPROCESSABLE_ENTITY;
87
+ }
88
+ if (exception instanceof IdempotencyTimeoutError) {
89
+ return HttpStatus.CONFLICT;
90
+ }
91
+ if (exception instanceof IdempotencyFailedError) {
92
+ return HttpStatus.CONFLICT;
93
+ }
94
+ return HttpStatus.INTERNAL_SERVER_ERROR;
95
+ }
96
+ reasonPhrase(status) {
97
+ switch (status) {
98
+ case HttpStatus.BAD_REQUEST:
99
+ return "Bad Request";
100
+ case HttpStatus.CONFLICT:
101
+ return "Conflict";
102
+ case HttpStatus.UNPROCESSABLE_ENTITY:
103
+ return "Unprocessable Entity";
104
+ default:
105
+ return "Internal Server Error";
106
+ }
107
+ }
108
+ };
109
+ IdempotencyExceptionFilter = __decorateClass([
110
+ Catch(IdempotencyError),
111
+ __decorateParam(0, Inject(HttpAdapterHost))
112
+ ], IdempotencyExceptionFilter);
113
+
114
+ // src/shared/constants/index.ts
115
+ var IDEMPOTENCY_PLUGIN_OPTIONS = /* @__PURE__ */ Symbol.for("IDEMPOTENCY_PLUGIN_OPTIONS");
116
+ var IDEMPOTENCY_SERVICE = /* @__PURE__ */ Symbol.for("IDEMPOTENCY_SERVICE");
117
+ var IDEMPOTENCY_STORE = /* @__PURE__ */ Symbol.for("IDEMPOTENCY_STORE");
118
+ var IDEMPOTENCY_REDIS_DRIVER = /* @__PURE__ */ Symbol.for("IDEMPOTENCY_REDIS_DRIVER");
57
119
  var IDEMPOTENT_OPTIONS = /* @__PURE__ */ Symbol.for("IDEMPOTENT_OPTIONS");
58
120
  function Idempotent(options = {}) {
59
121
  return applyDecorators(SetMetadata(IDEMPOTENT_OPTIONS, options), UseInterceptors(IdempotencyInterceptor));
@@ -67,6 +129,7 @@ var IdempotencyInterceptor = class {
67
129
  this.reflector = reflector;
68
130
  this.adapterHost = adapterHost;
69
131
  }
132
+ logger = new Logger(IdempotencyInterceptor.name);
70
133
  async intercept(context, next) {
71
134
  const options = this.getOptions(context);
72
135
  const response = context.switchToHttp().getResponse();
@@ -78,9 +141,18 @@ var IdempotencyInterceptor = class {
78
141
  return next.handle();
79
142
  }
80
143
  const fingerprint = await this.generateFingerprint(context, options);
81
- const checkResult = await this.idempotencyService.checkAndLock(key, fingerprint, {
82
- ttl: options.ttl
83
- });
144
+ let checkResult;
145
+ try {
146
+ checkResult = await this.idempotencyService.checkAndLock(key, fingerprint, {
147
+ ttl: options.ttl
148
+ });
149
+ } catch (error) {
150
+ if ((this.config.errorPolicy ?? "fail-closed") === "fail-open") {
151
+ this.logger.warn(`Idempotency store unavailable for key "${key}"; proceeding without idempotency (fail-open): ${error.message}`);
152
+ return next.handle();
153
+ }
154
+ throw error;
155
+ }
84
156
  if (!checkResult.isNew) {
85
157
  if (checkResult.fingerprintMismatch) {
86
158
  throw new IdempotencyFingerprintMismatchError(key);
@@ -228,7 +300,8 @@ var IdempotencyService = class {
228
300
  }
229
301
  async fail(key, error) {
230
302
  const fullKey = this.buildKey(key);
231
- await this.store.fail(fullKey, error);
303
+ const ttlSeconds = Math.ceil((this.config.lockTimeout ?? 3e4) / 1e3);
304
+ await this.store.fail(fullKey, error, ttlSeconds);
232
305
  }
233
306
  async get(key) {
234
307
  const fullKey = this.buildKey(key);
@@ -378,12 +451,13 @@ var RedisIdempotencyStoreAdapter = class {
378
451
  });
379
452
  await this.driver.expire(key, ttlSeconds);
380
453
  }
381
- async fail(key, error) {
454
+ async fail(key, error, ttlSeconds) {
382
455
  await this.driver.hmset(key, {
383
456
  status: "failed",
384
457
  error,
385
458
  completedAt: String(Date.now())
386
459
  });
460
+ await this.driver.expire(key, ttlSeconds);
387
461
  }
388
462
  async get(key) {
389
463
  const data = await this.driver.hgetall(key);
@@ -474,7 +548,7 @@ var IdempotencyPlugin = class _IdempotencyPlugin {
474
548
  const clientName = options.client ?? "default";
475
549
  try {
476
550
  return await manager.getClient(clientName);
477
- } catch (error) {
551
+ } catch {
478
552
  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.`);
479
553
  }
480
554
  },
@@ -484,7 +558,10 @@ var IdempotencyPlugin = class _IdempotencyPlugin {
484
558
  { provide: IDEMPOTENCY_SERVICE, useClass: IdempotencyService },
485
559
  // Reflector is needed for @Idempotent decorator metadata
486
560
  Reflector,
487
- IdempotencyInterceptor
561
+ IdempotencyInterceptor,
562
+ // Global exception filter to map idempotency errors to proper HTTP codes
563
+ // (otherwise they surface as 500 since they are not HttpExceptions)
564
+ { provide: APP_FILTER, useClass: IdempotencyExceptionFilter }
488
565
  ];
489
566
  }
490
567
  getExports() {
@@ -1 +1 @@
1
- {"version":3,"sources":["../package.json","../src/shared/constants/index.ts","../src/shared/errors/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":["Injectable","Inject","Reflector"],"mappings":"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","file":"index.mjs","sourcesContent":["{\n \"name\": \"@nestjs-redisx/idempotency\",\n \"version\": \"1.3.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/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\": \"^10.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","/**\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 { 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 { 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 } 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 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 const checkResult = await this.idempotencyService.checkAndLock(key, fingerprint, {\n ttl: options.ttl,\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 await this.store.fail(fullKey, error);\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): Promise<void> {\n await this.driver.hmset(key, {\n status: 'failed',\n error,\n completedAt: String(Date.now()),\n });\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 { Reflector } from '@nestjs/core';\nimport { IRedisXPlugin, IPluginAsyncOptions, CLIENT_MANAGER, REDIS_CLIENTS_INITIALIZATION, RedisClientManager } from '@nestjs-redisx/core';\n\nimport { version } from '../package.json';\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 (error) {\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 ];\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":["Inject","HttpAdapterHost","Injectable","Reflector"],"mappings":"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","file":"index.mjs","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"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nestjs-redisx/idempotency",
3
- "version": "1.3.1",
3
+ "version": "1.5.0",
4
4
  "description": "Idempotency plugin for NestJS RedisX with request deduplication and response replay",
5
5
  "author": "NestJS RedisX Team",
6
6
  "license": "MIT",
@@ -60,7 +60,7 @@
60
60
  "rxjs": "^7.8.0"
61
61
  },
62
62
  "devDependencies": {
63
- "@nestjs/testing": "^10.0.0",
63
+ "@nestjs/testing": "^9.0.0 || ^10.0.0 || ^11.0.0",
64
64
  "@types/node": "^20.0.0",
65
65
  "@typescript-eslint/eslint-plugin": "^6.0.0",
66
66
  "@typescript-eslint/parser": "^6.0.0",