@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.
Files changed (31) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +52 -0
  3. package/dist/idempotency/api/decorators/idempotent.decorator.d.ts +12 -0
  4. package/dist/idempotency/api/decorators/idempotent.decorator.d.ts.map +1 -0
  5. package/dist/idempotency/api/interceptors/idempotency.interceptor.d.ts +19 -0
  6. package/dist/idempotency/api/interceptors/idempotency.interceptor.d.ts.map +1 -0
  7. package/dist/idempotency/application/ports/idempotency-service.port.d.ts +45 -0
  8. package/dist/idempotency/application/ports/idempotency-service.port.d.ts.map +1 -0
  9. package/dist/idempotency/application/ports/idempotency-store.port.d.ts +67 -0
  10. package/dist/idempotency/application/ports/idempotency-store.port.d.ts.map +1 -0
  11. package/dist/idempotency/application/services/idempotency.service.d.ts +27 -0
  12. package/dist/idempotency/application/services/idempotency.service.d.ts.map +1 -0
  13. package/dist/idempotency/infrastructure/adapters/redis-idempotency-store.adapter.d.ts +22 -0
  14. package/dist/idempotency/infrastructure/adapters/redis-idempotency-store.adapter.d.ts.map +1 -0
  15. package/dist/idempotency/infrastructure/scripts/lua-scripts.d.ts +24 -0
  16. package/dist/idempotency/infrastructure/scripts/lua-scripts.d.ts.map +1 -0
  17. package/dist/idempotency.plugin.d.ts +45 -0
  18. package/dist/idempotency.plugin.d.ts.map +1 -0
  19. package/dist/index.d.ts +10 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js +459 -0
  22. package/dist/index.js.map +1 -0
  23. package/dist/index.mjs +446 -0
  24. package/dist/index.mjs.map +1 -0
  25. package/dist/shared/constants/index.d.ts +7 -0
  26. package/dist/shared/constants/index.d.ts.map +1 -0
  27. package/dist/shared/errors/index.d.ts +39 -0
  28. package/dist/shared/errors/index.d.ts.map +1 -0
  29. package/dist/shared/types/index.d.ts +79 -0
  30. package/dist/shared/types/index.d.ts.map +1 -0
  31. package/package.json +77 -0
