@nestjs-redisx/idempotency 1.3.0 → 1.4.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.
- package/dist/idempotency/api/filters/idempotency-exception.filter.d.ts +30 -0
- package/dist/idempotency/api/filters/idempotency-exception.filter.d.ts.map +1 -0
- package/dist/idempotency/api/interceptors/idempotency.interceptor.d.ts +1 -0
- package/dist/idempotency/api/interceptors/idempotency.interceptor.d.ts.map +1 -1
- package/dist/idempotency/application/ports/idempotency-store.port.d.ts +3 -1
- package/dist/idempotency/application/ports/idempotency-store.port.d.ts.map +1 -1
- package/dist/idempotency/application/services/idempotency.service.d.ts.map +1 -1
- package/dist/idempotency/infrastructure/adapters/redis-idempotency-store.adapter.d.ts +1 -1
- package/dist/idempotency/infrastructure/adapters/redis-idempotency-store.adapter.d.ts.map +1 -1
- package/dist/idempotency.plugin.d.ts.map +1 -1
- package/dist/index.js +91 -14
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +92 -15
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
|
@@ -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,
|
|
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
|
|
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;
|
|
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;
|
|
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;
|
|
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.
|
|
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.4.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
|
-
|
|
84
|
-
|
|
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
|
-
|
|
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);
|
|
@@ -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":";;;;;;;;;;;;;;;;;;;;AAEE,IAAA,OAAA,GAAW,OAAA;;;ACCN,IAAM,0BAAA,mBAA6B,MAAA,CAAO,GAAA,CAAI,4BAA4B;AAC1E,IAAM,mBAAA,mBAAsB,MAAA,CAAO,GAAA,CAAI,qBAAqB;AAC5D,IAAM,iBAAA,mBAAoB,MAAA,CAAO,GAAA,CAAI,mBAAmB;AAMxD,IAAM,wBAAA,mBAA2B,MAAA,CAAO,GAAA,CAAI,0BAA0B;ACNtE,IAAM,gBAAA,GAAN,cAA+BA,kBAAA,CAAY;AAAA,EAChD,WAAA,CACE,OAAA,EACA,IAAA,EACgB,cAAA,EAChB,KAAA,EACA;AACA,IAAA,KAAA,CAAM,OAAA,EAAS,IAAA,EAAM,KAAA,EAAO,EAAE,gBAAgB,CAAA;AAH9B,IAAA,IAAA,CAAA,cAAA,GAAA,cAAA;AAAA,EAIlB;AACF;AAKO,IAAM,2BAAA,GAAN,cAA0C,gBAAA,CAAiB;AAAA,EAChE,WAAA,GAAc;AACZ,IAAA,KAAA,CAAM,oCAAA,EAAsCC,gBAAA,CAAU,uBAAA,EAAyB,EAAE,CAAA;AAAA,EACnF;AACF;AAKO,IAAM,mCAAA,GAAN,cAAkD,gBAAA,CAAiB;AAAA,EACxE,YAAY,GAAA,EAAa;AACvB,IAAA,KAAA,CAAM,CAAA,mEAAA,EAAsE,GAAG,CAAA,CAAA,CAAA,EAAKA,gBAAA,CAAU,yBAAyB,GAAG,CAAA;AAAA,EAC5H;AACF;AAKO,IAAM,uBAAA,GAAN,cAAsC,gBAAA,CAAiB;AAAA,EAC5D,YAAY,GAAA,EAAa;AACvB,IAAA,KAAA,CAAM,CAAA,6DAAA,EAAgE,GAAG,CAAA,CAAA,CAAA,EAAKA,gBAAA,CAAU,YAAY,GAAG,CAAA;AAAA,EACzG;AACF;AAKO,IAAM,sBAAA,GAAN,cAAqC,gBAAA,CAAiB;AAAA,EAC3D,WAAA,CAAY,KAAa,KAAA,EAAgB;AACvC,IAAA,KAAA,CAAM,CAAA,uCAAA,EAA0C,GAAG,CAAA,QAAA,EAAW,KAAA,GAAQ,CAAA,EAAA,EAAK,KAAK,CAAA,CAAA,GAAK,EAAE,CAAA,CAAA,EAAIA,gBAAA,CAAU,2BAAA,EAA6B,GAAG,CAAA;AAAA,EACvI;AACF;AAKO,IAAM,8BAAA,GAAN,cAA6C,gBAAA,CAAiB;AAAA,EACnE,YAAY,GAAA,EAAa;AACvB,IAAA,KAAA,CAAM,CAAA,sCAAA,EAAyC,GAAG,CAAA,CAAA,CAAA,EAAKA,gBAAA,CAAU,kBAAkB,GAAG,CAAA;AAAA,EACxF;AACF;ACvDO,IAAM,kBAAA,mBAAqB,MAAA,CAAO,GAAA,CAAI,oBAAoB;AAW1D,SAAS,UAAA,CAAW,OAAA,GAA8B,EAAC,EAAoB;AAC5E,EAAA,OAAOC,uBAAgBC,kBAAA,CAAY,kBAAA,EAAoB,OAAO,CAAA,EAAGC,sBAAA,CAAgBC,8BAAsB,CAAC,CAAA;AAC1G;;;ACHaA,iCAAN,4BAAA,CAAwD;AAAA,EAC7D,WAAA,CACgD,kBAAA,EACO,MAAA,EACjB,SAAA,EACM,WAAA,EAC1C;AAJ8C,IAAA,IAAA,CAAA,kBAAA,GAAA,kBAAA;AACO,IAAA,IAAA,CAAA,MAAA,GAAA,MAAA;AACjB,IAAA,IAAA,CAAA,SAAA,GAAA,SAAA;AACM,IAAA,IAAA,CAAA,WAAA,GAAA,WAAA;AAAA,EACzC;AAAA,EAEH,MAAM,SAAA,CAAU,OAAA,EAA2B,IAAA,EAAiD;AAC1F,IAAA,MAAM,OAAA,GAAU,IAAA,CAAK,UAAA,CAAW,OAAO,CAAA;AACvC,IAAA,MAAM,QAAA,GAAW,OAAA,CAAQ,YAAA,EAAa,CAAE,WAAA,EAAY;AAEpD,IAAA,MAAM,GAAA,GAAM,MAAM,IAAA,CAAK,UAAA,CAAW,SAAS,OAAO,CAAA;AAElD,IAAA,IAAI,CAAC,GAAA,EAAK;AACR,MAAA,OAAO,KAAK,MAAA,EAAO;AAAA,IACrB;AAEA,IAAA,IAAI,QAAQ,IAAA,IAAS,MAAM,OAAA,CAAQ,IAAA,CAAK,OAAO,CAAA,EAAI;AACjD,MAAA,OAAO,KAAK,MAAA,EAAO;AAAA,IACrB;AAEA,IAAA,MAAM,WAAA,GAAc,MAAM,IAAA,CAAK,mBAAA,CAAoB,SAAS,OAAO,CAAA;AAEnE,IAAA,MAAM,cAAc,MAAM,IAAA,CAAK,kBAAA,CAAmB,YAAA,CAAa,KAAK,WAAA,EAAa;AAAA,MAC/E,KAAK,OAAA,CAAQ;AAAA,KACd,CAAA;AAED,IAAA,IAAI,CAAC,YAAY,KAAA,EAAO;AACtB,MAAA,IAAI,YAAY,mBAAA,EAAqB;AACnC,QAAA,MAAM,IAAI,oCAAoC,GAAG,CAAA;AAAA,MACnD;AAEA,MAAA,MAAM,SAAS,WAAA,CAAY,MAAA;AAE3B,MAAA,IAAI,MAAA,CAAO,WAAW,QAAA,EAAU;AAC9B,QAAA,MAAM,IAAI,sBAAA,CAAuB,GAAA,EAAK,MAAA,CAAO,KAAK,CAAA;AAAA,MACpD;AAEA,MAAA,OAAO,IAAA,CAAK,cAAA,CAAe,QAAA,EAAU,MAAM,CAAA;AAAA,IAC7C;AAEA,IAAA,OAAO,IAAA,CAAK,QAAO,CAAE,IAAA;AAAA,MACnBC,aAAA,CAAI;AAAA,QACF,IAAA,EAAM,CAAC,IAAA,KAAS;AACd,UAAA,KAAK,KAAK,kBAAA,CAAmB,QAAA;AAAA,YAC3B,GAAA;AAAA,YACA;AAAA,cACE,YAAY,QAAA,CAAS,UAAA;AAAA,cACrB,IAAA,EAAM,IAAA;AAAA,cACN,OAAA,EAAS,IAAA,CAAK,cAAA,CAAe,QAAA,EAAU,OAAO;AAAA,aAChD;AAAA,YACA,EAAE,GAAA,EAAK,OAAA,CAAQ,GAAA;AAAI,WACrB;AAAA,QACF,CAAA;AAAA,QACA,KAAA,EAAO,CAAC,KAAA,KAAU;AAChB,UAAA,KAAK,IAAA,CAAK,kBAAA,CAAmB,IAAA,CAAK,GAAA,EAAK,MAAM,OAAO,CAAA;AAAA,QACtD;AAAA,OACD;AAAA,KACH;AAAA,EACF;AAAA,EAEQ,WAAW,OAAA,EAA+C;AAChE,IAAA,OAAO,IAAA,CAAK,UAAU,GAAA,CAAwB,kBAAA,EAAoB,QAAQ,UAAA,EAAY,KAAK,EAAC;AAAA,EAC9F;AAAA,EAEA,MAAc,UAAA,CAAW,OAAA,EAA2B,OAAA,EAAqD;AACvG,IAAA,IAAI,QAAQ,YAAA,EAAc;AACxB,MAAA,OAAO,OAAA,CAAQ,aAAa,OAAO,CAAA;AAAA,IACrC;AAEA,IAAA,MAAM,OAAA,GAAU,OAAA,CAAQ,YAAA,EAAa,CAAE,UAAA,EAAW;AAClD,IAAA,MAAM,UAAA,GAAa,IAAA,CAAK,MAAA,CAAO,UAAA,IAAc,iBAAA;AAE7C,IAAA,OAAO,OAAA,CAAQ,OAAA,CAAQ,UAAA,CAAW,WAAA,EAAa,CAAA,IAAK,IAAA;AAAA,EACtD;AAAA,EAEA,MAAc,mBAAA,CAAoB,OAAA,EAA2B,OAAA,EAA8C;AACzG,IAAA,IAAI,IAAA,CAAK,OAAO,oBAAA,EAAsB;AACpC,MAAA,OAAO,IAAA,CAAK,MAAA,CAAO,oBAAA,CAAqB,OAAO,CAAA;AAAA,IACjD;AAEA,IAAA,MAAM,OAAA,GAAU,OAAA,CAAQ,YAAA,EAAa,CAAE,UAAA,EAAW;AAClD,IAAA,MAAM,MAAA,GAAS,QAAQ,iBAAA,IAAqB,IAAA,CAAK,OAAO,iBAAA,IAAqB,CAAC,QAAA,EAAU,MAAA,EAAQ,MAAM,CAAA;AAEtG,IAAA,MAAM,QAAkB,EAAC;AAEzB,IAAA,IAAI,OAAO,QAAA,CAAS,QAAQ,GAAG,KAAA,CAAM,IAAA,CAAK,QAAQ,MAAM,CAAA;AACxD,IAAA,IAAI,MAAA,CAAO,SAAS,MAAM,CAAA,QAAS,IAAA,CAAK,IAAA,CAAK,cAAA,CAAe,OAAO,CAAC,CAAA;AACpE,IAAA,IAAI,MAAA,CAAO,QAAA,CAAS,MAAM,CAAA,EAAG,KAAA,CAAM,IAAA,CAAK,IAAA,CAAK,SAAA,CAAU,OAAA,CAAQ,IAAA,IAAQ,EAAE,CAAC,CAAA;AAC1E,IAAA,IAAI,MAAA,CAAO,QAAA,CAAS,OAAO,CAAA,EAAG,KAAA,CAAM,IAAA,CAAK,IAAA,CAAK,SAAA,CAAU,OAAA,CAAQ,KAAA,IAAS,EAAE,CAAC,CAAA;AAE5E,IAAA,MAAM,IAAA,GAAO,KAAA,CAAM,IAAA,CAAK,GAAG,CAAA;AAC3B,IAAA,OAAO,IAAA,CAAK,KAAK,IAAI,CAAA;AAAA,EACvB;AAAA,EAEQ,eAAe,OAAA,EAA0B;AAC/C,IAAA,MAAM,WAAA,GAAc,KAAK,WAAA,CAAY,WAAA;AACrC,IAAA,MAAM,GAAA,GAAM,OAAA;AACZ,IAAA,OAAO,aAAa,aAAA,GAAgB,OAAO,KAAK,GAAA,CAAI,GAAA,IAAO,IAAI,IAAA,IAAQ,EAAA;AAAA,EACzE;AAAA,EAEQ,KAAK,IAAA,EAAsB;AACjC,IAAA,OAAOC,kBAAW,QAAQ,CAAA,CAAE,OAAO,IAAI,CAAA,CAAE,OAAO,KAAK,CAAA;AAAA,EACvD;AAAA,EAEQ,cAAA,CAAe,UAAmB,MAAA,EAAiD;AACzF,IAAA,MAAM,WAAA,GAAc,KAAK,WAAA,CAAY,WAAA;AACrC,IAAA,IAAI,CAAC,WAAA,EAAa;AAChB,MAAA,MAAM,IAAI,MAAM,2JAA2J,CAAA;AAAA,IAC7K;AAEA,IAAA,WAAA,CAAY,MAAA,CAAO,QAAA,EAAU,MAAA,CAAO,UAAA,IAAc,GAAG,CAAA;AAErD,IAAA,IAAI,OAAO,OAAA,EAAS;AAClB,MAAA,MAAM,OAAA,GAAU,IAAA,CAAK,KAAA,CAAM,MAAA,CAAO,OAAO,CAAA;AACzC,MAAA,KAAA,MAAW,CAAC,GAAA,EAAK,KAAK,KAAK,MAAA,CAAO,OAAA,CAAQ,OAAO,CAAA,EAAG;AAClD,QAAA,WAAA,CAAY,SAAA,CAAU,QAAA,EAAU,GAAA,EAAK,KAAe,CAAA;AAAA,MACtD;AAAA,IACF;AAEA,IAAA,MAAM,OAAO,MAAA,CAAO,QAAA,GAAW,KAAK,KAAA,CAAM,MAAA,CAAO,QAAQ,CAAA,GAAI,IAAA;AAC7D,IAAA,OAAOC,QAAG,IAAI,CAAA;AAAA,EAChB;AAAA,EAEQ,cAAA,CAAe,UAAoF,OAAA,EAAiE;AAC1K,IAAA,IAAI,CAAC,OAAA,CAAQ,YAAA,EAAc,MAAA,EAAQ,OAAO,MAAA;AAE1C,IAAA,MAAM,UAAkC,EAAC;AACzC,IAAA,KAAA,MAAW,IAAA,IAAQ,QAAQ,YAAA,EAAc;AACvC,MAAA,MAAM,KAAA,GAAQ,QAAA,CAAS,SAAA,GAAY,IAAI,CAAA;AACvC,MAAA,IAAI,KAAA,EAAO,OAAA,CAAQ,IAAI,CAAA,GAAI,OAAO,KAAK,CAAA;AAAA,IACzC;AAEA,IAAA,OAAO,OAAO,IAAA,CAAK,OAAO,CAAA,CAAE,MAAA,GAAS,IAAI,OAAA,GAAU,MAAA;AAAA,EACrD;AACF;AAxIaH,8BAAA,GAAN,eAAA,CAAA;AAAA,EADNI,iBAAA,EAAW;AAAA,EAGP,iCAAO,mBAAmB,CAAA,CAAA;AAAA,EAC1B,iCAAO,0BAA0B,CAAA,CAAA;AAAA,EACjC,iCAAOC,cAAS,CAAA,CAAA;AAAA,EAChB,iCAAOC,oBAAe,CAAA;AAAA,CAAA,EALdN,8BAAA,CAAA;ACTb,IAAM,gBAAA,GAAmB,GAAA;AAOzB,IAAM,eAAA,mBAAkB,MAAA,CAAO,GAAA,CAAI,iBAAiB,CAAA;AAWvCO,6BAAN,wBAAA,CAAwD;AAAA,EAC7D,WAAA,CAEmB,MAAA,EAEA,KAAA,EACqC,OAAA,EACtD;AAJiB,IAAA,IAAA,CAAA,MAAA,GAAA,MAAA;AAEA,IAAA,IAAA,CAAA,KAAA,GAAA,KAAA;AACqC,IAAA,IAAA,CAAA,OAAA,GAAA,OAAA;AAAA,EACrD;AAAA,EAEH,MAAM,YAAA,CAAa,GAAA,EAAa,WAAA,EAAqB,OAAA,GAA+B,EAAC,EAAqC;AACxH,IAAA,MAAM,SAAA,GAAY,KAAK,GAAA,EAAI;AAC3B,IAAA,MAAM,OAAA,GAAU,IAAA,CAAK,QAAA,CAAS,GAAG,CAAA;AACjC,IAAA,MAAM,WAAA,GAAc,OAAA,CAAQ,WAAA,IAAe,IAAA,CAAK,OAAO,WAAA,IAAe,GAAA;AAEtE,IAAA,MAAM,SAAS,MAAM,IAAA,CAAK,MAAM,YAAA,CAAa,OAAA,EAAS,aAAa,WAAW,CAAA;AAE9E,IAAA,IAAI,MAAA,CAAO,WAAW,KAAA,EAAO;AAC3B,MAAA,IAAA,CAAK,SAAS,gBAAA,CAAiB,mCAAA,EAAqC,EAAE,MAAA,EAAQ,OAAO,CAAA;AACrF,MAAA,IAAA,CAAK,eAAe,SAAS,CAAA;AAC7B,MAAA,OAAO,EAAE,OAAO,IAAA,EAAK;AAAA,IACvB;AAEA,IAAA,IAAI,MAAA,CAAO,WAAW,sBAAA,EAAwB;AAC5C,MAAA,IAAA,CAAK,SAAS,gBAAA,CAAiB,mCAAA,EAAqC,EAAE,MAAA,EAAQ,YAAY,CAAA;AAC1F,MAAA,IAAA,CAAK,eAAe,SAAS,CAAA;AAC7B,MAAA,OAAO,EAAE,KAAA,EAAO,KAAA,EAAO,mBAAA,EAAqB,IAAA,EAAK;AAAA,IACnD;AAEA,IAAA,IAAI,MAAA,CAAO,WAAW,YAAA,EAAc;AAElC,MAAA,MAAM,MAAA,GAAS,MAAM,IAAA,CAAK,iBAAA,CAAkB,OAAO,CAAA;AACnD,MAAA,IAAA,CAAK,SAAS,gBAAA,CAAiB,mCAAA,EAAqC,EAAE,MAAA,EAAQ,UAAU,CAAA;AACxF,MAAA,IAAA,CAAK,eAAe,SAAS,CAAA;AAC7B,MAAA,OAAO,EAAE,KAAA,EAAO,KAAA,EAAO,MAAA,EAAO;AAAA,IAChC;AAGA,IAAA,IAAA,CAAK,SAAS,gBAAA,CAAiB,mCAAA,EAAqC,EAAE,MAAA,EAAQ,UAAU,CAAA;AACxF,IAAA,IAAA,CAAK,eAAe,SAAS,CAAA;AAC7B,IAAA,OAAO,EAAE,KAAA,EAAO,KAAA,EAAO,MAAA,EAAQ,OAAO,MAAA,EAAO;AAAA,EAC/C;AAAA,EAEQ,eAAe,SAAA,EAAyB;AAC9C,IAAA,MAAM,QAAA,GAAA,CAAY,IAAA,CAAK,GAAA,EAAI,GAAI,SAAA,IAAa,GAAA;AAC5C,IAAA,IAAA,CAAK,OAAA,EAAS,gBAAA,CAAiB,qCAAA,EAAuC,QAAQ,CAAA;AAAA,EAChF;AAAA,EAEA,MAAM,QAAA,CAAS,GAAA,EAAa,QAAA,EAAgC,OAAA,GAA+B,EAAC,EAAkB;AAC5G,IAAA,MAAM,OAAA,GAAU,IAAA,CAAK,QAAA,CAAS,GAAG,CAAA;AACjC,IAAA,MAAM,GAAA,GAAM,OAAA,CAAQ,GAAA,IAAO,IAAA,CAAK,OAAO,UAAA,IAAc,KAAA;AAErD,IAAA,MAAM,KAAK,KAAA,CAAM,QAAA;AAAA,MACf,OAAA;AAAA,MACA;AAAA,QACE,YAAY,QAAA,CAAS,UAAA;AAAA,QACrB,QAAA,EAAU,IAAA,CAAK,SAAA,CAAU,QAAA,CAAS,IAAI,CAAA;AAAA,QACtC,SAAS,QAAA,CAAS,OAAA,GAAU,KAAK,SAAA,CAAU,QAAA,CAAS,OAAO,CAAA,GAAI,MAAA;AAAA,QAC/D,WAAA,EAAa,KAAK,GAAA;AAAI,OACxB;AAAA,MACA;AAAA,KACF;AAAA,EACF;AAAA,EAEA,MAAM,IAAA,CAAK,GAAA,EAAa,KAAA,EAA8B;AACpD,IAAA,MAAM,OAAA,GAAU,IAAA,CAAK,QAAA,CAAS,GAAG,CAAA;AACjC,IAAA,MAAM,IAAA,CAAK,KAAA,CAAM,IAAA,CAAK,OAAA,EAAS,KAAK,CAAA;AAAA,EACtC;AAAA,EAEA,MAAM,IAAI,GAAA,EAAiD;AACzD,IAAA,MAAM,OAAA,GAAU,IAAA,CAAK,QAAA,CAAS,GAAG,CAAA;AACjC,IAAA,OAAO,IAAA,CAAK,KAAA,CAAM,GAAA,CAAI,OAAO,CAAA;AAAA,EAC/B;AAAA,EAEA,MAAM,OAAO,GAAA,EAA+B;AAC1C,IAAA,MAAM,OAAA,GAAU,IAAA,CAAK,QAAA,CAAS,GAAG,CAAA;AACjC,IAAA,OAAO,IAAA,CAAK,KAAA,CAAM,MAAA,CAAO,OAAO,CAAA;AAAA,EAClC;AAAA,EAEA,MAAc,kBAAkB,GAAA,EAA0C;AACxE,IAAA,MAAM,WAAA,GAAc,IAAA,CAAK,MAAA,CAAO,WAAA,IAAe,GAAA;AAC/C,IAAA,MAAM,SAAA,GAAY,KAAK,GAAA,EAAI;AAC3B,IAAA,OAAO,IAAA,CAAK,GAAA,EAAI,GAAI,SAAA,GAAY,WAAA,EAAa;AAC3C,MAAA,MAAM,MAAA,GAAS,MAAM,IAAA,CAAK,KAAA,CAAM,IAAI,GAAG,CAAA;AAEvC,MAAA,IAAI,CAAC,MAAA,EAAQ;AACX,QAAA,MAAM,IAAI,+BAA+B,GAAG,CAAA;AAAA,MAC9C;AAEA,MAAA,IAAI,MAAA,CAAO,MAAA,KAAW,WAAA,IAAe,MAAA,CAAO,WAAW,QAAA,EAAU;AAC/D,QAAA,OAAO,MAAA;AAAA,MACT;AAEA,MAAA,MAAM,IAAA,CAAK,MAAM,gBAAgB,CAAA;AAAA,IACnC;AAEA,IAAA,MAAM,IAAI,wBAAwB,GAAG,CAAA;AAAA,EACvC;AAAA,EAEQ,SAAS,GAAA,EAAqB;AACpC,IAAA,MAAM,MAAA,GAAS,IAAA,CAAK,MAAA,CAAO,SAAA,IAAa,cAAA;AACxC,IAAA,OAAO,CAAA,EAAG,MAAM,CAAA,EAAG,GAAG,CAAA,CAAA;AAAA,EACxB;AAAA,EAEQ,MAAM,EAAA,EAA2B;AACvC,IAAA,OAAO,IAAI,OAAA,CAAQ,CAAC,YAAY,UAAA,CAAW,OAAA,EAAS,EAAE,CAAC,CAAA;AAAA,EACzD;AACF;AA1GaA,0BAAA,GAAN,eAAA,CAAA;AAAA,EADNH,iBAAAA,EAAW;AAAA,EAGP,eAAA,CAAA,CAAA,EAAAI,cAAO,0BAA0B,CAAA,CAAA;AAAA,EAEjC,eAAA,CAAA,CAAA,EAAAA,cAAO,iBAAiB,CAAA,CAAA;AAAA,EAExB,eAAA,CAAA,CAAA,EAAAC,eAAA,EAAS,CAAA;AAAA,EAAG,eAAA,CAAA,CAAA,EAAAD,cAAO,eAAe,CAAA;AAAA,CAAA,EAN1BD,0BAAA,CAAA;;;ACAN,IAAM,qBAAA,GAAwB;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,CAAA,CAuDnC,IAAA,EAAK;;;AClEA,IAAM,+BAAN,MAA8E;AAAA,EAGnF,YAA+D,MAAA,EAAsB;AAAtB,IAAA,IAAA,CAAA,MAAA,GAAA,MAAA;AAAA,EAAuB;AAAA,EAF9E,eAAA,GAAiC,IAAA;AAAA;AAAA;AAAA;AAAA,EAOzC,MAAM,YAAA,GAA8B;AAClC,IAAA,IAAA,CAAK,eAAA,GAAkB,MAAM,IAAA,CAAK,MAAA,CAAO,WAAW,qBAAqB,CAAA;AAAA,EAC3E;AAAA,EAEA,MAAM,YAAA,CAAa,GAAA,EAAa,WAAA,EAAqB,aAAA,EAAqD;AACxG,IAAA,MAAM,GAAA,GAAM,KAAK,GAAA,EAAI;AACrB,IAAA,MAAM,SAAA,GAAY,MAAM,IAAA,CAAK,MAAA,CAAO,QAAQ,IAAA,CAAK,eAAA,EAAkB,CAAC,GAAG,CAAA,EAAG,CAAC,WAAA,EAAa,aAAA,EAAe,GAAG,CAAC,CAAA;AAG3G,IAAA,MAAM,MAAA,GAAU,SAAA,CAAwB,GAAA,CAAI,CAAC,CAAA,KAAO,CAAA,KAAM,IAAA,IAAQ,CAAA,KAAM,MAAA,GAAY,EAAA,GAAK,MAAA,CAAO,CAAC,CAAE,CAAA;AAEnG,IAAA,MAAM,MAAA,GAAS,OAAO,CAAC,CAAA;AAEvB,IAAA,IAAI,WAAW,KAAA,EAAO;AACpB,MAAA,OAAO,EAAE,QAAQ,KAAA,EAAM;AAAA,IACzB;AAEA,IAAA,IAAI,WAAW,sBAAA,EAAwB;AACrC,MAAA,OAAO,EAAE,QAAQ,sBAAA,EAAuB;AAAA,IAC1C;AAEA,IAAA,IAAI,WAAW,YAAA,EAAc;AAC3B,MAAA,OAAO,EAAE,QAAQ,YAAA,EAAa;AAAA,IAChC;AAGA,IAAA,OAAO;AAAA,MACL,MAAA;AAAA,MACA,MAAA,EAAQ;AAAA,QACN,GAAA;AAAA,QACA,WAAA;AAAA,QACA,MAAA;AAAA,QACA,UAAA,EAAY,OAAO,CAAC,CAAA,GAAI,SAAS,MAAA,CAAO,CAAC,CAAA,EAAG,EAAE,CAAA,GAAI,MAAA;AAAA,QAClD,QAAA,EAAU,MAAA,CAAO,CAAC,CAAA,IAAK,MAAA;AAAA,QACvB,OAAA,EAAS,MAAA,CAAO,CAAC,CAAA,IAAK,MAAA;AAAA,QACtB,KAAA,EAAO,MAAA,CAAO,CAAC,CAAA,IAAK,MAAA;AAAA,QACpB,SAAA,EAAW;AAAA;AAAA;AACb,KACF;AAAA,EACF;AAAA,EAEA,MAAM,QAAA,CAAS,GAAA,EAAa,IAAA,EAAqB,UAAA,EAAmC;AAClF,IAAA,MAAM,IAAA,CAAK,MAAA,CAAO,KAAA,CAAM,GAAA,EAAK;AAAA,MAC3B,MAAA,EAAQ,WAAA;AAAA,MACR,UAAA,EAAY,MAAA,CAAO,IAAA,CAAK,UAAU,CAAA;AAAA,MAClC,UAAU,IAAA,CAAK,QAAA;AAAA,MACf,OAAA,EAAS,KAAK,OAAA,IAAW,EAAA;AAAA,MACzB,WAAA,EAAa,MAAA,CAAO,IAAA,CAAK,WAAW;AAAA,KACrC,CAAA;AACD,IAAA,MAAM,IAAA,CAAK,MAAA,CAAO,MAAA,CAAO,GAAA,EAAK,UAAU,CAAA;AAAA,EAC1C;AAAA,EAEA,MAAM,IAAA,CAAK,GAAA,EAAa,KAAA,EAA8B;AACpD,IAAA,MAAM,IAAA,CAAK,MAAA,CAAO,KAAA,CAAM,GAAA,EAAK;AAAA,MAC3B,MAAA,EAAQ,QAAA;AAAA,MACR,KAAA;AAAA,MACA,WAAA,EAAa,MAAA,CAAO,IAAA,CAAK,GAAA,EAAK;AAAA,KAC/B,CAAA;AAAA,EACH;AAAA,EAEA,MAAM,IAAI,GAAA,EAAiD;AACzD,IAAA,MAAM,IAAA,GAAO,MAAM,IAAA,CAAK,MAAA,CAAO,QAAQ,GAAG,CAAA;AAC1C,IAAA,IAAI,CAAC,IAAA,IAAQ,MAAA,CAAO,KAAK,IAAI,CAAA,CAAE,WAAW,CAAA,EAAG;AAC3C,MAAA,OAAO,IAAA;AAAA,IACT;AAEA,IAAA,OAAO;AAAA,MACL,GAAA;AAAA,MACA,aAAa,IAAA,CAAK,WAAA;AAAA,MAClB,QAAQ,IAAA,CAAK,MAAA;AAAA,MACb,YAAY,IAAA,CAAK,UAAA,GAAa,SAAS,IAAA,CAAK,UAAA,EAAY,EAAE,CAAA,GAAI,MAAA;AAAA,MAC9D,QAAA,EAAU,KAAK,QAAA,IAAY,MAAA;AAAA,MAC3B,OAAA,EAAS,KAAK,OAAA,IAAW,MAAA;AAAA,MACzB,SAAA,EAAW,QAAA,CAAS,IAAA,CAAK,SAAA,EAAY,EAAE,CAAA;AAAA,MACvC,aAAa,IAAA,CAAK,WAAA,GAAc,SAAS,IAAA,CAAK,WAAA,EAAa,EAAE,CAAA,GAAI,MAAA;AAAA,MACjE,KAAA,EAAO,KAAK,KAAA,IAAS;AAAA,KACvB;AAAA,EACF;AAAA,EAEA,MAAM,OAAO,GAAA,EAA+B;AAC1C,IAAA,MAAM,MAAA,GAAS,MAAM,IAAA,CAAK,MAAA,CAAO,IAAI,GAAG,CAAA;AACxC,IAAA,OAAO,MAAA,GAAS,CAAA;AAAA,EAClB;AACF,CAAA;AA3Fa,4BAAA,GAAN,eAAA,CAAA;AAAA,EADNH,iBAAAA,EAAW;AAAA,EAIG,eAAA,CAAA,CAAA,EAAAI,cAAO,wBAAwB,CAAA;AAAA,CAAA,EAHjC,4BAAA,CAAA;;;ACIb,IAAM,0BAAA,GAAwH;AAAA,EAC5H,UAAA,EAAY,KAAA;AAAA,EACZ,SAAA,EAAW,cAAA;AAAA,EACX,UAAA,EAAY,iBAAA;AAAA,EACZ,WAAA,EAAa,GAAA;AAAA,EACb,WAAA,EAAa,GAAA;AAAA,EACb,mBAAA,EAAqB,IAAA;AAAA,EACrB,iBAAA,EAAmB,CAAC,QAAA,EAAU,MAAA,EAAQ,MAAM,CAAA;AAAA,EAC5C,WAAA,EAAa;AACf,CAAA;AA8BO,IAAM,iBAAA,GAAN,MAAM,kBAAA,CAA2C;AAAA,EAOtD,WAAA,CAA6B,OAAA,GAAqC,EAAC,EAAG;AAAzC,IAAA,IAAA,CAAA,OAAA,GAAA,OAAA;AAAA,EAA0C;AAAA,EAN9D,IAAA,GAAO,aAAA;AAAA,EACP,OAAA,GAAkB,OAAA;AAAA,EAClB,WAAA,GAAc,sEAAA;AAAA,EAEf,YAAA;AAAA,EAIR,OAAO,cAAc,YAAA,EAAiF;AACpG,IAAA,MAAM,MAAA,GAAS,IAAI,kBAAA,EAAkB;AACrC,IAAA,MAAA,CAAO,YAAA,GAAe,YAAA;AACtB,IAAA,OAAO,MAAA;AAAA,EACT;AAAA,EAEA,OAAe,cAAc,OAAA,EAA+D;AAC1F,IAAA,OAAO;AAAA,MACL,QAAQ,OAAA,CAAQ,MAAA;AAAA,MAChB,UAAA,EAAY,OAAA,CAAQ,UAAA,IAAc,0BAAA,CAA2B,UAAA;AAAA,MAC7D,SAAA,EAAW,OAAA,CAAQ,SAAA,IAAa,0BAAA,CAA2B,SAAA;AAAA,MAC3D,UAAA,EAAY,OAAA,CAAQ,UAAA,IAAc,0BAAA,CAA2B,UAAA;AAAA,MAC7D,WAAA,EAAa,OAAA,CAAQ,WAAA,IAAe,0BAAA,CAA2B,WAAA;AAAA,MAC/D,WAAA,EAAa,OAAA,CAAQ,WAAA,IAAe,0BAAA,CAA2B,WAAA;AAAA,MAC/D,mBAAA,EAAqB,OAAA,CAAQ,mBAAA,IAAuB,0BAAA,CAA2B,mBAAA;AAAA,MAC/E,iBAAA,EAAmB,OAAA,CAAQ,iBAAA,IAAqB,0BAAA,CAA2B,iBAAA;AAAA,MAC3E,WAAA,EAAa,OAAA,CAAQ,WAAA,IAAe,0BAAA,CAA2B,WAAA;AAAA,MAC/D,sBAAsB,OAAA,CAAQ;AAAA,KAChC;AAAA,EACF;AAAA,EAEA,UAAA,GAAsE;AACpE,IAAA,OAAO,IAAA,CAAK,YAAA,EAAc,OAAA,IAAW,EAAC;AAAA,EACxC;AAAA,EAEA,YAAA,GAA2B;AACzB,IAAA,MAAM,eAAA,GAA4B,KAAK,YAAA,GACnC;AAAA,MACE,OAAA,EAAS,0BAAA;AAAA,MACT,UAAA,EAAY,UAAU,IAAA,KAAoB;AACxC,QAAA,MAAM,cAAc,MAAM,IAAA,CAAK,YAAA,CAAc,UAAA,CAAW,GAAG,IAAI,CAAA;AAC/D,QAAA,OAAO,kBAAA,CAAkB,cAAc,WAAW,CAAA;AAAA,MACpD,CAAA;AAAA,MACA,MAAA,EAAQ,IAAA,CAAK,YAAA,CAAa,MAAA,IAAU;AAAC,KACvC,GACA;AAAA,MACE,OAAA,EAAS,0BAAA;AAAA,MACT,QAAA,EAAU,kBAAA,CAAkB,aAAA,CAAc,IAAA,CAAK,OAAO;AAAA,KACxD;AAEJ,IAAA,OAAO;AAAA,MACL,eAAA;AAAA;AAAA,MAEA;AAAA,QACE,OAAA,EAAS,wBAAA;AAAA,QACT,UAAA,EAAY,OAAO,OAAA,EAA6B,KAAA,EAAa,OAAA,KAAuC;AAClG,UAAA,MAAM,UAAA,GAAa,QAAQ,MAAA,IAAU,SAAA;AACrC,UAAA,IAAI;AACF,YAAA,OAAO,MAAM,OAAA,CAAQ,SAAA,CAAU,UAAU,CAAA;AAAA,UAC3C,SAAS,KAAA,EAAO;AACd,YAAA,MAAM,IAAI,KAAA,CAAM,CAAA,iCAAA,EAAoC,UAAU,CAAA,0GAAA,EAAuH,UAAU,CAAA,qEAAA,CAAuE,CAAA;AAAA,UACxQ;AAAA,QACF,CAAA;AAAA,QACA,MAAA,EAAQ,CAACE,qBAAA,EAAgBC,mCAAA,EAA8B,0BAA0B;AAAA,OACnF;AAAA,MACA,EAAE,OAAA,EAAS,iBAAA,EAAmB,QAAA,EAAU,4BAAA,EAA6B;AAAA,MACrE,EAAE,OAAA,EAAS,mBAAA,EAAqB,QAAA,EAAUJ,0BAAA,EAAmB;AAAA;AAAA,MAE7DF,cAAAA;AAAA,MACAL;AAAA,KACF;AAAA,EACF;AAAA,EAEA,UAAA,GAAgD;AAC9C,IAAA,OAAO,CAAC,0BAAA,EAA4B,mBAAA,EAAqBA,8BAAsB,CAAA;AAAA,EACjF;AACF","file":"index.js","sourcesContent":["{\n \"name\": \"@nestjs-redisx/idempotency\",\n \"version\": \"1.3.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\": \"^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.4.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\": \"^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 (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 // 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,
|
|
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.
|
|
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.4.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
|
-
|
|
82
|
-
|
|
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
|
-
|
|
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);
|
|
@@ -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() {
|
package/dist/index.mjs.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":["Injectable","Inject","Reflector"],"mappings":"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","file":"index.mjs","sourcesContent":["{\n \"name\": \"@nestjs-redisx/idempotency\",\n \"version\": \"1.3.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\": \"^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.4.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\": \"^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 (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 // 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
|
+
"version": "1.4.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": "^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",
|