@nestjs-redisx/idempotency 1.0.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/LICENSE +21 -0
- package/README.md +52 -0
- package/dist/idempotency/api/decorators/idempotent.decorator.d.ts +12 -0
- package/dist/idempotency/api/decorators/idempotent.decorator.d.ts.map +1 -0
- package/dist/idempotency/api/interceptors/idempotency.interceptor.d.ts +19 -0
- package/dist/idempotency/api/interceptors/idempotency.interceptor.d.ts.map +1 -0
- package/dist/idempotency/application/ports/idempotency-service.port.d.ts +45 -0
- package/dist/idempotency/application/ports/idempotency-service.port.d.ts.map +1 -0
- package/dist/idempotency/application/ports/idempotency-store.port.d.ts +67 -0
- package/dist/idempotency/application/ports/idempotency-store.port.d.ts.map +1 -0
- package/dist/idempotency/application/services/idempotency.service.d.ts +27 -0
- package/dist/idempotency/application/services/idempotency.service.d.ts.map +1 -0
- package/dist/idempotency/infrastructure/adapters/redis-idempotency-store.adapter.d.ts +22 -0
- package/dist/idempotency/infrastructure/adapters/redis-idempotency-store.adapter.d.ts.map +1 -0
- package/dist/idempotency/infrastructure/scripts/lua-scripts.d.ts +24 -0
- package/dist/idempotency/infrastructure/scripts/lua-scripts.d.ts.map +1 -0
- package/dist/idempotency.plugin.d.ts +45 -0
- package/dist/idempotency.plugin.d.ts.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +459 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +446 -0
- package/dist/index.mjs.map +1 -0
- package/dist/shared/constants/index.d.ts +7 -0
- package/dist/shared/constants/index.d.ts.map +1 -0
- package/dist/shared/errors/index.d.ts +39 -0
- package/dist/shared/errors/index.d.ts.map +1 -0
- package/dist/shared/types/index.d.ts +79 -0
- package/dist/shared/types/index.d.ts.map +1 -0
- package/package.json +77 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 NestJS RedisX Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/nestjs-redisx/nestjs-redisx/main/website/public/images/logo.png" alt="NestJS RedisX" />
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
# @nestjs-redisx/idempotency
|
|
6
|
+
|
|
7
|
+
[](https://www.npmjs.com/package/@nestjs-redisx/idempotency)
|
|
8
|
+
[](https://www.npmjs.com/package/@nestjs-redisx/idempotency)
|
|
9
|
+
[](https://opensource.org/licenses/MIT)
|
|
10
|
+
|
|
11
|
+
HTTP idempotency plugin for NestJS RedisX. Prevents duplicate request processing using the `@Idempotent` decorator with fingerprint validation and automatic response replay.
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install @nestjs-redisx/core @nestjs-redisx/idempotency ioredis
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Quick Example
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
import { RedisModule } from '@nestjs-redisx/core';
|
|
23
|
+
import { IdempotencyPlugin, Idempotent } from '@nestjs-redisx/idempotency';
|
|
24
|
+
|
|
25
|
+
@Module({
|
|
26
|
+
imports: [
|
|
27
|
+
RedisModule.forRoot({
|
|
28
|
+
clients: { host: 'localhost', port: 6379 },
|
|
29
|
+
plugins: [new IdempotencyPlugin({ defaultTtl: 86400 })],
|
|
30
|
+
}),
|
|
31
|
+
],
|
|
32
|
+
})
|
|
33
|
+
export class AppModule {}
|
|
34
|
+
|
|
35
|
+
@Controller('payments')
|
|
36
|
+
export class PaymentsController {
|
|
37
|
+
@Post()
|
|
38
|
+
@Idempotent({ ttl: 3600 })
|
|
39
|
+
async createPayment(@Body() dto: CreatePaymentDto) {
|
|
40
|
+
// Executes once per Idempotency-Key header, replays cached response after
|
|
41
|
+
return this.paymentsService.create(dto);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Documentation
|
|
47
|
+
|
|
48
|
+
Full documentation: [nestjs-redisx.dev/en/reference/idempotency/](https://nestjs-redisx.dev/en/reference/idempotency/)
|
|
49
|
+
|
|
50
|
+
## License
|
|
51
|
+
|
|
52
|
+
MIT
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { ExecutionContext } from '@nestjs/common';
|
|
2
|
+
export declare const IDEMPOTENT_OPTIONS: unique symbol;
|
|
3
|
+
export interface IIdempotentOptions {
|
|
4
|
+
ttl?: number;
|
|
5
|
+
keyExtractor?: (context: ExecutionContext) => string | Promise<string>;
|
|
6
|
+
fingerprintFields?: ('method' | 'path' | 'body' | 'query')[];
|
|
7
|
+
validateFingerprint?: boolean;
|
|
8
|
+
cacheHeaders?: string[];
|
|
9
|
+
skip?: (context: ExecutionContext) => boolean | Promise<boolean>;
|
|
10
|
+
}
|
|
11
|
+
export declare function Idempotent(options?: IIdempotentOptions): MethodDecorator;
|
|
12
|
+
//# sourceMappingURL=idempotent.decorator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"idempotent.decorator.d.ts","sourceRoot":"","sources":["../../../../src/idempotency/api/decorators/idempotent.decorator.ts"],"names":[],"mappings":"AAAA,OAAO,EAAiD,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAIjG,eAAO,MAAM,kBAAkB,eAAmC,CAAC;AAEnE,MAAM,WAAW,kBAAkB;IACjC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,YAAY,CAAC,EAAE,CAAC,OAAO,EAAE,gBAAgB,KAAK,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACvE,iBAAiB,CAAC,EAAE,CAAC,QAAQ,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,EAAE,CAAC;IAC7D,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,gBAAgB,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CAClE;AAED,wBAAgB,UAAU,CAAC,OAAO,GAAE,kBAAuB,GAAG,eAAe,CAE5E"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
|
|
2
|
+
import { Reflector } from '@nestjs/core';
|
|
3
|
+
import { Observable } from 'rxjs';
|
|
4
|
+
import { IIdempotencyPluginOptions } from '../../../shared/types';
|
|
5
|
+
import { IIdempotencyService } from '../../application/ports/idempotency-service.port';
|
|
6
|
+
export declare class IdempotencyInterceptor implements NestInterceptor {
|
|
7
|
+
private readonly idempotencyService;
|
|
8
|
+
private readonly config;
|
|
9
|
+
private readonly reflector;
|
|
10
|
+
constructor(idempotencyService: IIdempotencyService, config: IIdempotencyPluginOptions, reflector: Reflector);
|
|
11
|
+
intercept(context: ExecutionContext, next: CallHandler): Promise<Observable<unknown>>;
|
|
12
|
+
private getOptions;
|
|
13
|
+
private extractKey;
|
|
14
|
+
private generateFingerprint;
|
|
15
|
+
private hash;
|
|
16
|
+
private replayResponse;
|
|
17
|
+
private extractHeaders;
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=idempotency.interceptor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"idempotency.interceptor.d.ts","sourceRoot":"","sources":["../../../../src/idempotency/api/interceptors/idempotency.interceptor.ts"],"names":[],"mappings":"AAEA,OAAO,EAAc,eAAe,EAAE,gBAAgB,EAAE,WAAW,EAAU,MAAM,gBAAgB,CAAC;AACpG,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACzC,OAAO,EAAE,UAAU,EAAM,MAAM,MAAM,CAAC;AAKtC,OAAO,EAAE,yBAAyB,EAAsB,MAAM,uBAAuB,CAAC;AACtF,OAAO,EAAE,mBAAmB,EAAE,MAAM,kDAAkD,CAAC;AAavF,qBACa,sBAAuB,YAAW,eAAe;IAE7B,OAAO,CAAC,QAAQ,CAAC,kBAAkB;IAC5B,OAAO,CAAC,QAAQ,CAAC,MAAM;IACxC,OAAO,CAAC,QAAQ,CAAC,SAAS;gBAFC,kBAAkB,EAAE,mBAAmB,EAChC,MAAM,EAAE,yBAAyB,EAClD,SAAS,EAAE,SAAS;IAGpD,SAAS,CAAC,OAAO,EAAE,gBAAgB,EAAE,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC;IAsD3F,OAAO,CAAC,UAAU;YAIJ,UAAU;YAWV,mBAAmB;IAmBjC,OAAO,CAAC,IAAI;IAIZ,OAAO,CAAC,cAAc;IActB,OAAO,CAAC,cAAc;CAWvB"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { IIdempotencyRecord, IIdempotencyCheckResult, IIdempotencyResponse, IIdempotencyOptions } from '../../../shared/types';
|
|
2
|
+
/**
|
|
3
|
+
* Service port for idempotency operations
|
|
4
|
+
*/
|
|
5
|
+
export interface IIdempotencyService {
|
|
6
|
+
/**
|
|
7
|
+
* Check if key exists and acquire lock if not.
|
|
8
|
+
*
|
|
9
|
+
* @param key - Idempotency key
|
|
10
|
+
* @param fingerprint - Request fingerprint hash
|
|
11
|
+
* @param options - Operation options
|
|
12
|
+
* @returns Check result with isNew flag and optional record
|
|
13
|
+
*/
|
|
14
|
+
checkAndLock(key: string, fingerprint: string, options?: IIdempotencyOptions): Promise<IIdempotencyCheckResult>;
|
|
15
|
+
/**
|
|
16
|
+
* Store successful response.
|
|
17
|
+
*
|
|
18
|
+
* @param key - Idempotency key
|
|
19
|
+
* @param response - Response to store
|
|
20
|
+
* @param options - Operation options
|
|
21
|
+
*/
|
|
22
|
+
complete(key: string, response: IIdempotencyResponse, options?: IIdempotencyOptions): Promise<void>;
|
|
23
|
+
/**
|
|
24
|
+
* Mark request as failed.
|
|
25
|
+
*
|
|
26
|
+
* @param key - Idempotency key
|
|
27
|
+
* @param error - Error message
|
|
28
|
+
*/
|
|
29
|
+
fail(key: string, error: string): Promise<void>;
|
|
30
|
+
/**
|
|
31
|
+
* Get existing record by key.
|
|
32
|
+
*
|
|
33
|
+
* @param key - Idempotency key
|
|
34
|
+
* @returns Record or null if not found
|
|
35
|
+
*/
|
|
36
|
+
get(key: string): Promise<IIdempotencyRecord | null>;
|
|
37
|
+
/**
|
|
38
|
+
* Delete record (for testing/admin).
|
|
39
|
+
*
|
|
40
|
+
* @param key - Idempotency key
|
|
41
|
+
* @returns True if deleted, false if not found
|
|
42
|
+
*/
|
|
43
|
+
delete(key: string): Promise<boolean>;
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=idempotency-service.port.d.ts.map
|
|
@@ -0,0 +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;IAEhD;;;;;OAKG;IACH,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,CAAC;IAErD;;;;;OAKG;IACH,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACvC"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { IIdempotencyRecord } from '../../../shared/types';
|
|
2
|
+
/**
|
|
3
|
+
* Result of check and lock operation
|
|
4
|
+
*/
|
|
5
|
+
export interface ICheckAndLockResult {
|
|
6
|
+
/** Status of the check */
|
|
7
|
+
status: 'new' | 'processing' | 'completed' | 'failed' | 'fingerprint_mismatch';
|
|
8
|
+
/** Record if exists */
|
|
9
|
+
record?: IIdempotencyRecord;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Data for completing a request
|
|
13
|
+
*/
|
|
14
|
+
export interface ICompleteData {
|
|
15
|
+
/** HTTP status code */
|
|
16
|
+
statusCode: number;
|
|
17
|
+
/** Response body (JSON string) */
|
|
18
|
+
response: string;
|
|
19
|
+
/** Response headers (JSON string) */
|
|
20
|
+
headers?: string;
|
|
21
|
+
/** Completion timestamp (ms) */
|
|
22
|
+
completedAt: number;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Store port for Redis operations
|
|
26
|
+
*/
|
|
27
|
+
export interface IIdempotencyStore {
|
|
28
|
+
/**
|
|
29
|
+
* Atomic check and lock operation using Lua script.
|
|
30
|
+
*
|
|
31
|
+
* @param key - Redis key
|
|
32
|
+
* @param fingerprint - Request fingerprint hash
|
|
33
|
+
* @param lockTimeoutMs - Lock timeout in milliseconds
|
|
34
|
+
* @returns Check result
|
|
35
|
+
*/
|
|
36
|
+
checkAndLock(key: string, fingerprint: string, lockTimeoutMs: number): Promise<ICheckAndLockResult>;
|
|
37
|
+
/**
|
|
38
|
+
* Mark record as completed with response data.
|
|
39
|
+
*
|
|
40
|
+
* @param key - Redis key
|
|
41
|
+
* @param data - Completion data
|
|
42
|
+
* @param ttlSeconds - TTL in seconds
|
|
43
|
+
*/
|
|
44
|
+
complete(key: string, data: ICompleteData, ttlSeconds: number): Promise<void>;
|
|
45
|
+
/**
|
|
46
|
+
* Mark record as failed.
|
|
47
|
+
*
|
|
48
|
+
* @param key - Redis key
|
|
49
|
+
* @param error - Error message
|
|
50
|
+
*/
|
|
51
|
+
fail(key: string, error: string): Promise<void>;
|
|
52
|
+
/**
|
|
53
|
+
* Get record by key.
|
|
54
|
+
*
|
|
55
|
+
* @param key - Redis key
|
|
56
|
+
* @returns Record or null if not found
|
|
57
|
+
*/
|
|
58
|
+
get(key: string): Promise<IIdempotencyRecord | null>;
|
|
59
|
+
/**
|
|
60
|
+
* Delete record.
|
|
61
|
+
*
|
|
62
|
+
* @param key - Redis key
|
|
63
|
+
* @returns True if deleted, false if not found
|
|
64
|
+
*/
|
|
65
|
+
delete(key: string): Promise<boolean>;
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=idempotency-store.port.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"idempotency-store.port.d.ts","sourceRoot":"","sources":["../../../../src/idempotency/application/ports/idempotency-store.port.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAE3D;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,0BAA0B;IAC1B,MAAM,EAAE,KAAK,GAAG,YAAY,GAAG,WAAW,GAAG,QAAQ,GAAG,sBAAsB,CAAC;IAE/E,uBAAuB;IACvB,MAAM,CAAC,EAAE,kBAAkB,CAAC;CAC7B;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,uBAAuB;IACvB,UAAU,EAAE,MAAM,CAAC;IAEnB,kCAAkC;IAClC,QAAQ,EAAE,MAAM,CAAC;IAEjB,qCAAqC;IACrC,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB,gCAAgC;IAChC,WAAW,EAAE,MAAM,CAAC;CACrB;AAED;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC;;;;;;;OAOG;IACH,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAAC;IAEpG;;;;;;OAMG;IACH,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAE9E;;;;;OAKG;IACH,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAEhD;;;;;OAKG;IACH,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,CAAC;IAErD;;;;;OAKG;IACH,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACvC"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { IIdempotencyPluginOptions, IIdempotencyRecord, IIdempotencyCheckResult, IIdempotencyResponse, IIdempotencyOptions } from '../../../shared/types';
|
|
2
|
+
import { IIdempotencyService } from '../ports/idempotency-service.port';
|
|
3
|
+
import { IIdempotencyStore } from '../ports/idempotency-store.port';
|
|
4
|
+
interface IMetricsService {
|
|
5
|
+
incrementCounter(name: string, labels?: Record<string, string>, value?: number): void;
|
|
6
|
+
observeHistogram(name: string, value: number, labels?: Record<string, string>): void;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Idempotency service implementation
|
|
10
|
+
*/
|
|
11
|
+
export declare class IdempotencyService implements IIdempotencyService {
|
|
12
|
+
private readonly config;
|
|
13
|
+
private readonly store;
|
|
14
|
+
private readonly metrics?;
|
|
15
|
+
constructor(config: IIdempotencyPluginOptions, store: IIdempotencyStore, metrics?: IMetricsService | undefined);
|
|
16
|
+
checkAndLock(key: string, fingerprint: string, options?: IIdempotencyOptions): Promise<IIdempotencyCheckResult>;
|
|
17
|
+
private recordDuration;
|
|
18
|
+
complete(key: string, response: IIdempotencyResponse, options?: IIdempotencyOptions): Promise<void>;
|
|
19
|
+
fail(key: string, error: string): Promise<void>;
|
|
20
|
+
get(key: string): Promise<IIdempotencyRecord | null>;
|
|
21
|
+
delete(key: string): Promise<boolean>;
|
|
22
|
+
private waitForCompletion;
|
|
23
|
+
private buildKey;
|
|
24
|
+
private sleep;
|
|
25
|
+
}
|
|
26
|
+
export {};
|
|
27
|
+
//# sourceMappingURL=idempotency.service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"idempotency.service.d.ts","sourceRoot":"","sources":["../../../../src/idempotency/application/services/idempotency.service.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,yBAAyB,EAAE,kBAAkB,EAAE,uBAAuB,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAC1J,OAAO,EAAE,mBAAmB,EAAE,MAAM,mCAAmC,CAAC;AACxE,OAAO,EAAE,iBAAiB,EAAE,MAAM,iCAAiC,CAAC;AAKpE,UAAU,eAAe;IACvB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtF,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CAAC;CACtF;AAED;;GAEG;AACH,qBACa,kBAAmB,YAAW,mBAAmB;IAG1D,OAAO,CAAC,QAAQ,CAAC,MAAM;IAEvB,OAAO,CAAC,QAAQ,CAAC,KAAK;IACe,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC;gBAH7C,MAAM,EAAE,yBAAyB,EAEjC,KAAK,EAAE,iBAAiB,EACa,OAAO,CAAC,EAAE,eAAe,YAAA;IAG3E,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,OAAO,GAAE,mBAAwB,GAAG,OAAO,CAAC,uBAAuB,CAAC;IAiCzH,OAAO,CAAC,cAAc;IAKhB,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,oBAAoB,EAAE,OAAO,GAAE,mBAAwB,GAAG,OAAO,CAAC,IAAI,CAAC;IAgBvG,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAK/C,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC;IAKpD,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;YAK7B,iBAAiB;IAoB/B,OAAO,CAAC,QAAQ;IAKhB,OAAO,CAAC,KAAK;CAGd"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { OnModuleInit } from '@nestjs/common';
|
|
2
|
+
import { IRedisDriver } from '@nestjs-redisx/core';
|
|
3
|
+
import { IIdempotencyRecord } from '../../../shared/types';
|
|
4
|
+
import { IIdempotencyStore, ICheckAndLockResult, ICompleteData } from '../../application/ports/idempotency-store.port';
|
|
5
|
+
/**
|
|
6
|
+
* Redis-based idempotency store implementation
|
|
7
|
+
*/
|
|
8
|
+
export declare class RedisIdempotencyStoreAdapter implements IIdempotencyStore, OnModuleInit {
|
|
9
|
+
private readonly driver;
|
|
10
|
+
private checkAndLockSha;
|
|
11
|
+
constructor(driver: IRedisDriver);
|
|
12
|
+
/**
|
|
13
|
+
* Pre-load Lua script on module initialization
|
|
14
|
+
*/
|
|
15
|
+
onModuleInit(): Promise<void>;
|
|
16
|
+
checkAndLock(key: string, fingerprint: string, lockTimeoutMs: number): Promise<ICheckAndLockResult>;
|
|
17
|
+
complete(key: string, data: ICompleteData, ttlSeconds: number): Promise<void>;
|
|
18
|
+
fail(key: string, error: string): Promise<void>;
|
|
19
|
+
get(key: string): Promise<IIdempotencyRecord | null>;
|
|
20
|
+
delete(key: string): Promise<boolean>;
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=redis-idempotency-store.adapter.d.ts.map
|
|
@@ -0,0 +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,EAAgB,MAAM,qBAAqB,CAAC;AAEjE,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;IAGhD,OAAO,CAAC,QAAQ,CAAC,MAAM;IAFzD,OAAO,CAAC,eAAe,CAAuB;gBAEK,MAAM,EAAE,YAAY;IAEvE;;OAEG;IACG,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC;IAI7B,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,CAAC;IAqCnG,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAW7E,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAQ/C,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC;IAmBpD,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;CAI5C"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inline Lua scripts for idempotency operations.
|
|
3
|
+
*
|
|
4
|
+
* Scripts are stored as inline strings to avoid issues with file reading
|
|
5
|
+
* after build (dist directory doesn't contain .lua files).
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Check and Lock Lua script for idempotency
|
|
9
|
+
*
|
|
10
|
+
* This script atomically checks if an idempotency key exists and locks it if new.
|
|
11
|
+
*
|
|
12
|
+
* KEYS[1] = idempotency key
|
|
13
|
+
* ARGV[1] = fingerprint
|
|
14
|
+
* ARGV[2] = lock timeout (ms)
|
|
15
|
+
* ARGV[3] = current timestamp (ms)
|
|
16
|
+
*
|
|
17
|
+
* Returns:
|
|
18
|
+
* - ['new'] - new request, lock acquired
|
|
19
|
+
* - ['fingerprint_mismatch'] - same key, different fingerprint
|
|
20
|
+
* - ['processing'] - another request is processing
|
|
21
|
+
* - [status, statusCode, response, headers, error] - completed/failed record
|
|
22
|
+
*/
|
|
23
|
+
export declare const CHECK_AND_LOCK_SCRIPT: string;
|
|
24
|
+
//# sourceMappingURL=lua-scripts.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lua-scripts.d.ts","sourceRoot":"","sources":["../../../../src/idempotency/infrastructure/scripts/lua-scripts.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,qBAAqB,QAuD1B,CAAC"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Idempotency plugin for NestJS RedisX.
|
|
3
|
+
* Provides request deduplication with response replay for idempotent operations.
|
|
4
|
+
*/
|
|
5
|
+
import { Provider } from '@nestjs/common';
|
|
6
|
+
import { IRedisXPlugin } from '@nestjs-redisx/core';
|
|
7
|
+
import { IIdempotencyPluginOptions } from './shared/types';
|
|
8
|
+
/**
|
|
9
|
+
* Idempotency plugin for NestJS RedisX.
|
|
10
|
+
*
|
|
11
|
+
* Provides request deduplication with response replay:
|
|
12
|
+
* - Prevents duplicate processing of same request
|
|
13
|
+
* - Replays successful responses
|
|
14
|
+
* - Handles concurrent requests
|
|
15
|
+
* - Validates request fingerprints
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```typescript
|
|
19
|
+
* @Module({
|
|
20
|
+
* imports: [
|
|
21
|
+
* RedisModule.forRoot({
|
|
22
|
+
* clients: { host: 'localhost', port: 6379 },
|
|
23
|
+
* plugins: [
|
|
24
|
+
* new IdempotencyPlugin({
|
|
25
|
+
* defaultTtl: 86400,
|
|
26
|
+
* headerName: 'Idempotency-Key',
|
|
27
|
+
* validateFingerprint: true,
|
|
28
|
+
* }),
|
|
29
|
+
* ],
|
|
30
|
+
* }),
|
|
31
|
+
* ],
|
|
32
|
+
* })
|
|
33
|
+
* export class AppModule {}
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export declare class IdempotencyPlugin implements IRedisXPlugin {
|
|
37
|
+
private readonly options;
|
|
38
|
+
readonly name = "idempotency";
|
|
39
|
+
readonly version = "0.1.0";
|
|
40
|
+
readonly description = "Request deduplication with response replay for idempotent operations";
|
|
41
|
+
constructor(options?: IIdempotencyPluginOptions);
|
|
42
|
+
getProviders(): Provider[];
|
|
43
|
+
getExports(): Array<string | symbol | Provider>;
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=idempotency.plugin.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"idempotency.plugin.d.ts","sourceRoot":"","sources":["../src/idempotency.plugin.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAE1C,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAMpD,OAAO,EAAE,yBAAyB,EAAE,MAAM,gBAAgB,CAAC;AAa3D;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,qBAAa,iBAAkB,YAAW,aAAa;IAKzC,OAAO,CAAC,QAAQ,CAAC,OAAO;IAJpC,QAAQ,CAAC,IAAI,iBAAiB;IAC9B,QAAQ,CAAC,OAAO,WAAW;IAC3B,QAAQ,CAAC,WAAW,0EAA0E;gBAEjE,OAAO,GAAE,yBAA8B;IAEpE,YAAY,IAAI,QAAQ,EAAE;IAuB1B,UAAU,IAAI,KAAK,CAAC,MAAM,GAAG,MAAM,GAAG,QAAQ,CAAC;CAGhD"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { IdempotencyPlugin } from './idempotency.plugin';
|
|
2
|
+
export { IdempotencyService } from './idempotency/application/services/idempotency.service';
|
|
3
|
+
export type { IIdempotencyService } from './idempotency/application/ports/idempotency-service.port';
|
|
4
|
+
export type { IIdempotencyStore } from './idempotency/application/ports/idempotency-store.port';
|
|
5
|
+
export { Idempotent, type IIdempotentOptions, IDEMPOTENT_OPTIONS } from './idempotency/api/decorators/idempotent.decorator';
|
|
6
|
+
export { IdempotencyInterceptor } from './idempotency/api/interceptors/idempotency.interceptor';
|
|
7
|
+
export type { IIdempotencyPluginOptions, IIdempotencyRecord, IIdempotencyCheckResult, IIdempotencyResponse, IIdempotencyOptions } from './shared/types';
|
|
8
|
+
export { IdempotencyError, IdempotencyKeyRequiredError, IdempotencyFingerprintMismatchError, IdempotencyTimeoutError, IdempotencyFailedError, IdempotencyRecordNotFoundError } from './shared/errors';
|
|
9
|
+
export { IDEMPOTENCY_PLUGIN_OPTIONS, IDEMPOTENCY_SERVICE, IDEMPOTENCY_STORE } from './shared/constants';
|
|
10
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +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;AAGtM,OAAO,EAAE,0BAA0B,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC"}
|