@nestjs-redisx/idempotency 1.5.1 → 1.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/idempotency/api/filters/idempotency-exception.filter.d.ts +1 -0
- package/dist/idempotency/api/filters/idempotency-exception.filter.d.ts.map +1 -1
- package/dist/idempotency/api/interceptors/idempotency.interceptor.d.ts.map +1 -1
- package/dist/idempotency/application/ports/idempotency-service.port.d.ts +1 -1
- package/dist/idempotency/application/ports/idempotency-service.port.d.ts.map +1 -1
- package/dist/idempotency/application/ports/idempotency-store.port.d.ts +4 -2
- package/dist/idempotency/application/ports/idempotency-store.port.d.ts.map +1 -1
- package/dist/idempotency/application/services/idempotency.service.d.ts +15 -1
- package/dist/idempotency/application/services/idempotency.service.d.ts.map +1 -1
- package/dist/idempotency/infrastructure/adapters/redis-idempotency-store.adapter.d.ts +2 -2
- package/dist/idempotency/infrastructure/adapters/redis-idempotency-store.adapter.d.ts.map +1 -1
- package/dist/idempotency/infrastructure/scripts/lua-scripts.d.ts +1 -0
- package/dist/idempotency/infrastructure/scripts/lua-scripts.d.ts.map +1 -1
- package/dist/idempotency.plugin.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +106 -34
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +106 -35
- package/dist/index.mjs.map +1 -1
- package/dist/shared/errors/index.d.ts +7 -0
- package/dist/shared/errors/index.d.ts.map +1 -1
- package/dist/shared/types/index.d.ts +13 -1
- package/dist/shared/types/index.d.ts.map +1 -1
- package/package.json +7 -4
|
@@ -22,6 +22,7 @@ export declare class IdempotencyExceptionFilter implements ExceptionFilter {
|
|
|
22
22
|
* - Reused key with a different request -> 422 Unprocessable Entity
|
|
23
23
|
* - Concurrent request still in progress (timeout) -> 409 Conflict
|
|
24
24
|
* - Previous request with the same key failed -> 409 Conflict
|
|
25
|
+
* - In-flight record vanished mid-wait -> 409 Conflict (retryable)
|
|
25
26
|
* - Anything else (unexpected) -> 500 Internal Server Error
|
|
26
27
|
*/
|
|
27
28
|
private resolveStatus;
|
|
@@ -1 +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,
|
|
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,EAAqJ,MAAM,wBAAwB,CAAC;AAE7M;;;;;;;;;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;;;;;;;;;OASG;IACH,OAAO,CAAC,aAAa;IAqBrB,OAAO,CAAC,YAAY;CAYrB"}
|
|
@@ -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,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;
|
|
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;AAcvF,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;IAsF3F,OAAO,CAAC,UAAU;YAIJ,UAAU;YAgBV,mBAAmB;IAmBjC,OAAO,CAAC,cAAc;IAMtB,OAAO,CAAC,IAAI;IAIZ,OAAO,CAAC,cAAc;IAmBtB,OAAO,CAAC,cAAc;CAWvB"}
|
|
@@ -26,7 +26,7 @@ export interface IIdempotencyService {
|
|
|
26
26
|
* @param key - Idempotency key
|
|
27
27
|
* @param error - Error message
|
|
28
28
|
*/
|
|
29
|
-
fail(key: string, error: string): Promise<void>;
|
|
29
|
+
fail(key: string, error: string, options?: IIdempotencyOptions): Promise<void>;
|
|
30
30
|
/**
|
|
31
31
|
* Get existing record by key.
|
|
32
32
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"idempotency-service.port.d.ts","sourceRoot":"","sources":["../../../../src/idempotency/application/ports/idempotency-service.port.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,uBAAuB,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAE/H;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC;;;;;;;OAOG;IACH,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,mBAAmB,GAAG,OAAO,CAAC,uBAAuB,CAAC,CAAC;IAEhH;;;;;;OAMG;IACH,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,oBAAoB,EAAE,OAAO,CAAC,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAEpG;;;;;OAKG;IACH,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"idempotency-service.port.d.ts","sourceRoot":"","sources":["../../../../src/idempotency/application/ports/idempotency-service.port.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,uBAAuB,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAE/H;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC;;;;;;;OAOG;IACH,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,mBAAmB,GAAG,OAAO,CAAC,uBAAuB,CAAC,CAAC;IAEhH;;;;;;OAMG;IACH,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,oBAAoB,EAAE,OAAO,CAAC,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAEpG;;;;;OAKG;IACH,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAE/E;;;;;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"}
|
|
@@ -12,6 +12,8 @@ export interface ICheckAndLockResult {
|
|
|
12
12
|
* Data for completing a request
|
|
13
13
|
*/
|
|
14
14
|
export interface ICompleteData {
|
|
15
|
+
/** Request fingerprint (persisted so replays keep matching after rewrites) */
|
|
16
|
+
fingerprint?: string;
|
|
15
17
|
/** HTTP status code */
|
|
16
18
|
statusCode: number;
|
|
17
19
|
/** Response body (JSON string) */
|
|
@@ -33,7 +35,7 @@ export interface IIdempotencyStore {
|
|
|
33
35
|
* @param lockTimeoutMs - Lock timeout in milliseconds
|
|
34
36
|
* @returns Check result
|
|
35
37
|
*/
|
|
36
|
-
checkAndLock(key: string, fingerprint: string, lockTimeoutMs: number): Promise<ICheckAndLockResult>;
|
|
38
|
+
checkAndLock(key: string, fingerprint: string, lockTimeoutMs: number, validateFingerprint?: boolean): Promise<ICheckAndLockResult>;
|
|
37
39
|
/**
|
|
38
40
|
* Mark record as completed with response data.
|
|
39
41
|
*
|
|
@@ -50,7 +52,7 @@ export interface IIdempotencyStore {
|
|
|
50
52
|
* @param ttlSeconds - TTL in seconds after which the failed record expires,
|
|
51
53
|
* allowing a fresh attempt with the same key
|
|
52
54
|
*/
|
|
53
|
-
fail(key: string, error: string, ttlSeconds: number): Promise<void>;
|
|
55
|
+
fail(key: string, error: string, ttlSeconds: number, fingerprint?: string): Promise<void>;
|
|
54
56
|
/**
|
|
55
57
|
* Get record by key.
|
|
56
58
|
*
|
|
@@ -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;
|
|
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,8EAA8E;IAC9E,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB,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,EAAE,mBAAmB,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAAC;IAEnI;;;;;;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,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAE1F;;;;;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"}
|
|
@@ -16,9 +16,23 @@ export declare class IdempotencyService implements IIdempotencyService {
|
|
|
16
16
|
checkAndLock(key: string, fingerprint: string, options?: IIdempotencyOptions): Promise<IIdempotencyCheckResult>;
|
|
17
17
|
private recordDuration;
|
|
18
18
|
complete(key: string, response: IIdempotencyResponse, options?: IIdempotencyOptions): Promise<void>;
|
|
19
|
-
fail(key: string, error: string): Promise<void>;
|
|
19
|
+
fail(key: string, error: string, options?: IIdempotencyOptions): Promise<void>;
|
|
20
20
|
get(key: string): Promise<IIdempotencyRecord | null>;
|
|
21
21
|
delete(key: string): Promise<boolean>;
|
|
22
|
+
/**
|
|
23
|
+
* Polls until the in-flight attempt completes or fails, bounded by
|
|
24
|
+
* waitTimeout.
|
|
25
|
+
*
|
|
26
|
+
* If the processing record VANISHES (the first attempt died between
|
|
27
|
+
* checkAndLock and complete, so its lock TTL expired), the waiter attempts
|
|
28
|
+
* an atomic takeover via checkAndLock: the winner returns 'takeover' (the
|
|
29
|
+
* caller executes the handler itself), losers keep waiting on the new
|
|
30
|
+
* owner's record. This turns a dead first attempt into a self-healing
|
|
31
|
+
* retry instead of surfacing an unexplained error to every waiter.
|
|
32
|
+
*
|
|
33
|
+
* @returns the completed/failed record to replay, or 'takeover' when this
|
|
34
|
+
* caller now owns the key and must execute the request
|
|
35
|
+
*/
|
|
22
36
|
private waitForCompletion;
|
|
23
37
|
private buildKey;
|
|
24
38
|
private sleep;
|
|
@@ -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;
|
|
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;IA2CzH,OAAO,CAAC,cAAc;IAKhB,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,oBAAoB,EAAE,OAAO,GAAE,mBAAwB,GAAG,OAAO,CAAC,IAAI,CAAC;IAiBvG,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,GAAE,mBAAwB,GAAG,OAAO,CAAC,IAAI,CAAC;IAQlF,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC;IAKpD,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAK3C;;;;;;;;;;;;;OAaG;YACW,iBAAiB;IA+B/B,OAAO,CAAC,QAAQ;IAKhB,OAAO,CAAC,KAAK;CAGd"}
|
|
@@ -13,9 +13,9 @@ export declare class RedisIdempotencyStoreAdapter implements IIdempotencyStore,
|
|
|
13
13
|
* Pre-load Lua script on module initialization
|
|
14
14
|
*/
|
|
15
15
|
onModuleInit(): Promise<void>;
|
|
16
|
-
checkAndLock(key: string, fingerprint: string, lockTimeoutMs: number): Promise<ICheckAndLockResult>;
|
|
16
|
+
checkAndLock(key: string, fingerprint: string, lockTimeoutMs: number, validateFingerprint?: boolean): Promise<ICheckAndLockResult>;
|
|
17
17
|
complete(key: string, data: ICompleteData, ttlSeconds: number): Promise<void>;
|
|
18
|
-
fail(key: string, error: string, ttlSeconds: number): Promise<void>;
|
|
18
|
+
fail(key: string, error: string, ttlSeconds: number, fingerprint?: string): 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;
|
|
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,EAAE,mBAAmB,UAAO,GAAG,OAAO,CAAC,mBAAmB,CAAC;IAqC/H,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAkB7E,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAezF,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":"lua-scripts.d.ts","sourceRoot":"","sources":["../../../../src/idempotency/infrastructure/scripts/lua-scripts.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH
|
|
1
|
+
{"version":3,"file":"lua-scripts.d.ts","sourceRoot":"","sources":["../../../../src/idempotency/infrastructure/scripts/lua-scripts.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,qBAAqB,QAwD1B,CAAC"}
|
|
@@ -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;AAS3I,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;IA0B5B,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.d.ts
CHANGED
|
@@ -5,6 +5,6 @@ export type { IIdempotencyStore } from './idempotency/application/ports/idempote
|
|
|
5
5
|
export { Idempotent, type IIdempotentOptions, IDEMPOTENT_OPTIONS } from './idempotency/api/decorators/idempotent.decorator';
|
|
6
6
|
export { IdempotencyInterceptor } from './idempotency/api/interceptors/idempotency.interceptor';
|
|
7
7
|
export type { IIdempotencyPluginOptions, IIdempotencyRecord, IIdempotencyCheckResult, IIdempotencyResponse, IIdempotencyOptions } from './shared/types';
|
|
8
|
-
export { IdempotencyError, IdempotencyKeyRequiredError, IdempotencyFingerprintMismatchError, IdempotencyTimeoutError, IdempotencyFailedError, IdempotencyRecordNotFoundError } from './shared/errors';
|
|
8
|
+
export { IdempotencyError, IdempotencyKeyRequiredError, IdempotencyFingerprintMismatchError, IdempotencyTimeoutError, IdempotencyFailedError, IdempotencyRecordNotFoundError, IdempotencyConfigError } from './shared/errors';
|
|
9
9
|
export { IDEMPOTENCY_PLUGIN_OPTIONS, IDEMPOTENCY_REDIS_DRIVER, IDEMPOTENCY_SERVICE, IDEMPOTENCY_STORE } from './shared/constants';
|
|
10
10
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AAGzD,OAAO,EAAE,kBAAkB,EAAE,MAAM,wDAAwD,CAAC;AAG5F,YAAY,EAAE,mBAAmB,EAAE,MAAM,0DAA0D,CAAC;AACpG,YAAY,EAAE,iBAAiB,EAAE,MAAM,wDAAwD,CAAC;AAGhG,OAAO,EAAE,UAAU,EAAE,KAAK,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,mDAAmD,CAAC;AAG5H,OAAO,EAAE,sBAAsB,EAAE,MAAM,wDAAwD,CAAC;AAGhG,YAAY,EAAE,yBAAyB,EAAE,kBAAkB,EAAE,uBAAuB,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAGxJ,OAAO,EAAE,gBAAgB,EAAE,2BAA2B,EAAE,mCAAmC,EAAE,uBAAuB,EAAE,sBAAsB,EAAE,8BAA8B,EAAE,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AAGzD,OAAO,EAAE,kBAAkB,EAAE,MAAM,wDAAwD,CAAC;AAG5F,YAAY,EAAE,mBAAmB,EAAE,MAAM,0DAA0D,CAAC;AACpG,YAAY,EAAE,iBAAiB,EAAE,MAAM,wDAAwD,CAAC;AAGhG,OAAO,EAAE,UAAU,EAAE,KAAK,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,mDAAmD,CAAC;AAG5H,OAAO,EAAE,sBAAsB,EAAE,MAAM,wDAAwD,CAAC;AAGhG,YAAY,EAAE,yBAAyB,EAAE,kBAAkB,EAAE,uBAAuB,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAGxJ,OAAO,EAAE,gBAAgB,EAAE,2BAA2B,EAAE,mCAAmC,EAAE,uBAAuB,EAAE,sBAAsB,EAAE,8BAA8B,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAC;AAG9N,OAAO,EAAE,0BAA0B,EAAE,wBAAwB,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -18,7 +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.
|
|
21
|
+
var version = "1.6.1";
|
|
22
22
|
var IdempotencyError = class extends core$1.RedisXError {
|
|
23
23
|
constructor(message, code, idempotencyKey, cause) {
|
|
24
24
|
super(message, code, cause, { idempotencyKey });
|
|
@@ -50,6 +50,11 @@ var IdempotencyRecordNotFoundError = class extends IdempotencyError {
|
|
|
50
50
|
super(`Idempotency record not found for key "${key}"`, core$1.ErrorCode.OP_KEY_NOT_FOUND, key);
|
|
51
51
|
}
|
|
52
52
|
};
|
|
53
|
+
var IdempotencyConfigError = class extends IdempotencyError {
|
|
54
|
+
constructor(message) {
|
|
55
|
+
super(message, core$1.ErrorCode.CFG_INVALID, "");
|
|
56
|
+
}
|
|
57
|
+
};
|
|
53
58
|
|
|
54
59
|
// src/idempotency/api/filters/idempotency-exception.filter.ts
|
|
55
60
|
var IdempotencyExceptionFilter = class {
|
|
@@ -78,6 +83,7 @@ var IdempotencyExceptionFilter = class {
|
|
|
78
83
|
* - Reused key with a different request -> 422 Unprocessable Entity
|
|
79
84
|
* - Concurrent request still in progress (timeout) -> 409 Conflict
|
|
80
85
|
* - Previous request with the same key failed -> 409 Conflict
|
|
86
|
+
* - In-flight record vanished mid-wait -> 409 Conflict (retryable)
|
|
81
87
|
* - Anything else (unexpected) -> 500 Internal Server Error
|
|
82
88
|
*/
|
|
83
89
|
resolveStatus(exception) {
|
|
@@ -93,6 +99,9 @@ var IdempotencyExceptionFilter = class {
|
|
|
93
99
|
if (exception instanceof IdempotencyFailedError) {
|
|
94
100
|
return common.HttpStatus.CONFLICT;
|
|
95
101
|
}
|
|
102
|
+
if (exception instanceof IdempotencyRecordNotFoundError) {
|
|
103
|
+
return common.HttpStatus.CONFLICT;
|
|
104
|
+
}
|
|
96
105
|
return common.HttpStatus.INTERNAL_SERVER_ERROR;
|
|
97
106
|
}
|
|
98
107
|
reasonPhrase(status) {
|
|
@@ -124,6 +133,7 @@ function Idempotent(options = {}) {
|
|
|
124
133
|
}
|
|
125
134
|
|
|
126
135
|
// src/idempotency/api/interceptors/idempotency.interceptor.ts
|
|
136
|
+
var IDEMPOTENCY_HANDLED = /* @__PURE__ */ Symbol("redisx.idempotencyHandled");
|
|
127
137
|
exports.IdempotencyInterceptor = class IdempotencyInterceptor {
|
|
128
138
|
constructor(idempotencyService, config, reflector, adapterHost) {
|
|
129
139
|
this.idempotencyService = idempotencyService;
|
|
@@ -133,6 +143,10 @@ exports.IdempotencyInterceptor = class IdempotencyInterceptor {
|
|
|
133
143
|
}
|
|
134
144
|
logger = new common.Logger(exports.IdempotencyInterceptor.name);
|
|
135
145
|
async intercept(context, next) {
|
|
146
|
+
const request = context.switchToHttp().getRequest();
|
|
147
|
+
if (request[IDEMPOTENCY_HANDLED]) {
|
|
148
|
+
return next.handle();
|
|
149
|
+
}
|
|
136
150
|
const options = this.getOptions(context);
|
|
137
151
|
const response = context.switchToHttp().getResponse();
|
|
138
152
|
const key = await this.extractKey(context, options);
|
|
@@ -142,11 +156,13 @@ exports.IdempotencyInterceptor = class IdempotencyInterceptor {
|
|
|
142
156
|
if (options.skip && await options.skip(context)) {
|
|
143
157
|
return next.handle();
|
|
144
158
|
}
|
|
159
|
+
request[IDEMPOTENCY_HANDLED] = true;
|
|
145
160
|
const fingerprint = await this.generateFingerprint(context, options);
|
|
146
161
|
let checkResult;
|
|
147
162
|
try {
|
|
148
163
|
checkResult = await this.idempotencyService.checkAndLock(key, fingerprint, {
|
|
149
|
-
ttl: options.ttl
|
|
164
|
+
ttl: options.ttl,
|
|
165
|
+
validateFingerprint: options.validateFingerprint
|
|
150
166
|
});
|
|
151
167
|
} catch (error) {
|
|
152
168
|
if ((this.config.errorPolicy ?? "fail-closed") === "fail-open") {
|
|
@@ -168,18 +184,24 @@ exports.IdempotencyInterceptor = class IdempotencyInterceptor {
|
|
|
168
184
|
return next.handle().pipe(
|
|
169
185
|
operators.tap({
|
|
170
186
|
next: (data) => {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
187
|
+
Promise.resolve(
|
|
188
|
+
this.idempotencyService.complete(
|
|
189
|
+
key,
|
|
190
|
+
{
|
|
191
|
+
statusCode: response.statusCode,
|
|
192
|
+
body: data,
|
|
193
|
+
headers: this.extractHeaders(response, options)
|
|
194
|
+
},
|
|
195
|
+
{ ttl: options.ttl, fingerprint }
|
|
196
|
+
)
|
|
197
|
+
).catch((err) => {
|
|
198
|
+
this.logger.error(`Failed to persist idempotency completion for key "${key}": ${err.message}`);
|
|
199
|
+
});
|
|
180
200
|
},
|
|
181
201
|
error: (error) => {
|
|
182
|
-
|
|
202
|
+
Promise.resolve(this.idempotencyService.fail(key, error.message, { fingerprint })).catch((err) => {
|
|
203
|
+
this.logger.error(`Failed to persist idempotency failure for key "${key}": ${err.message}`);
|
|
204
|
+
});
|
|
183
205
|
}
|
|
184
206
|
})
|
|
185
207
|
);
|
|
@@ -193,7 +215,11 @@ exports.IdempotencyInterceptor = class IdempotencyInterceptor {
|
|
|
193
215
|
}
|
|
194
216
|
const request = context.switchToHttp().getRequest();
|
|
195
217
|
const headerName = this.config.headerName ?? "Idempotency-Key";
|
|
196
|
-
|
|
218
|
+
const value = request.headers[headerName.toLowerCase()];
|
|
219
|
+
if (Array.isArray(value)) {
|
|
220
|
+
return value[0] ?? null;
|
|
221
|
+
}
|
|
222
|
+
return value ?? null;
|
|
197
223
|
}
|
|
198
224
|
async generateFingerprint(context, options) {
|
|
199
225
|
if (this.config.fingerprintGenerator) {
|
|
@@ -261,7 +287,8 @@ exports.IdempotencyService = class IdempotencyService {
|
|
|
261
287
|
const startTime = Date.now();
|
|
262
288
|
const fullKey = this.buildKey(key);
|
|
263
289
|
const lockTimeout = options.lockTimeout ?? this.config.lockTimeout ?? 3e4;
|
|
264
|
-
const
|
|
290
|
+
const validateFingerprint = options.validateFingerprint ?? this.config.validateFingerprint ?? true;
|
|
291
|
+
const result = await this.store.checkAndLock(fullKey, fingerprint, lockTimeout, validateFingerprint);
|
|
265
292
|
if (result.status === "new") {
|
|
266
293
|
this.metrics?.incrementCounter("redisx_idempotency_requests_total", { status: "new" });
|
|
267
294
|
this.recordDuration(startTime);
|
|
@@ -273,10 +300,15 @@ exports.IdempotencyService = class IdempotencyService {
|
|
|
273
300
|
return { isNew: false, fingerprintMismatch: true };
|
|
274
301
|
}
|
|
275
302
|
if (result.status === "processing") {
|
|
276
|
-
const
|
|
303
|
+
const outcome = await this.waitForCompletion(fullKey, fingerprint, lockTimeout, validateFingerprint);
|
|
304
|
+
if (outcome === "takeover") {
|
|
305
|
+
this.metrics?.incrementCounter("redisx_idempotency_requests_total", { status: "takeover" });
|
|
306
|
+
this.recordDuration(startTime);
|
|
307
|
+
return { isNew: true };
|
|
308
|
+
}
|
|
277
309
|
this.metrics?.incrementCounter("redisx_idempotency_requests_total", { status: "replay" });
|
|
278
310
|
this.recordDuration(startTime);
|
|
279
|
-
return { isNew: false, record };
|
|
311
|
+
return { isNew: false, record: outcome };
|
|
280
312
|
}
|
|
281
313
|
this.metrics?.incrementCounter("redisx_idempotency_requests_total", { status: "replay" });
|
|
282
314
|
this.recordDuration(startTime);
|
|
@@ -292,6 +324,7 @@ exports.IdempotencyService = class IdempotencyService {
|
|
|
292
324
|
await this.store.complete(
|
|
293
325
|
fullKey,
|
|
294
326
|
{
|
|
327
|
+
fingerprint: options.fingerprint,
|
|
295
328
|
statusCode: response.statusCode,
|
|
296
329
|
response: JSON.stringify(response.body),
|
|
297
330
|
headers: response.headers ? JSON.stringify(response.headers) : void 0,
|
|
@@ -300,10 +333,10 @@ exports.IdempotencyService = class IdempotencyService {
|
|
|
300
333
|
ttl
|
|
301
334
|
);
|
|
302
335
|
}
|
|
303
|
-
async fail(key, error) {
|
|
336
|
+
async fail(key, error, options = {}) {
|
|
304
337
|
const fullKey = this.buildKey(key);
|
|
305
338
|
const ttlSeconds = Math.ceil((this.config.lockTimeout ?? 3e4) / 1e3);
|
|
306
|
-
await this.store.fail(fullKey, error, ttlSeconds);
|
|
339
|
+
await this.store.fail(fullKey, error, ttlSeconds, options.fingerprint);
|
|
307
340
|
}
|
|
308
341
|
async get(key) {
|
|
309
342
|
const fullKey = this.buildKey(key);
|
|
@@ -313,15 +346,37 @@ exports.IdempotencyService = class IdempotencyService {
|
|
|
313
346
|
const fullKey = this.buildKey(key);
|
|
314
347
|
return this.store.delete(fullKey);
|
|
315
348
|
}
|
|
316
|
-
|
|
349
|
+
/**
|
|
350
|
+
* Polls until the in-flight attempt completes or fails, bounded by
|
|
351
|
+
* waitTimeout.
|
|
352
|
+
*
|
|
353
|
+
* If the processing record VANISHES (the first attempt died between
|
|
354
|
+
* checkAndLock and complete, so its lock TTL expired), the waiter attempts
|
|
355
|
+
* an atomic takeover via checkAndLock: the winner returns 'takeover' (the
|
|
356
|
+
* caller executes the handler itself), losers keep waiting on the new
|
|
357
|
+
* owner's record. This turns a dead first attempt into a self-healing
|
|
358
|
+
* retry instead of surfacing an unexplained error to every waiter.
|
|
359
|
+
*
|
|
360
|
+
* @returns the completed/failed record to replay, or 'takeover' when this
|
|
361
|
+
* caller now owns the key and must execute the request
|
|
362
|
+
*/
|
|
363
|
+
async waitForCompletion(key, fingerprint, lockTimeoutMs, validateFingerprint) {
|
|
317
364
|
const waitTimeout = this.config.waitTimeout ?? 6e4;
|
|
318
365
|
const startTime = Date.now();
|
|
319
366
|
while (Date.now() - startTime < waitTimeout) {
|
|
320
367
|
const record = await this.store.get(key);
|
|
321
368
|
if (!record) {
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
369
|
+
const retry = await this.store.checkAndLock(key, fingerprint, lockTimeoutMs, validateFingerprint);
|
|
370
|
+
if (retry.status === "new") {
|
|
371
|
+
return "takeover";
|
|
372
|
+
}
|
|
373
|
+
if (retry.status === "completed" || retry.status === "failed") {
|
|
374
|
+
return retry.record;
|
|
375
|
+
}
|
|
376
|
+
if (retry.status === "fingerprint_mismatch") {
|
|
377
|
+
throw new IdempotencyFingerprintMismatchError(key);
|
|
378
|
+
}
|
|
379
|
+
} else if (record.status === "completed" || record.status === "failed") {
|
|
325
380
|
return record;
|
|
326
381
|
}
|
|
327
382
|
await this.sleep(POLL_INTERVAL_MS);
|
|
@@ -350,6 +405,7 @@ local key = KEYS[1]
|
|
|
350
405
|
local fingerprint = ARGV[1]
|
|
351
406
|
local lock_timeout = tonumber(ARGV[2])
|
|
352
407
|
local now = tonumber(ARGV[3])
|
|
408
|
+
local validate = ARGV[4] ~= '0'
|
|
353
409
|
|
|
354
410
|
-- Check if key exists
|
|
355
411
|
local existing = redis.call('HGETALL', key)
|
|
@@ -371,8 +427,8 @@ for i = 1, #existing, 2 do
|
|
|
371
427
|
record[existing[i]] = existing[i + 1]
|
|
372
428
|
end
|
|
373
429
|
|
|
374
|
-
-- Check fingerprint
|
|
375
|
-
if record.fingerprint ~= fingerprint then
|
|
430
|
+
-- Check fingerprint (skipped when validateFingerprint is disabled)
|
|
431
|
+
if validate and record.fingerprint ~= fingerprint then
|
|
376
432
|
return {'fingerprint_mismatch'}
|
|
377
433
|
end
|
|
378
434
|
|
|
@@ -414,9 +470,9 @@ var RedisIdempotencyStoreAdapter = class {
|
|
|
414
470
|
async onModuleInit() {
|
|
415
471
|
this.checkAndLockSha = await this.driver.scriptLoad(CHECK_AND_LOCK_SCRIPT);
|
|
416
472
|
}
|
|
417
|
-
async checkAndLock(key, fingerprint, lockTimeoutMs) {
|
|
473
|
+
async checkAndLock(key, fingerprint, lockTimeoutMs, validateFingerprint = true) {
|
|
418
474
|
const now = Date.now();
|
|
419
|
-
const rawResult = await this.driver.evalsha(this.checkAndLockSha, [key], [fingerprint, lockTimeoutMs, now]);
|
|
475
|
+
const rawResult = await this.driver.evalsha(this.checkAndLockSha, [key], [fingerprint, lockTimeoutMs, now, validateFingerprint ? "1" : "0"]);
|
|
420
476
|
const result = rawResult.map((v) => v === null || v === void 0 ? "" : String(v));
|
|
421
477
|
const status = result[0];
|
|
422
478
|
if (status === "new") {
|
|
@@ -444,21 +500,29 @@ var RedisIdempotencyStoreAdapter = class {
|
|
|
444
500
|
};
|
|
445
501
|
}
|
|
446
502
|
async complete(key, data, ttlSeconds) {
|
|
447
|
-
|
|
503
|
+
const fields = {
|
|
448
504
|
status: "completed",
|
|
449
505
|
statusCode: String(data.statusCode),
|
|
450
506
|
response: data.response,
|
|
451
507
|
headers: data.headers || "",
|
|
452
508
|
completedAt: String(data.completedAt)
|
|
453
|
-
}
|
|
509
|
+
};
|
|
510
|
+
if (data.fingerprint) {
|
|
511
|
+
fields.fingerprint = data.fingerprint;
|
|
512
|
+
}
|
|
513
|
+
await this.driver.hmset(key, fields);
|
|
454
514
|
await this.driver.expire(key, ttlSeconds);
|
|
455
515
|
}
|
|
456
|
-
async fail(key, error, ttlSeconds) {
|
|
457
|
-
|
|
516
|
+
async fail(key, error, ttlSeconds, fingerprint) {
|
|
517
|
+
const fields = {
|
|
458
518
|
status: "failed",
|
|
459
519
|
error,
|
|
460
520
|
completedAt: String(Date.now())
|
|
461
|
-
}
|
|
521
|
+
};
|
|
522
|
+
if (fingerprint) {
|
|
523
|
+
fields.fingerprint = fingerprint;
|
|
524
|
+
}
|
|
525
|
+
await this.driver.hmset(key, fields);
|
|
462
526
|
await this.driver.expire(key, ttlSeconds);
|
|
463
527
|
}
|
|
464
528
|
async get(key) {
|
|
@@ -468,12 +532,12 @@ var RedisIdempotencyStoreAdapter = class {
|
|
|
468
532
|
}
|
|
469
533
|
return {
|
|
470
534
|
key,
|
|
471
|
-
fingerprint: data.fingerprint,
|
|
535
|
+
fingerprint: data.fingerprint ?? "",
|
|
472
536
|
status: data.status,
|
|
473
537
|
statusCode: data.statusCode ? parseInt(data.statusCode, 10) : void 0,
|
|
474
538
|
response: data.response || void 0,
|
|
475
539
|
headers: data.headers || void 0,
|
|
476
|
-
startedAt: parseInt(data.startedAt, 10),
|
|
540
|
+
startedAt: data.startedAt ? parseInt(data.startedAt, 10) : 0,
|
|
477
541
|
completedAt: data.completedAt ? parseInt(data.completedAt, 10) : void 0,
|
|
478
542
|
error: data.error || void 0
|
|
479
543
|
};
|
|
@@ -513,7 +577,7 @@ var IdempotencyPlugin = class _IdempotencyPlugin {
|
|
|
513
577
|
return plugin;
|
|
514
578
|
}
|
|
515
579
|
static mergeDefaults(options) {
|
|
516
|
-
|
|
580
|
+
const merged = {
|
|
517
581
|
client: options.client,
|
|
518
582
|
defaultTtl: options.defaultTtl ?? DEFAULT_IDEMPOTENCY_CONFIG.defaultTtl,
|
|
519
583
|
keyPrefix: options.keyPrefix ?? DEFAULT_IDEMPOTENCY_CONFIG.keyPrefix,
|
|
@@ -525,6 +589,13 @@ var IdempotencyPlugin = class _IdempotencyPlugin {
|
|
|
525
589
|
errorPolicy: options.errorPolicy ?? DEFAULT_IDEMPOTENCY_CONFIG.errorPolicy,
|
|
526
590
|
fingerprintGenerator: options.fingerprintGenerator
|
|
527
591
|
};
|
|
592
|
+
for (const knob of ["defaultTtl", "lockTimeout", "waitTimeout"]) {
|
|
593
|
+
const value = merged[knob];
|
|
594
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
|
|
595
|
+
throw new IdempotencyConfigError(`${knob} must be a positive number (got ${String(value)})`);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
return merged;
|
|
528
599
|
}
|
|
529
600
|
getImports() {
|
|
530
601
|
return this.asyncOptions?.imports ?? [];
|
|
@@ -576,6 +647,7 @@ exports.IDEMPOTENCY_REDIS_DRIVER = IDEMPOTENCY_REDIS_DRIVER;
|
|
|
576
647
|
exports.IDEMPOTENCY_SERVICE = IDEMPOTENCY_SERVICE;
|
|
577
648
|
exports.IDEMPOTENCY_STORE = IDEMPOTENCY_STORE;
|
|
578
649
|
exports.IDEMPOTENT_OPTIONS = IDEMPOTENT_OPTIONS;
|
|
650
|
+
exports.IdempotencyConfigError = IdempotencyConfigError;
|
|
579
651
|
exports.IdempotencyError = IdempotencyError;
|
|
580
652
|
exports.IdempotencyFailedError = IdempotencyFailedError;
|
|
581
653
|
exports.IdempotencyFingerprintMismatchError = IdempotencyFingerprintMismatchError;
|