package/dist/index.mjs ADDED
@@ -0,0 +1,446 @@
1
+ import { Reflector } from '@nestjs/core';
2
+ import { createHash } from 'crypto';
3
+ import { Injectable, Inject, Optional, applyDecorators, SetMetadata, UseInterceptors } from '@nestjs/common';
4
+ import { of } from 'rxjs';
5
+ import { tap } from 'rxjs/operators';
6
+ import { REDIS_DRIVER, RedisXError, ErrorCode } from '@nestjs-redisx/core';
7
+
8
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
9
+ var __decorateClass = (decorators, target, key, kind) => {
10
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
11
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
12
+ if (decorator = decorators[i])
13
+ result = (decorator(result)) || result;
14
+ return result;
15
+ };
16
+ var __decorateParam = (index, decorator) => (target, key) => decorator(target, key, index);
17
+
18
+ // src/shared/constants/index.ts
19
+ var IDEMPOTENCY_PLUGIN_OPTIONS = /* @__PURE__ */ Symbol.for("IDEMPOTENCY_PLUGIN_OPTIONS");
20
+ var IDEMPOTENCY_SERVICE = /* @__PURE__ */ Symbol.for("IDEMPOTENCY_SERVICE");
21
+ var IDEMPOTENCY_STORE = /* @__PURE__ */ Symbol.for("IDEMPOTENCY_STORE");
22
+ var IdempotencyError = class extends RedisXError {
23
+ constructor(message, code, idempotencyKey, cause) {
24
+ super(message, code, cause, { idempotencyKey });
25
+ this.idempotencyKey = idempotencyKey;
26
+ }
27
+ };
28
+ var IdempotencyKeyRequiredError = class extends IdempotencyError {
29
+ constructor() {
30
+ super("Idempotency-Key header is required", ErrorCode.IDEMPOTENCY_KEY_INVALID, "");
31
+ }
32
+ };
33
+ var IdempotencyFingerprintMismatchError = class extends IdempotencyError {
34
+ constructor(key) {
35
+ super(`Request body does not match previous request with idempotency key "${key}"`, ErrorCode.IDEMPOTENCY_KEY_INVALID, key);
36
+ }
37
+ };
38
+ var IdempotencyTimeoutError = class extends IdempotencyError {
39
+ constructor(key) {
40
+ super(`Timeout waiting for concurrent request with idempotency key "${key}"`, ErrorCode.OP_TIMEOUT, key);
41
+ }
42
+ };
43
+ var IdempotencyFailedError = class extends IdempotencyError {
44
+ constructor(key, error) {
45
+ super(`Previous request with idempotency key "${key}" failed${error ? `: ${error}` : ""}`, ErrorCode.IDEMPOTENCY_PREVIOUS_FAILED, key);
46
+ }
47
+ };
48
+ var IdempotencyRecordNotFoundError = class extends IdempotencyError {
49
+ constructor(key) {
50
+ super(`Idempotency record not found for key "${key}"`, ErrorCode.OP_KEY_NOT_FOUND, key);
51
+ }
52
+ };
53
+ var IDEMPOTENT_OPTIONS = /* @__PURE__ */ Symbol.for("IDEMPOTENT_OPTIONS");
54
+ function Idempotent(options = {}) {
55
+ return applyDecorators(SetMetadata(IDEMPOTENT_OPTIONS, options), UseInterceptors(IdempotencyInterceptor));
56
+ }
57
+
58
+ // src/idempotency/api/interceptors/idempotency.interceptor.ts
59
+ var IdempotencyInterceptor = class {
60
+ constructor(idempotencyService, config, reflector) {
61
+ this.idempotencyService = idempotencyService;
62
+ this.config = config;
63
+ this.reflector = reflector;
64
+ }
65
+ async intercept(context, next) {
66
+ const options = this.getOptions(context);
67
+ const response = context.switchToHttp().getResponse();
68
+ const key = await this.extractKey(context, options);
69
+ if (!key) {
70
+ return next.handle();
71
+ }
72
+ if (options.skip && await options.skip(context)) {
73
+ return next.handle();
74
+ }
75
+ const fingerprint = await this.generateFingerprint(context, options);
76
+ const checkResult = await this.idempotencyService.checkAndLock(key, fingerprint, {
77
+ ttl: options.ttl
78
+ });
79
+ if (!checkResult.isNew) {
80
+ if (checkResult.fingerprintMismatch) {
81
+ throw new IdempotencyFingerprintMismatchError(key);
82
+ }
83
+ const record = checkResult.record;
84
+ if (record.status === "failed") {
85
+ throw new IdempotencyFailedError(key, record.error);
86
+ }
87
+ return this.replayResponse(response, record);
88
+ }
89
+ return next.handle().pipe(
90
+ tap({
91
+ next: (data) => {
92
+ void this.idempotencyService.complete(
93
+ key,
94
+ {
95
+ statusCode: response.statusCode,
96
+ body: data,
97
+ headers: this.extractHeaders(response, options)
98
+ },
99
+ { ttl: options.ttl }
100
+ );
101
+ },
102
+ error: (error) => {
103
+ void this.idempotencyService.fail(key, error.message);
104
+ }
105
+ })
106
+ );
107
+ }
108
+ getOptions(context) {
109
+ return this.reflector.get(IDEMPOTENT_OPTIONS, context.getHandler()) ?? {};
110
+ }
111
+ async extractKey(context, options) {
112
+ if (options.keyExtractor) {
113
+ return options.keyExtractor(context);
114
+ }
115
+ const request = context.switchToHttp().getRequest();
116
+ const headerName = this.config.headerName ?? "Idempotency-Key";
117
+ return request.headers[headerName.toLowerCase()] ?? null;
118
+ }
119
+ async generateFingerprint(context, options) {
120
+ if (this.config.fingerprintGenerator) {
121
+ return this.config.fingerprintGenerator(context);
122
+ }
123
+ const request = context.switchToHttp().getRequest();
124
+ const fields = options.fingerprintFields ?? this.config.fingerprintFields ?? ["method", "path", "body"];
125
+ const parts = [];
126
+ if (fields.includes("method")) parts.push(request.method);
127
+ if (fields.includes("path")) parts.push(request.path);
128
+ if (fields.includes("body")) parts.push(JSON.stringify(request.body ?? {}));
129
+ if (fields.includes("query")) parts.push(JSON.stringify(request.query ?? {}));
130
+ const data = parts.join("|");
131
+ return this.hash(data);
132
+ }
133
+ hash(data) {
134
+ return createHash("sha256").update(data).digest("hex");
135
+ }
136
+ replayResponse(response, record) {
137
+ response.status(record.statusCode ?? 200);
138
+ if (record.headers) {
139
+ const headers = JSON.parse(record.headers);
140
+ for (const [key, value] of Object.entries(headers)) {
141
+ response.setHeader(key, value);
142
+ }
143
+ }
144
+ const body = record.response ? JSON.parse(record.response) : null;
145
+ return of(body);
146
+ }
147
+ extractHeaders(response, options) {
148
+ if (!options.cacheHeaders?.length) return void 0;
149
+ const headers = {};
150
+ for (const name of options.cacheHeaders) {
151
+ const value = response.getHeader(name);
152
+ if (value) headers[name] = String(value);
153
+ }
154
+ return Object.keys(headers).length > 0 ? headers : void 0;
155
+ }
156
+ };
157
+ IdempotencyInterceptor = __decorateClass([
158
+ Injectable(),
159
+ __decorateParam(0, Inject(IDEMPOTENCY_SERVICE)),
160
+ __decorateParam(1, Inject(IDEMPOTENCY_PLUGIN_OPTIONS)),
161
+ __decorateParam(2, Inject(Reflector))
162
+ ], IdempotencyInterceptor);
163
+ var POLL_INTERVAL_MS = 100;
164
+ var METRICS_SERVICE = /* @__PURE__ */ Symbol.for("METRICS_SERVICE");
165
+ var IdempotencyService = class {
166
+ constructor(config, store, metrics) {
167
+ this.config = config;
168
+ this.store = store;
169
+ this.metrics = metrics;
170
+ }
171
+ async checkAndLock(key, fingerprint, options = {}) {
172
+ const startTime = Date.now();
173
+ const fullKey = this.buildKey(key);
174
+ const lockTimeout = options.lockTimeout ?? this.config.lockTimeout ?? 3e4;
175
+ const result = await this.store.checkAndLock(fullKey, fingerprint, lockTimeout);
176
+ if (result.status === "new") {
177
+ this.metrics?.incrementCounter("redisx_idempotency_requests_total", { status: "new" });
178
+ this.recordDuration(startTime);
179
+ return { isNew: true };
180
+ }
181
+ if (result.status === "fingerprint_mismatch") {
182
+ this.metrics?.incrementCounter("redisx_idempotency_requests_total", { status: "mismatch" });
183
+ this.recordDuration(startTime);
184
+ return { isNew: false, fingerprintMismatch: true };
185
+ }
186
+ if (result.status === "processing") {
187
+ const record = await this.waitForCompletion(fullKey);
188
+ this.metrics?.incrementCounter("redisx_idempotency_requests_total", { status: "replay" });
189
+ this.recordDuration(startTime);
190
+ return { isNew: false, record };
191
+ }
192
+ this.metrics?.incrementCounter("redisx_idempotency_requests_total", { status: "replay" });
193
+ this.recordDuration(startTime);
194
+ return { isNew: false, record: result.record };
195
+ }
196
+ recordDuration(startTime) {
197
+ const duration = (Date.now() - startTime) / 1e3;
198
+ this.metrics?.observeHistogram("redisx_idempotency_duration_seconds", duration);
199
+ }
200
+ async complete(key, response, options = {}) {
201
+ const fullKey = this.buildKey(key);
202
+ const ttl = options.ttl ?? this.config.defaultTtl ?? 86400;
203
+ await this.store.complete(
204
+ fullKey,
205
+ {
206
+ statusCode: response.statusCode,
207
+ response: JSON.stringify(response.body),
208
+ headers: response.headers ? JSON.stringify(response.headers) : void 0,
209
+ completedAt: Date.now()
210
+ },
211
+ ttl
212
+ );
213
+ }
214
+ async fail(key, error) {
215
+ const fullKey = this.buildKey(key);
216
+ await this.store.fail(fullKey, error);
217
+ }
218
+ async get(key) {
219
+ const fullKey = this.buildKey(key);
220
+ return this.store.get(fullKey);
221
+ }
222
+ async delete(key) {
223
+ const fullKey = this.buildKey(key);
224
+ return this.store.delete(fullKey);
225
+ }
226
+ async waitForCompletion(key) {
227
+ const waitTimeout = this.config.waitTimeout ?? 6e4;
228
+ const startTime = Date.now();
229
+ while (Date.now() - startTime < waitTimeout) {
230
+ const record = await this.store.get(key);
231
+ if (!record) {
232
+ throw new IdempotencyRecordNotFoundError(key);
233
+ }
234
+ if (record.status === "completed" || record.status === "failed") {
235
+ return record;
236
+ }
237
+ await this.sleep(POLL_INTERVAL_MS);
238
+ }
239
+ throw new IdempotencyTimeoutError(key);
240
+ }
241
+ buildKey(key) {
242
+ const prefix = this.config.keyPrefix ?? "idempotency:";
243
+ return `${prefix}${key}`;
244
+ }
245
+ sleep(ms) {
246
+ return new Promise((resolve) => setTimeout(resolve, ms));
247
+ }
248
+ };
249
+ IdempotencyService = __decorateClass([
250
+ Injectable(),
251
+ __decorateParam(0, Inject(IDEMPOTENCY_PLUGIN_OPTIONS)),
252
+ __decorateParam(1, Inject(IDEMPOTENCY_STORE)),
253
+ __decorateParam(2, Optional()),
254
+ __decorateParam(2, Inject(METRICS_SERVICE))
255
+ ], IdempotencyService);
256
+
257
+ // src/idempotency/infrastructure/scripts/lua-scripts.ts
258
+ var CHECK_AND_LOCK_SCRIPT = `
259
+ local key = KEYS[1]
260
+ local fingerprint = ARGV[1]
261
+ local lock_timeout = tonumber(ARGV[2])
262
+ local now = tonumber(ARGV[3])
263
+
264
+ -- Check if key exists
265
+ local existing = redis.call('HGETALL', key)
266
+
267
+ if #existing == 0 then
268
+ -- New request - create lock
269
+ redis.call('HMSET', key,
270
+ 'fingerprint', fingerprint,
271
+ 'status', 'processing',
272
+ 'startedAt', now
273
+ )
274
+ redis.call('PEXPIRE', key, lock_timeout)
275
+ return {'new'}
276
+ end
277
+
278
+ -- Convert to table
279
+ local record = {}
280
+ for i = 1, #existing, 2 do
281
+ record[existing[i]] = existing[i + 1]
282
+ end
283
+
284
+ -- Check fingerprint
285
+ if record.fingerprint ~= fingerprint then
286
+ return {'fingerprint_mismatch'}
287
+ end
288
+
289
+ -- Check status
290
+ if record.status == 'processing' then
291
+ -- Check if lock expired (stale)
292
+ local started = tonumber(record.startedAt)
293
+ if now - started > lock_timeout then
294
+ -- Stale lock - take over
295
+ redis.call('HMSET', key,
296
+ 'status', 'processing',
297
+ 'startedAt', now
298
+ )
299
+ redis.call('PEXPIRE', key, lock_timeout)
300
+ return {'new'}
301
+ end
302
+ return {'processing'}
303
+ end
304
+
305
+ -- Completed or failed - return record
306
+ return {
307
+ record.status,
308
+ record.statusCode or '',
309
+ record.response or '',
310
+ record.headers or '',
311
+ record.error or ''
312
+ }
313
+ `.trim();
314
+
315
+ // src/idempotency/infrastructure/adapters/redis-idempotency-store.adapter.ts
316
+ var RedisIdempotencyStoreAdapter = class {
317
+ constructor(driver) {
318
+ this.driver = driver;
319
+ }
320
+ checkAndLockSha = null;
321
+ /**
322
+ * Pre-load Lua script on module initialization
323
+ */
324
+ async onModuleInit() {
325
+ this.checkAndLockSha = await this.driver.scriptLoad(CHECK_AND_LOCK_SCRIPT);
326
+ }
327
+ async checkAndLock(key, fingerprint, lockTimeoutMs) {
328
+ const now = Date.now();
329
+ const rawResult = await this.driver.evalsha(this.checkAndLockSha, [key], [fingerprint, lockTimeoutMs, now]);
330
+ const result = rawResult.map((v) => v === null || v === void 0 ? "" : String(v));
331
+ const status = result[0];
332
+ if (status === "new") {
333
+ return { status: "new" };
334
+ }
335
+ if (status === "fingerprint_mismatch") {
336
+ return { status: "fingerprint_mismatch" };
337
+ }
338
+ if (status === "processing") {
339
+ return { status: "processing" };
340
+ }
341
+ return {
342
+ status,
343
+ record: {
344
+ key,
345
+ fingerprint,
346
+ status,
347
+ statusCode: result[1] ? parseInt(result[1], 10) : void 0,
348
+ response: result[2] || void 0,
349
+ headers: result[3] || void 0,
350
+ error: result[4] || void 0,
351
+ startedAt: 0
352
+ // Not returned from Lua
353
+ }
354
+ };
355
+ }
356
+ async complete(key, data, ttlSeconds) {
357
+ await this.driver.hmset(key, {
358
+ status: "completed",
359
+ statusCode: String(data.statusCode),
360
+ response: data.response,
361
+ headers: data.headers || "",
362
+ completedAt: String(data.completedAt)
363
+ });
364
+ await this.driver.expire(key, ttlSeconds);
365
+ }
366
+ async fail(key, error) {
367
+ await this.driver.hmset(key, {
368
+ status: "failed",
369
+ error,
370
+ completedAt: String(Date.now())
371
+ });
372
+ }
373
+ async get(key) {
374
+ const data = await this.driver.hgetall(key);
375
+ if (!data || Object.keys(data).length === 0) {
376
+ return null;
377
+ }
378
+ return {
379
+ key,
380
+ fingerprint: data.fingerprint,
381
+ status: data.status,
382
+ statusCode: data.statusCode ? parseInt(data.statusCode, 10) : void 0,
383
+ response: data.response || void 0,
384
+ headers: data.headers || void 0,
385
+ startedAt: parseInt(data.startedAt, 10),
386
+ completedAt: data.completedAt ? parseInt(data.completedAt, 10) : void 0,
387
+ error: data.error || void 0
388
+ };
389
+ }
390
+ async delete(key) {
391
+ const result = await this.driver.del(key);
392
+ return result > 0;
393
+ }
394
+ };
395
+ RedisIdempotencyStoreAdapter = __decorateClass([
396
+ Injectable(),
397
+ __decorateParam(0, Inject(REDIS_DRIVER))
398
+ ], RedisIdempotencyStoreAdapter);
399
+
400
+ // src/idempotency.plugin.ts
401
+ var DEFAULT_IDEMPOTENCY_CONFIG = {
402
+ defaultTtl: 86400,
403
+ keyPrefix: "idempotency:",
404
+ headerName: "Idempotency-Key",
405
+ lockTimeout: 3e4,
406
+ waitTimeout: 6e4,
407
+ validateFingerprint: true,
408
+ fingerprintFields: ["method", "path", "body"],
409
+ errorPolicy: "fail-closed"
410
+ };
411
+ var IdempotencyPlugin = class {
412
+ constructor(options = {}) {
413
+ this.options = options;
414
+ }
415
+ name = "idempotency";
416
+ version = "0.1.0";
417
+ description = "Request deduplication with response replay for idempotent operations";
418
+ getProviders() {
419
+ const config = {
420
+ defaultTtl: this.options.defaultTtl ?? DEFAULT_IDEMPOTENCY_CONFIG.defaultTtl,
421
+ keyPrefix: this.options.keyPrefix ?? DEFAULT_IDEMPOTENCY_CONFIG.keyPrefix,
422
+ headerName: this.options.headerName ?? DEFAULT_IDEMPOTENCY_CONFIG.headerName,
423
+ lockTimeout: this.options.lockTimeout ?? DEFAULT_IDEMPOTENCY_CONFIG.lockTimeout,
424
+ waitTimeout: this.options.waitTimeout ?? DEFAULT_IDEMPOTENCY_CONFIG.waitTimeout,
425
+ validateFingerprint: this.options.validateFingerprint ?? DEFAULT_IDEMPOTENCY_CONFIG.validateFingerprint,
426
+ fingerprintFields: this.options.fingerprintFields ?? DEFAULT_IDEMPOTENCY_CONFIG.fingerprintFields,
427
+ errorPolicy: this.options.errorPolicy ?? DEFAULT_IDEMPOTENCY_CONFIG.errorPolicy,
428
+ fingerprintGenerator: this.options.fingerprintGenerator
429
+ };
430
+ return [
431
+ { provide: IDEMPOTENCY_PLUGIN_OPTIONS, useValue: config },
432
+ { provide: IDEMPOTENCY_STORE, useClass: RedisIdempotencyStoreAdapter },
433
+ { provide: IDEMPOTENCY_SERVICE, useClass: IdempotencyService },
434
+ // Reflector is needed for @Idempotent decorator metadata
435
+ Reflector,
436
+ IdempotencyInterceptor
437
+ ];
438
+ }
439
+ getExports() {
440
+ return [IDEMPOTENCY_PLUGIN_OPTIONS, IDEMPOTENCY_SERVICE, IdempotencyInterceptor];
441
+ }
442
+ };
443
+
444
+ export { IDEMPOTENCY_PLUGIN_OPTIONS, IDEMPOTENCY_SERVICE, IDEMPOTENCY_STORE, IDEMPOTENT_OPTIONS, IdempotencyError, IdempotencyFailedError, IdempotencyFingerprintMismatchError, IdempotencyInterceptor, IdempotencyKeyRequiredError, IdempotencyPlugin, IdempotencyRecordNotFoundError, IdempotencyService, IdempotencyTimeoutError, Idempotent };
445
+ //# sourceMappingURL=index.mjs.map
446
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../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 * 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","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 { Reflector } from '@nestjs/core';\nimport { Observable, of } from 'rxjs';\nimport { tap } from 'rxjs/operators';\n\nimport { IDEMPOTENCY_SERVICE, IDEMPOTENCY_PLUGIN_OPTIONS } from '../../../shared/constants';\nimport { IdempotencyFingerprintMismatchError, IdempotencyFailedError } from '../../../shared/errors';\nimport { IIdempotencyPluginOptions, IIdempotencyRecord } from '../../../shared/types';\nimport { IIdempotencyService } from '../../application/ports/idempotency-service.port';\nimport { IDEMPOTENT_OPTIONS, IIdempotentOptions } from '../decorators/idempotent.decorator';\n\n/**\n * HTTP response interface for interceptor use.\n */\ninterface IHttpResponse {\n status(code: number): this;\n statusCode: number;\n setHeader(name: string, value: string): void;\n getHeader(name: string): string | number | string[] | undefined;\n}\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 ) {}\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(request.path);\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 hash(data: string): string {\n return createHash('sha256').update(data).digest('hex');\n }\n\n private replayResponse(response: IHttpResponse, record: IIdempotencyRecord): Observable<unknown> {\n response.status(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 response.setHeader(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: IHttpResponse, 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, REDIS_DRIVER } from '@nestjs-redisx/core';\n\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(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 { Provider } from '@nestjs/common';\nimport { Reflector } from '@nestjs/core';\nimport { IRedisXPlugin } from '@nestjs-redisx/core';\n\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_SERVICE, IDEMPOTENCY_STORE } from './shared/constants';\nimport { IIdempotencyPluginOptions } from './shared/types';\n\nconst DEFAULT_IDEMPOTENCY_CONFIG: Required<Omit<IIdempotencyPluginOptions, 'isGlobal' | '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 = '0.1.0';\n readonly description = 'Request deduplication with response replay for idempotent operations';\n\n constructor(private readonly options: IIdempotencyPluginOptions = {}) {}\n\n getProviders(): Provider[] {\n const config: IIdempotencyPluginOptions = {\n defaultTtl: this.options.defaultTtl ?? DEFAULT_IDEMPOTENCY_CONFIG.defaultTtl,\n keyPrefix: this.options.keyPrefix ?? DEFAULT_IDEMPOTENCY_CONFIG.keyPrefix,\n headerName: this.options.headerName ?? DEFAULT_IDEMPOTENCY_CONFIG.headerName,\n lockTimeout: this.options.lockTimeout ?? DEFAULT_IDEMPOTENCY_CONFIG.lockTimeout,\n waitTimeout: this.options.waitTimeout ?? DEFAULT_IDEMPOTENCY_CONFIG.waitTimeout,\n validateFingerprint: this.options.validateFingerprint ?? DEFAULT_IDEMPOTENCY_CONFIG.validateFingerprint,\n fingerprintFields: this.options.fingerprintFields ?? DEFAULT_IDEMPOTENCY_CONFIG.fingerprintFields,\n errorPolicy: this.options.errorPolicy ?? DEFAULT_IDEMPOTENCY_CONFIG.errorPolicy,\n fingerprintGenerator: this.options.fingerprintGenerator,\n };\n\n return [\n { provide: IDEMPOTENCY_PLUGIN_OPTIONS, useValue: config },\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"]}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Injection tokens for idempotency plugin.
3
+ */
4
+ export declare const IDEMPOTENCY_PLUGIN_OPTIONS: unique symbol;
5
+ export declare const IDEMPOTENCY_SERVICE: unique symbol;
6
+ export declare const IDEMPOTENCY_STORE: unique symbol;
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/shared/constants/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,eAAO,MAAM,0BAA0B,eAA2C,CAAC;AACnF,eAAO,MAAM,mBAAmB,eAAoC,CAAC;AACrE,eAAO,MAAM,iBAAiB,eAAkC,CAAC"}
@@ -0,0 +1,39 @@
1
+ import { RedisXError, ErrorCode } from '@nestjs-redisx/core';
2
+ /**
3
+ * Base class for all idempotency-related errors
4
+ */
5
+ export declare class IdempotencyError extends RedisXError {
6
+ readonly idempotencyKey: string;
7
+ constructor(message: string, code: ErrorCode, idempotencyKey: string, cause?: Error);
8
+ }
9
+ /**
10
+ * Thrown when Idempotency-Key header is required but not provided
11
+ */
12
+ export declare class IdempotencyKeyRequiredError extends IdempotencyError {
13
+ constructor();
14
+ }
15
+ /**
16
+ * Thrown when request fingerprint doesn't match the stored one
17
+ */
18
+ export declare class IdempotencyFingerprintMismatchError extends IdempotencyError {
19
+ constructor(key: string);
20
+ }
21
+ /**
22
+ * Thrown when timeout waiting for concurrent request to complete
23
+ */
24
+ export declare class IdempotencyTimeoutError extends IdempotencyError {
25
+ constructor(key: string);
26
+ }
27
+ /**
28
+ * Thrown when previous request with same key failed
29
+ */
30
+ export declare class IdempotencyFailedError extends IdempotencyError {
31
+ constructor(key: string, error?: string);
32
+ }
33
+ /**
34
+ * Thrown when idempotency record not found in Redis
35
+ */
36
+ export declare class IdempotencyRecordNotFoundError extends IdempotencyError {
37
+ constructor(key: string);
38
+ }
39
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/shared/errors/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAE7D;;GAEG;AACH,qBAAa,gBAAiB,SAAQ,WAAW;aAI7B,cAAc,EAAE,MAAM;gBAFtC,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,SAAS,EACC,cAAc,EAAE,MAAM,EACtC,KAAK,CAAC,EAAE,KAAK;CAIhB;AAED;;GAEG;AACH,qBAAa,2BAA4B,SAAQ,gBAAgB;;CAIhE;AAED;;GAEG;AACH,qBAAa,mCAAoC,SAAQ,gBAAgB;gBAC3D,GAAG,EAAE,MAAM;CAGxB;AAED;;GAEG;AACH,qBAAa,uBAAwB,SAAQ,gBAAgB;gBAC/C,GAAG,EAAE,MAAM;CAGxB;AAED;;GAEG;AACH,qBAAa,sBAAuB,SAAQ,gBAAgB;gBAC9C,GAAG,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM;CAGxC;AAED;;GAEG;AACH,qBAAa,8BAA+B,SAAQ,gBAAgB;gBACtD,GAAG,EAAE,MAAM;CAGxB"}
@@ -0,0 +1,79 @@
1
+ import { ExecutionContext } from '@nestjs/common';
2
+ /**
3
+ * Idempotency plugin configuration options
4
+ */
5
+ export interface IIdempotencyPluginOptions {
6
+ /**
7
+ * Make the module global.
8
+ * @default false
9
+ */
10
+ isGlobal?: boolean;
11
+ /** Default TTL for idempotency records in seconds. @default 86400 (24 hours) */
12
+ defaultTtl?: number;
13
+ /** Key prefix in Redis. @default 'idempotency:' */
14
+ keyPrefix?: string;
15
+ /** Header name for idempotency key. @default 'Idempotency-Key' */
16
+ headerName?: string;
17
+ /** Lock timeout while processing in ms. @default 30000 (30 seconds) */
18
+ lockTimeout?: number;
19
+ /** Timeout waiting for concurrent request in ms. @default 60000 (60 seconds) */
20
+ waitTimeout?: number;
21
+ /** Whether to validate request fingerprint. @default true */
22
+ validateFingerprint?: boolean;
23
+ /** Fields to include in fingerprint. @default ['method', 'path', 'body'] */
24
+ fingerprintFields?: ('method' | 'path' | 'body' | 'query' | 'headers')[];
25
+ /** Custom fingerprint generator */
26
+ fingerprintGenerator?: (context: ExecutionContext) => string | Promise<string>;
27
+ /** Error handling strategy. @default 'fail-closed' */
28
+ errorPolicy?: 'fail-open' | 'fail-closed';
29
+ }
30
+ /**
31
+ * Idempotency record stored in Redis
32
+ */
33
+ export interface IIdempotencyRecord {
34
+ /** Idempotency key */
35
+ key: string;
36
+ /** Request fingerprint hash */
37
+ fingerprint: string;
38
+ /** Current status */
39
+ status: 'processing' | 'completed' | 'failed';
40
+ /** HTTP status code */
41
+ statusCode?: number;
42
+ /** Response body (JSON string) */
43
+ response?: string;
44
+ /** Response headers to replay (JSON string) */
45
+ headers?: string;
46
+ /** When processing started (timestamp ms) */
47
+ startedAt: number;
48
+ /** When completed (timestamp ms) */
49
+ completedAt?: number;
50
+ /** Error message if failed */
51
+ error?: string;
52
+ }
53
+ /**
54
+ * Result of checking idempotency key
55
+ */
56
+ export interface IIdempotencyCheckResult {
57
+ /** Whether this is a new request */
58
+ isNew: boolean;
59
+ /** If duplicate, the stored record */
60
+ record?: IIdempotencyRecord;
61
+ /** If fingerprint mismatch */
62
+ fingerprintMismatch?: boolean;
63
+ }
64
+ /**
65
+ * Response data to store
66
+ */
67
+ export interface IIdempotencyResponse {
68
+ statusCode: number;
69
+ body: unknown;
70
+ headers?: Record<string, string>;
71
+ }
72
+ /**
73
+ * Options for idempotency operations
74
+ */
75
+ export interface IIdempotencyOptions {
76
+ ttl?: number;
77
+ lockTimeout?: number;
78
+ }
79
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/shared/types/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAElD;;GAEG;AACH,MAAM,WAAW,yBAAyB;IACxC;;;OAGG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IAEnB,gFAAgF;IAChF,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB,mDAAmD;IACnD,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB,kEAAkE;IAClE,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB,uEAAuE;IACvE,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB,gFAAgF;IAChF,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB,6DAA6D;IAC7D,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAE9B,4EAA4E;IAC5E,iBAAiB,CAAC,EAAE,CAAC,QAAQ,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,SAAS,CAAC,EAAE,CAAC;IAEzE,mCAAmC;IACnC,oBAAoB,CAAC,EAAE,CAAC,OAAO,EAAE,gBAAgB,KAAK,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAE/E,sDAAsD;IACtD,WAAW,CAAC,EAAE,WAAW,GAAG,aAAa,CAAC;CAC3C;AAED;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,sBAAsB;IACtB,GAAG,EAAE,MAAM,CAAC;IAEZ,+BAA+B;IAC/B,WAAW,EAAE,MAAM,CAAC;IAEpB,qBAAqB;IACrB,MAAM,EAAE,YAAY,GAAG,WAAW,GAAG,QAAQ,CAAC;IAE9C,uBAAuB;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB,kCAAkC;IAClC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB,+CAA+C;IAC/C,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB,6CAA6C;IAC7C,SAAS,EAAE,MAAM,CAAC;IAElB,oCAAoC;IACpC,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB,8BAA8B;IAC9B,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;GAEG;AACH,MAAM,WAAW,uBAAuB;IACtC,oCAAoC;IACpC,KAAK,EAAE,OAAO,CAAC;IAEf,sCAAsC;IACtC,MAAM,CAAC,EAAE,kBAAkB,CAAC;IAE5B,8BAA8B;IAC9B,mBAAmB,CAAC,EAAE,OAAO,CAAC;CAC/B;AAED;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClC;AAED;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB"}