@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.js ADDED
@@ -0,0 +1,459 @@
1
+ 'use strict';
2
+
3
+ var core = require('@nestjs/core');
4
+ var crypto = require('crypto');
5
+ var common = require('@nestjs/common');
6
+ var rxjs = require('rxjs');
7
+ var operators = require('rxjs/operators');
8
+ var core$1 = require('@nestjs-redisx/core');
9
+
10
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
11
+ var __decorateClass = (decorators, target, key, kind) => {
12
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
13
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
14
+ if (decorator = decorators[i])
15
+ result = (decorator(result)) || result;
16
+ return result;
17
+ };
18
+ var __decorateParam = (index, decorator) => (target, key) => decorator(target, key, index);
19
+
20
+ // src/shared/constants/index.ts
21
+ var IDEMPOTENCY_PLUGIN_OPTIONS = /* @__PURE__ */ Symbol.for("IDEMPOTENCY_PLUGIN_OPTIONS");
22
+ var IDEMPOTENCY_SERVICE = /* @__PURE__ */ Symbol.for("IDEMPOTENCY_SERVICE");
23
+ var IDEMPOTENCY_STORE = /* @__PURE__ */ Symbol.for("IDEMPOTENCY_STORE");
24
+ var IdempotencyError = class extends core$1.RedisXError {
25
+ constructor(message, code, idempotencyKey, cause) {
26
+ super(message, code, cause, { idempotencyKey });
27
+ this.idempotencyKey = idempotencyKey;
28
+ }
29
+ };
30
+ var IdempotencyKeyRequiredError = class extends IdempotencyError {
31
+ constructor() {
32
+ super("Idempotency-Key header is required", core$1.ErrorCode.IDEMPOTENCY_KEY_INVALID, "");
33
+ }
34
+ };
35
+ var IdempotencyFingerprintMismatchError = class extends IdempotencyError {
36
+ constructor(key) {
37
+ super(`Request body does not match previous request with idempotency key "${key}"`, core$1.ErrorCode.IDEMPOTENCY_KEY_INVALID, key);
38
+ }
39
+ };
40
+ var IdempotencyTimeoutError = class extends IdempotencyError {
41
+ constructor(key) {
42
+ super(`Timeout waiting for concurrent request with idempotency key "${key}"`, core$1.ErrorCode.OP_TIMEOUT, key);
43
+ }
44
+ };
45
+ var IdempotencyFailedError = class extends IdempotencyError {
46
+ constructor(key, error) {
47
+ super(`Previous request with idempotency key "${key}" failed${error ? `: ${error}` : ""}`, core$1.ErrorCode.IDEMPOTENCY_PREVIOUS_FAILED, key);
48
+ }
49
+ };
50
+ var IdempotencyRecordNotFoundError = class extends IdempotencyError {
51
+ constructor(key) {
52
+ super(`Idempotency record not found for key "${key}"`, core$1.ErrorCode.OP_KEY_NOT_FOUND, key);
53
+ }
54
+ };
55
+ var IDEMPOTENT_OPTIONS = /* @__PURE__ */ Symbol.for("IDEMPOTENT_OPTIONS");
56
+ function Idempotent(options = {}) {
57
+ return common.applyDecorators(common.SetMetadata(IDEMPOTENT_OPTIONS, options), common.UseInterceptors(exports.IdempotencyInterceptor));
58
+ }
59
+
60
+ // src/idempotency/api/interceptors/idempotency.interceptor.ts
61
+ exports.IdempotencyInterceptor = class IdempotencyInterceptor {
62
+ constructor(idempotencyService, config, reflector) {
63
+ this.idempotencyService = idempotencyService;
64
+ this.config = config;
65
+ this.reflector = reflector;
66
+ }
67
+ async intercept(context, next) {
68
+ const options = this.getOptions(context);
69
+ const response = context.switchToHttp().getResponse();
70
+ const key = await this.extractKey(context, options);
71
+ if (!key) {
72
+ return next.handle();
73
+ }
74
+ if (options.skip && await options.skip(context)) {
75
+ return next.handle();
76
+ }
77
+ const fingerprint = await this.generateFingerprint(context, options);
78
+ const checkResult = await this.idempotencyService.checkAndLock(key, fingerprint, {
79
+ ttl: options.ttl
80
+ });
81
+ if (!checkResult.isNew) {
82
+ if (checkResult.fingerprintMismatch) {
83
+ throw new IdempotencyFingerprintMismatchError(key);
84
+ }
85
+ const record = checkResult.record;
86
+ if (record.status === "failed") {
87
+ throw new IdempotencyFailedError(key, record.error);
88
+ }
89
+ return this.replayResponse(response, record);
90
+ }
91
+ return next.handle().pipe(
92
+ operators.tap({
93
+ next: (data) => {
94
+ void this.idempotencyService.complete(
95
+ key,
96
+ {
97
+ statusCode: response.statusCode,
98
+ body: data,
99
+ headers: this.extractHeaders(response, options)
100
+ },
101
+ { ttl: options.ttl }
102
+ );
103
+ },
104
+ error: (error) => {
105
+ void this.idempotencyService.fail(key, error.message);
106
+ }
107
+ })
108
+ );
109
+ }
110
+ getOptions(context) {
111
+ return this.reflector.get(IDEMPOTENT_OPTIONS, context.getHandler()) ?? {};
112
+ }
113
+ async extractKey(context, options) {
114
+ if (options.keyExtractor) {
115
+ return options.keyExtractor(context);
116
+ }
117
+ const request = context.switchToHttp().getRequest();
118
+ const headerName = this.config.headerName ?? "Idempotency-Key";
119
+ return request.headers[headerName.toLowerCase()] ?? null;
120
+ }
121
+ async generateFingerprint(context, options) {
122
+ if (this.config.fingerprintGenerator) {
123
+ return this.config.fingerprintGenerator(context);
124
+ }
125
+ const request = context.switchToHttp().getRequest();
126
+ const fields = options.fingerprintFields ?? this.config.fingerprintFields ?? ["method", "path", "body"];
127
+ const parts = [];
128
+ if (fields.includes("method")) parts.push(request.method);
129
+ if (fields.includes("path")) parts.push(request.path);
130
+ if (fields.includes("body")) parts.push(JSON.stringify(request.body ?? {}));
131
+ if (fields.includes("query")) parts.push(JSON.stringify(request.query ?? {}));
132
+ const data = parts.join("|");
133
+ return this.hash(data);
134
+ }
135
+ hash(data) {
136
+ return crypto.createHash("sha256").update(data).digest("hex");
137
+ }
138
+ replayResponse(response, record) {
139
+ response.status(record.statusCode ?? 200);
140
+ if (record.headers) {
141
+ const headers = JSON.parse(record.headers);
142
+ for (const [key, value] of Object.entries(headers)) {
143
+ response.setHeader(key, value);
144
+ }
145
+ }
146
+ const body = record.response ? JSON.parse(record.response) : null;
147
+ return rxjs.of(body);
148
+ }
149
+ extractHeaders(response, options) {
150
+ if (!options.cacheHeaders?.length) return void 0;
151
+ const headers = {};
152
+ for (const name of options.cacheHeaders) {
153
+ const value = response.getHeader(name);
154
+ if (value) headers[name] = String(value);
155
+ }
156
+ return Object.keys(headers).length > 0 ? headers : void 0;
157
+ }
158
+ };
159
+ exports.IdempotencyInterceptor = __decorateClass([
160
+ common.Injectable(),
161
+ __decorateParam(0, common.Inject(IDEMPOTENCY_SERVICE)),
162
+ __decorateParam(1, common.Inject(IDEMPOTENCY_PLUGIN_OPTIONS)),
163
+ __decorateParam(2, common.Inject(core.Reflector))
164
+ ], exports.IdempotencyInterceptor);
165
+ var POLL_INTERVAL_MS = 100;
166
+ var METRICS_SERVICE = /* @__PURE__ */ Symbol.for("METRICS_SERVICE");
167
+ exports.IdempotencyService = class IdempotencyService {
168
+ constructor(config, store, metrics) {
169
+ this.config = config;
170
+ this.store = store;
171
+ this.metrics = metrics;
172
+ }
173
+ async checkAndLock(key, fingerprint, options = {}) {
174
+ const startTime = Date.now();
175
+ const fullKey = this.buildKey(key);
176
+ const lockTimeout = options.lockTimeout ?? this.config.lockTimeout ?? 3e4;
177
+ const result = await this.store.checkAndLock(fullKey, fingerprint, lockTimeout);
178
+ if (result.status === "new") {
179
+ this.metrics?.incrementCounter("redisx_idempotency_requests_total", { status: "new" });
180
+ this.recordDuration(startTime);
181
+ return { isNew: true };
182
+ }
183
+ if (result.status === "fingerprint_mismatch") {
184
+ this.metrics?.incrementCounter("redisx_idempotency_requests_total", { status: "mismatch" });
185
+ this.recordDuration(startTime);
186
+ return { isNew: false, fingerprintMismatch: true };
187
+ }
188
+ if (result.status === "processing") {
189
+ const record = await this.waitForCompletion(fullKey);
190
+ this.metrics?.incrementCounter("redisx_idempotency_requests_total", { status: "replay" });
191
+ this.recordDuration(startTime);
192
+ return { isNew: false, record };
193
+ }
194
+ this.metrics?.incrementCounter("redisx_idempotency_requests_total", { status: "replay" });
195
+ this.recordDuration(startTime);
196
+ return { isNew: false, record: result.record };
197
+ }
198
+ recordDuration(startTime) {
199
+ const duration = (Date.now() - startTime) / 1e3;
200
+ this.metrics?.observeHistogram("redisx_idempotency_duration_seconds", duration);
201
+ }
202
+ async complete(key, response, options = {}) {
203
+ const fullKey = this.buildKey(key);
204
+ const ttl = options.ttl ?? this.config.defaultTtl ?? 86400;
205
+ await this.store.complete(
206
+ fullKey,
207
+ {
208
+ statusCode: response.statusCode,
209
+ response: JSON.stringify(response.body),
210
+ headers: response.headers ? JSON.stringify(response.headers) : void 0,
211
+ completedAt: Date.now()
212
+ },
213
+ ttl
214
+ );
215
+ }
216
+ async fail(key, error) {
217
+ const fullKey = this.buildKey(key);
218
+ await this.store.fail(fullKey, error);
219
+ }
220
+ async get(key) {
221
+ const fullKey = this.buildKey(key);
222
+ return this.store.get(fullKey);
223
+ }
224
+ async delete(key) {
225
+ const fullKey = this.buildKey(key);
226
+ return this.store.delete(fullKey);
227
+ }
228
+ async waitForCompletion(key) {
229
+ const waitTimeout = this.config.waitTimeout ?? 6e4;
230
+ const startTime = Date.now();
231
+ while (Date.now() - startTime < waitTimeout) {
232
+ const record = await this.store.get(key);
233
+ if (!record) {
234
+ throw new IdempotencyRecordNotFoundError(key);
235
+ }
236
+ if (record.status === "completed" || record.status === "failed") {
237
+ return record;
238
+ }
239
+ await this.sleep(POLL_INTERVAL_MS);
240
+ }
241
+ throw new IdempotencyTimeoutError(key);
242
+ }
243
+ buildKey(key) {
244
+ const prefix = this.config.keyPrefix ?? "idempotency:";
245
+ return `${prefix}${key}`;
246
+ }
247
+ sleep(ms) {
248
+ return new Promise((resolve) => setTimeout(resolve, ms));
249
+ }
250
+ };
251
+ exports.IdempotencyService = __decorateClass([
252
+ common.Injectable(),
253
+ __decorateParam(0, common.Inject(IDEMPOTENCY_PLUGIN_OPTIONS)),
254
+ __decorateParam(1, common.Inject(IDEMPOTENCY_STORE)),
255
+ __decorateParam(2, common.Optional()),
256
+ __decorateParam(2, common.Inject(METRICS_SERVICE))
257
+ ], exports.IdempotencyService);
258
+
259
+ // src/idempotency/infrastructure/scripts/lua-scripts.ts
260
+ var CHECK_AND_LOCK_SCRIPT = `
261
+ local key = KEYS[1]
262
+ local fingerprint = ARGV[1]
263
+ local lock_timeout = tonumber(ARGV[2])
264
+ local now = tonumber(ARGV[3])
265
+
266
+ -- Check if key exists
267
+ local existing = redis.call('HGETALL', key)
268
+
269
+ if #existing == 0 then
270
+ -- New request - create lock
271
+ redis.call('HMSET', key,
272
+ 'fingerprint', fingerprint,
273
+ 'status', 'processing',
274
+ 'startedAt', now
275
+ )
276
+ redis.call('PEXPIRE', key, lock_timeout)
277
+ return {'new'}
278
+ end
279
+
280
+ -- Convert to table
281
+ local record = {}
282
+ for i = 1, #existing, 2 do
283
+ record[existing[i]] = existing[i + 1]
284
+ end
285
+
286
+ -- Check fingerprint
287
+ if record.fingerprint ~= fingerprint then
288
+ return {'fingerprint_mismatch'}
289
+ end
290
+
291
+ -- Check status
292
+ if record.status == 'processing' then
293
+ -- Check if lock expired (stale)
294
+ local started = tonumber(record.startedAt)
295
+ if now - started > lock_timeout then
296
+ -- Stale lock - take over
297
+ redis.call('HMSET', key,
298
+ 'status', 'processing',
299
+ 'startedAt', now
300
+ )
301
+ redis.call('PEXPIRE', key, lock_timeout)
302
+ return {'new'}
303
+ end
304
+ return {'processing'}
305
+ end
306
+
307
+ -- Completed or failed - return record
308
+ return {
309
+ record.status,
310
+ record.statusCode or '',
311
+ record.response or '',
312
+ record.headers or '',
313
+ record.error or ''
314
+ }
315
+ `.trim();
316
+
317
+ // src/idempotency/infrastructure/adapters/redis-idempotency-store.adapter.ts
318
+ var RedisIdempotencyStoreAdapter = class {
319
+ constructor(driver) {
320
+ this.driver = driver;
321
+ }
322
+ checkAndLockSha = null;
323
+ /**
324
+ * Pre-load Lua script on module initialization
325
+ */
326
+ async onModuleInit() {
327
+ this.checkAndLockSha = await this.driver.scriptLoad(CHECK_AND_LOCK_SCRIPT);
328
+ }
329
+ async checkAndLock(key, fingerprint, lockTimeoutMs) {
330
+ const now = Date.now();
331
+ const rawResult = await this.driver.evalsha(this.checkAndLockSha, [key], [fingerprint, lockTimeoutMs, now]);
332
+ const result = rawResult.map((v) => v === null || v === void 0 ? "" : String(v));
333
+ const status = result[0];
334
+ if (status === "new") {
335
+ return { status: "new" };
336
+ }
337
+ if (status === "fingerprint_mismatch") {
338
+ return { status: "fingerprint_mismatch" };
339
+ }
340
+ if (status === "processing") {
341
+ return { status: "processing" };
342
+ }
343
+ return {
344
+ status,
345
+ record: {
346
+ key,
347
+ fingerprint,
348
+ status,
349
+ statusCode: result[1] ? parseInt(result[1], 10) : void 0,
350
+ response: result[2] || void 0,
351
+ headers: result[3] || void 0,
352
+ error: result[4] || void 0,
353
+ startedAt: 0
354
+ // Not returned from Lua
355
+ }
356
+ };
357
+ }
358
+ async complete(key, data, ttlSeconds) {
359
+ await this.driver.hmset(key, {
360
+ status: "completed",
361
+ statusCode: String(data.statusCode),
362
+ response: data.response,
363
+ headers: data.headers || "",
364
+ completedAt: String(data.completedAt)
365
+ });
366
+ await this.driver.expire(key, ttlSeconds);
367
+ }
368
+ async fail(key, error) {
369
+ await this.driver.hmset(key, {
370
+ status: "failed",
371
+ error,
372
+ completedAt: String(Date.now())
373
+ });
374
+ }
375
+ async get(key) {
376
+ const data = await this.driver.hgetall(key);
377
+ if (!data || Object.keys(data).length === 0) {
378
+ return null;
379
+ }
380
+ return {
381
+ key,
382
+ fingerprint: data.fingerprint,
383
+ status: data.status,
384
+ statusCode: data.statusCode ? parseInt(data.statusCode, 10) : void 0,
385
+ response: data.response || void 0,
386
+ headers: data.headers || void 0,
387
+ startedAt: parseInt(data.startedAt, 10),
388
+ completedAt: data.completedAt ? parseInt(data.completedAt, 10) : void 0,
389
+ error: data.error || void 0
390
+ };
391
+ }
392
+ async delete(key) {
393
+ const result = await this.driver.del(key);
394
+ return result > 0;
395
+ }
396
+ };
397
+ RedisIdempotencyStoreAdapter = __decorateClass([
398
+ common.Injectable(),
399
+ __decorateParam(0, common.Inject(core$1.REDIS_DRIVER))
400
+ ], RedisIdempotencyStoreAdapter);
401
+
402
+ // src/idempotency.plugin.ts
403
+ var DEFAULT_IDEMPOTENCY_CONFIG = {
404
+ defaultTtl: 86400,
405
+ keyPrefix: "idempotency:",
406
+ headerName: "Idempotency-Key",
407
+ lockTimeout: 3e4,
408
+ waitTimeout: 6e4,
409
+ validateFingerprint: true,
410
+ fingerprintFields: ["method", "path", "body"],
411
+ errorPolicy: "fail-closed"
412
+ };
413
+ var IdempotencyPlugin = class {
414
+ constructor(options = {}) {
415
+ this.options = options;
416
+ }
417
+ name = "idempotency";
418
+ version = "0.1.0";
419
+ description = "Request deduplication with response replay for idempotent operations";
420
+ getProviders() {
421
+ const config = {
422
+ defaultTtl: this.options.defaultTtl ?? DEFAULT_IDEMPOTENCY_CONFIG.defaultTtl,
423
+ keyPrefix: this.options.keyPrefix ?? DEFAULT_IDEMPOTENCY_CONFIG.keyPrefix,
424
+ headerName: this.options.headerName ?? DEFAULT_IDEMPOTENCY_CONFIG.headerName,
425
+ lockTimeout: this.options.lockTimeout ?? DEFAULT_IDEMPOTENCY_CONFIG.lockTimeout,
426
+ waitTimeout: this.options.waitTimeout ?? DEFAULT_IDEMPOTENCY_CONFIG.waitTimeout,
427
+ validateFingerprint: this.options.validateFingerprint ?? DEFAULT_IDEMPOTENCY_CONFIG.validateFingerprint,
428
+ fingerprintFields: this.options.fingerprintFields ?? DEFAULT_IDEMPOTENCY_CONFIG.fingerprintFields,
429
+ errorPolicy: this.options.errorPolicy ?? DEFAULT_IDEMPOTENCY_CONFIG.errorPolicy,
430
+ fingerprintGenerator: this.options.fingerprintGenerator
431
+ };
432
+ return [
433
+ { provide: IDEMPOTENCY_PLUGIN_OPTIONS, useValue: config },
434
+ { provide: IDEMPOTENCY_STORE, useClass: RedisIdempotencyStoreAdapter },
435
+ { provide: IDEMPOTENCY_SERVICE, useClass: exports.IdempotencyService },
436
+ // Reflector is needed for @Idempotent decorator metadata
437
+ core.Reflector,
438
+ exports.IdempotencyInterceptor
439
+ ];
440
+ }
441
+ getExports() {
442
+ return [IDEMPOTENCY_PLUGIN_OPTIONS, IDEMPOTENCY_SERVICE, exports.IdempotencyInterceptor];
443
+ }
444
+ };
445
+
446
+ exports.IDEMPOTENCY_PLUGIN_OPTIONS = IDEMPOTENCY_PLUGIN_OPTIONS;
447
+ exports.IDEMPOTENCY_SERVICE = IDEMPOTENCY_SERVICE;
448
+ exports.IDEMPOTENCY_STORE = IDEMPOTENCY_STORE;
449
+ exports.IDEMPOTENT_OPTIONS = IDEMPOTENT_OPTIONS;
450
+ exports.IdempotencyError = IdempotencyError;
451
+ exports.IdempotencyFailedError = IdempotencyFailedError;
452
+ exports.IdempotencyFingerprintMismatchError = IdempotencyFingerprintMismatchError;
453
+ exports.IdempotencyKeyRequiredError = IdempotencyKeyRequiredError;
454
+ exports.IdempotencyPlugin = IdempotencyPlugin;
455
+ exports.IdempotencyRecordNotFoundError = IdempotencyRecordNotFoundError;
456
+ exports.IdempotencyTimeoutError = IdempotencyTimeoutError;
457
+ exports.Idempotent = Idempotent;
458
+ //# sourceMappingURL=index.js.map
459
+ //# sourceMappingURL=index.js.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":["RedisXError","ErrorCode","applyDecorators","SetMetadata","UseInterceptors","IdempotencyInterceptor","tap","createHash","of","Injectable","Reflector","IdempotencyService","Inject","Optional","REDIS_DRIVER"],"mappings":"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","file":"index.js","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"]}