@signaltree/events 7.3.6 → 7.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/dist/angular/handlers.cjs +38 -0
  2. package/dist/angular/handlers.js +35 -0
  3. package/dist/angular/index.cjs +15 -0
  4. package/dist/angular/index.js +3 -0
  5. package/dist/angular/optimistic-updates.cjs +161 -0
  6. package/dist/angular/optimistic-updates.js +159 -0
  7. package/{angular.cjs.js → dist/angular/websocket.service.cjs} +0 -194
  8. package/{angular.esm.js → dist/angular/websocket.service.js} +1 -191
  9. package/dist/core/error-classification.cjs +282 -0
  10. package/dist/core/error-classification.js +276 -0
  11. package/{factory.cjs.js → dist/core/factory.cjs} +3 -40
  12. package/{factory.esm.js → dist/core/factory.js} +2 -37
  13. package/dist/core/idempotency.cjs +252 -0
  14. package/dist/core/idempotency.js +247 -0
  15. package/dist/core/registry.cjs +183 -0
  16. package/dist/core/registry.js +180 -0
  17. package/dist/core/types.cjs +41 -0
  18. package/dist/core/types.js +38 -0
  19. package/{index.cjs.js → dist/core/validation.cjs} +1 -23
  20. package/{index.esm.js → dist/core/validation.js} +1 -4
  21. package/dist/index.cjs +43 -0
  22. package/dist/index.js +7 -0
  23. package/dist/nestjs/base.subscriber.cjs +287 -0
  24. package/dist/nestjs/base.subscriber.js +287 -0
  25. package/dist/nestjs/decorators.cjs +35 -0
  26. package/dist/nestjs/decorators.js +32 -0
  27. package/dist/nestjs/dlq.service.cjs +249 -0
  28. package/dist/nestjs/dlq.service.js +249 -0
  29. package/dist/nestjs/event-bus.module.cjs +152 -0
  30. package/dist/nestjs/event-bus.module.js +152 -0
  31. package/dist/nestjs/event-bus.service.cjs +243 -0
  32. package/dist/nestjs/event-bus.service.js +243 -0
  33. package/dist/nestjs/index.cjs +33 -0
  34. package/dist/nestjs/index.js +6 -0
  35. package/dist/nestjs/tokens.cjs +14 -0
  36. package/dist/nestjs/tokens.js +9 -0
  37. package/dist/testing/assertions.cjs +172 -0
  38. package/dist/testing/assertions.js +169 -0
  39. package/dist/testing/factories.cjs +122 -0
  40. package/dist/testing/factories.js +119 -0
  41. package/dist/testing/helpers.cjs +233 -0
  42. package/dist/testing/helpers.js +227 -0
  43. package/dist/testing/index.cjs +20 -0
  44. package/dist/testing/index.js +4 -0
  45. package/dist/testing/mock-event-bus.cjs +237 -0
  46. package/dist/testing/mock-event-bus.js +234 -0
  47. package/package.json +35 -23
  48. package/angular.d.ts +0 -1
  49. package/idempotency.cjs.js +0 -713
  50. package/idempotency.esm.js +0 -701
  51. package/index.d.ts +0 -1
  52. package/nestjs.cjs.js +0 -951
  53. package/nestjs.d.ts +0 -1
  54. package/nestjs.esm.js +0 -944
  55. package/testing.cjs.js +0 -755
  56. package/testing.d.ts +0 -1
  57. package/testing.esm.js +0 -743
@@ -0,0 +1,276 @@
1
+ /**
2
+ * Error Classification - Determine retry behavior for errors
3
+ *
4
+ * Provides:
5
+ * - Retryable vs non-retryable error classification
6
+ * - Error categories (transient, permanent, poison)
7
+ * - Retry configuration per error type
8
+ * - Custom error classifiers
9
+ */
10
+ /**
11
+ * Default retry configurations by classification
12
+ */
13
+ const DEFAULT_RETRY_CONFIGS = {
14
+ transient: {
15
+ maxAttempts: 5,
16
+ initialDelayMs: 1000,
17
+ maxDelayMs: 60000,
18
+ backoffMultiplier: 2,
19
+ jitter: 0.1
20
+ },
21
+ permanent: {
22
+ maxAttempts: 0,
23
+ initialDelayMs: 0,
24
+ maxDelayMs: 0,
25
+ backoffMultiplier: 1,
26
+ jitter: 0
27
+ },
28
+ poison: {
29
+ maxAttempts: 0,
30
+ initialDelayMs: 0,
31
+ maxDelayMs: 0,
32
+ backoffMultiplier: 1,
33
+ jitter: 0
34
+ },
35
+ unknown: {
36
+ maxAttempts: 3,
37
+ initialDelayMs: 2000,
38
+ maxDelayMs: 30000,
39
+ backoffMultiplier: 2,
40
+ jitter: 0.2
41
+ }
42
+ };
43
+ /**
44
+ * Known transient error patterns
45
+ */
46
+ const TRANSIENT_ERROR_PATTERNS = [
47
+ // Network errors
48
+ /ECONNREFUSED/i, /ECONNRESET/i, /ETIMEDOUT/i, /ENETUNREACH/i, /EHOSTUNREACH/i, /ENOTFOUND/i, /socket hang up/i, /network error/i, /connection.*timeout/i, /request.*timeout/i,
49
+ // Database transient
50
+ /deadlock/i, /lock wait timeout/i, /too many connections/i, /connection pool exhausted/i, /temporarily unavailable/i,
51
+ // HTTP transient
52
+ /502 bad gateway/i, /503 service unavailable/i, /504 gateway timeout/i, /429 too many requests/i,
53
+ // Redis/Queue transient
54
+ /BUSY/i, /LOADING/i, /CLUSTERDOWN/i, /READONLY/i,
55
+ // Generic transient
56
+ /temporary failure/i, /try again/i, /retry/i, /throttl/i, /rate limit/i, /circuit breaker/i];
57
+ /**
58
+ * Known permanent error patterns
59
+ */
60
+ const PERMANENT_ERROR_PATTERNS = [
61
+ // Auth errors
62
+ /unauthorized/i, /forbidden/i, /access denied/i, /permission denied/i, /invalid token/i, /token expired/i,
63
+ // Business logic
64
+ /not found/i, /already exists/i, /duplicate/i, /conflict/i, /invalid state/i, /precondition failed/i,
65
+ // HTTP permanent
66
+ /400 bad request/i, /401 unauthorized/i, /403 forbidden/i, /404 not found/i, /409 conflict/i, /422 unprocessable/i];
67
+ /**
68
+ * Known poison error patterns (send to DLQ immediately)
69
+ */
70
+ const POISON_ERROR_PATTERNS = [
71
+ // Schema/Serialization
72
+ /invalid json/i, /json parse error/i, /unexpected token/i, /schema validation/i, /invalid event schema/i, /deserialization/i, /malformed/i,
73
+ // Data corruption
74
+ /data corruption/i, /checksum mismatch/i, /integrity error/i];
75
+ /**
76
+ * Error codes that indicate transient failures
77
+ */
78
+ const TRANSIENT_ERROR_CODES = new Set(['ECONNREFUSED', 'ECONNRESET', 'ETIMEDOUT', 'ENETUNREACH', 'EHOSTUNREACH', 'ENOTFOUND', 'EPIPE', 'EAI_AGAIN']);
79
+ /**
80
+ * HTTP status codes that indicate transient failures
81
+ */
82
+ const TRANSIENT_HTTP_STATUS = new Set([408, 429, 500, 502, 503, 504]);
83
+ /**
84
+ * HTTP status codes that indicate permanent failures
85
+ */
86
+ const PERMANENT_HTTP_STATUS = new Set([400, 401, 403, 404, 405, 409, 410, 422]);
87
+ /**
88
+ * Create an error classifier
89
+ *
90
+ * @example
91
+ * ```typescript
92
+ * const classifier = createErrorClassifier({
93
+ * customClassifiers: [
94
+ * (error) => {
95
+ * if (error instanceof MyCustomTransientError) return 'transient';
96
+ * return null; // Let default classification handle it
97
+ * }
98
+ * ],
99
+ * retryConfigs: {
100
+ * transient: { maxAttempts: 10 }, // Override max attempts
101
+ * },
102
+ * });
103
+ *
104
+ * const result = classifier.classify(error);
105
+ * if (result.sendToDlq) {
106
+ * await dlqService.send(event, error, result.reason);
107
+ * }
108
+ * ```
109
+ */
110
+ function createErrorClassifier(config = {}) {
111
+ const customClassifiers = config.customClassifiers ?? [];
112
+ const defaultClassification = config.defaultClassification ?? 'unknown';
113
+ // Merge retry configs
114
+ const retryConfigs = {
115
+ transient: {
116
+ ...DEFAULT_RETRY_CONFIGS.transient,
117
+ ...config.retryConfigs?.transient
118
+ },
119
+ permanent: {
120
+ ...DEFAULT_RETRY_CONFIGS.permanent,
121
+ ...config.retryConfigs?.permanent
122
+ },
123
+ poison: {
124
+ ...DEFAULT_RETRY_CONFIGS.poison,
125
+ ...config.retryConfigs?.poison
126
+ },
127
+ unknown: {
128
+ ...DEFAULT_RETRY_CONFIGS.unknown,
129
+ ...config.retryConfigs?.unknown
130
+ }
131
+ };
132
+ function extractErrorInfo(error) {
133
+ if (error instanceof Error) {
134
+ const errWithCode = error;
135
+ return {
136
+ message: error.message,
137
+ name: error.name,
138
+ code: errWithCode.code,
139
+ status: errWithCode.status ?? errWithCode.statusCode ?? errWithCode.response?.status
140
+ };
141
+ }
142
+ if (typeof error === 'object' && error !== null) {
143
+ const obj = error;
144
+ return {
145
+ message: String(obj['message'] ?? obj['error'] ?? ''),
146
+ code: obj['code'],
147
+ status: obj['status'] ?? obj['statusCode']
148
+ };
149
+ }
150
+ return {
151
+ message: String(error)
152
+ };
153
+ }
154
+ function classifyByPatterns(message) {
155
+ // Check poison patterns first (most specific)
156
+ for (const pattern of POISON_ERROR_PATTERNS) {
157
+ if (pattern.test(message)) {
158
+ return 'poison';
159
+ }
160
+ }
161
+ // Check permanent patterns
162
+ for (const pattern of PERMANENT_ERROR_PATTERNS) {
163
+ if (pattern.test(message)) {
164
+ return 'permanent';
165
+ }
166
+ }
167
+ // Check transient patterns
168
+ for (const pattern of TRANSIENT_ERROR_PATTERNS) {
169
+ if (pattern.test(message)) {
170
+ return 'transient';
171
+ }
172
+ }
173
+ return null;
174
+ }
175
+ function classify(error) {
176
+ // 1. Try custom classifiers first
177
+ for (const classifier of customClassifiers) {
178
+ const result = classifier(error);
179
+ if (result !== null) {
180
+ return {
181
+ classification: result,
182
+ retryConfig: retryConfigs[result],
183
+ sendToDlq: result === 'poison' || result === 'permanent',
184
+ reason: `Custom classifier: ${result}`
185
+ };
186
+ }
187
+ }
188
+ const {
189
+ message,
190
+ code,
191
+ status,
192
+ name
193
+ } = extractErrorInfo(error);
194
+ // 2. Check error code
195
+ if (code && TRANSIENT_ERROR_CODES.has(code)) {
196
+ return {
197
+ classification: 'transient',
198
+ retryConfig: retryConfigs.transient,
199
+ sendToDlq: false,
200
+ reason: `Error code: ${code}`
201
+ };
202
+ }
203
+ // 3. Check HTTP status
204
+ if (status !== undefined) {
205
+ if (TRANSIENT_HTTP_STATUS.has(status)) {
206
+ return {
207
+ classification: 'transient',
208
+ retryConfig: retryConfigs.transient,
209
+ sendToDlq: false,
210
+ reason: `HTTP status: ${status}`
211
+ };
212
+ }
213
+ if (PERMANENT_HTTP_STATUS.has(status)) {
214
+ return {
215
+ classification: 'permanent',
216
+ retryConfig: retryConfigs.permanent,
217
+ sendToDlq: true,
218
+ reason: `HTTP status: ${status}`
219
+ };
220
+ }
221
+ }
222
+ // 4. Check error patterns
223
+ const patternResult = classifyByPatterns(message) ?? classifyByPatterns(name ?? '');
224
+ if (patternResult) {
225
+ return {
226
+ classification: patternResult,
227
+ retryConfig: retryConfigs[patternResult],
228
+ sendToDlq: patternResult === 'poison' || patternResult === 'permanent',
229
+ reason: `Pattern match: ${message.slice(0, 50)}`
230
+ };
231
+ }
232
+ // 5. Use default classification
233
+ return {
234
+ classification: defaultClassification,
235
+ retryConfig: retryConfigs[defaultClassification],
236
+ sendToDlq: defaultClassification === 'poison' || defaultClassification === 'permanent',
237
+ reason: 'No matching pattern, using default'
238
+ };
239
+ }
240
+ function isRetryable(error) {
241
+ const result = classify(error);
242
+ return result.classification === 'transient' || result.classification === 'unknown';
243
+ }
244
+ function calculateDelay(attempt, retryConfig) {
245
+ // Exponential backoff: initialDelay * multiplier^attempt
246
+ const baseDelay = retryConfig.initialDelayMs * Math.pow(retryConfig.backoffMultiplier, attempt);
247
+ // Cap at maxDelay
248
+ const cappedDelay = Math.min(baseDelay, retryConfig.maxDelayMs);
249
+ // Add jitter to prevent thundering herd
250
+ const jitter = cappedDelay * retryConfig.jitter * Math.random();
251
+ return Math.round(cappedDelay + jitter);
252
+ }
253
+ return {
254
+ classify,
255
+ isRetryable,
256
+ calculateDelay
257
+ };
258
+ }
259
+ /**
260
+ * Pre-configured error classifier instance
261
+ */
262
+ const defaultErrorClassifier = createErrorClassifier();
263
+ /**
264
+ * Quick helper to check if error is retryable
265
+ */
266
+ function isRetryableError(error) {
267
+ return defaultErrorClassifier.isRetryable(error);
268
+ }
269
+ /**
270
+ * Quick helper to classify error
271
+ */
272
+ function classifyError(error) {
273
+ return defaultErrorClassifier.classify(error);
274
+ }
275
+
276
+ export { DEFAULT_RETRY_CONFIGS, classifyError, createErrorClassifier, defaultErrorClassifier, isRetryableError };
@@ -1,41 +1,6 @@
1
1
  'use strict';
2
2
 
3
- /**
4
- * Core event types - framework-agnostic definitions
5
- */
6
- /**
7
- * Priority configuration with SLA targets
8
- */
9
- const EVENT_PRIORITIES = {
10
- critical: {
11
- sla: 100,
12
- weight: 10
13
- },
14
- // < 100ms
15
- high: {
16
- sla: 500,
17
- weight: 7
18
- },
19
- // < 500ms
20
- normal: {
21
- sla: 2000,
22
- weight: 5
23
- },
24
- // < 2s
25
- low: {
26
- sla: 30000,
27
- weight: 3
28
- },
29
- // < 30s
30
- bulk: {
31
- sla: 300000,
32
- weight: 1
33
- } // < 5min
34
- };
35
- const DEFAULT_EVENT_VERSION = {
36
- major: 1,
37
- minor: 0
38
- };
3
+ var types = require('./types.cjs');
39
4
 
40
5
  /**
41
6
  * Generate a UUID v7 (time-sortable)
@@ -92,7 +57,7 @@ function createEvent(type, data, options) {
92
57
  return {
93
58
  id,
94
59
  type,
95
- version: options.version ?? DEFAULT_EVENT_VERSION,
60
+ version: options.version ?? types.DEFAULT_EVENT_VERSION,
96
61
  timestamp,
97
62
  correlationId,
98
63
  causationId: options.causationId,
@@ -147,7 +112,7 @@ function createEventFactory(config) {
147
112
  return {
148
113
  id,
149
114
  type,
150
- version: options.version ?? DEFAULT_EVENT_VERSION,
115
+ version: options.version ?? types.DEFAULT_EVENT_VERSION,
151
116
  timestamp,
152
117
  correlationId,
153
118
  causationId: options.causationId,
@@ -177,8 +142,6 @@ function createEventFactory(config) {
177
142
  };
178
143
  }
179
144
 
180
- exports.DEFAULT_EVENT_VERSION = DEFAULT_EVENT_VERSION;
181
- exports.EVENT_PRIORITIES = EVENT_PRIORITIES;
182
145
  exports.createEvent = createEvent;
183
146
  exports.createEventFactory = createEventFactory;
184
147
  exports.generateCorrelationId = generateCorrelationId;
@@ -1,39 +1,4 @@
1
- /**
2
- * Core event types - framework-agnostic definitions
3
- */
4
- /**
5
- * Priority configuration with SLA targets
6
- */
7
- const EVENT_PRIORITIES = {
8
- critical: {
9
- sla: 100,
10
- weight: 10
11
- },
12
- // < 100ms
13
- high: {
14
- sla: 500,
15
- weight: 7
16
- },
17
- // < 500ms
18
- normal: {
19
- sla: 2000,
20
- weight: 5
21
- },
22
- // < 2s
23
- low: {
24
- sla: 30000,
25
- weight: 3
26
- },
27
- // < 30s
28
- bulk: {
29
- sla: 300000,
30
- weight: 1
31
- } // < 5min
32
- };
33
- const DEFAULT_EVENT_VERSION = {
34
- major: 1,
35
- minor: 0
36
- };
1
+ import { DEFAULT_EVENT_VERSION } from './types.js';
37
2
 
38
3
  /**
39
4
  * Generate a UUID v7 (time-sortable)
@@ -175,4 +140,4 @@ function createEventFactory(config) {
175
140
  };
176
141
  }
177
142
 
178
- export { DEFAULT_EVENT_VERSION as D, EVENT_PRIORITIES as E, createEventFactory as a, generateCorrelationId as b, createEvent as c, generateEventId as g };
143
+ export { createEvent, createEventFactory, generateCorrelationId, generateEventId };
@@ -0,0 +1,252 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * In-memory idempotency store
5
+ *
6
+ * Best for:
7
+ * - Development and testing
8
+ * - Single-instance deployments
9
+ * - Short-lived processes
10
+ *
11
+ * NOT recommended for:
12
+ * - Production multi-instance deployments
13
+ * - Long-running processes with many events
14
+ */
15
+ class InMemoryIdempotencyStore {
16
+ records = new Map();
17
+ cleanupTimer;
18
+ defaultTtlMs;
19
+ defaultLockTtlMs;
20
+ maxRecords;
21
+ constructor(config = {}) {
22
+ this.defaultTtlMs = config.defaultTtlMs ?? 24 * 60 * 60 * 1000; // 24 hours
23
+ this.defaultLockTtlMs = config.defaultLockTtlMs ?? 30 * 1000; // 30 seconds
24
+ this.maxRecords = config.maxRecords ?? 100000;
25
+ if (config.cleanupIntervalMs !== 0) {
26
+ const interval = config.cleanupIntervalMs ?? 60 * 1000;
27
+ this.cleanupTimer = setInterval(() => this.cleanup(), interval);
28
+ }
29
+ }
30
+ makeKey(eventId, consumer) {
31
+ return `${consumer}:${eventId}`;
32
+ }
33
+ async check(event, consumer, options = {}) {
34
+ const key = this.makeKey(event.id, consumer);
35
+ const record = this.records.get(key);
36
+ const now = Date.now();
37
+ // Check for existing record
38
+ if (record && record.expiresAt > now) {
39
+ // If processing and lock expired, treat as stale and allow reprocessing
40
+ if (record.status === 'processing') {
41
+ const lockExpired = record.startedAt.getTime() + this.defaultLockTtlMs < now;
42
+ if (lockExpired) {
43
+ // Lock expired, allow new processing attempt
44
+ if (options.acquireLock) {
45
+ const updated = {
46
+ ...record,
47
+ startedAt: new Date(),
48
+ attempts: record.attempts + 1
49
+ };
50
+ this.records.set(key, updated);
51
+ return {
52
+ isDuplicate: false,
53
+ lockAcquired: true
54
+ };
55
+ }
56
+ return {
57
+ isDuplicate: false
58
+ };
59
+ }
60
+ // Still processing within lock period - treat as duplicate
61
+ return {
62
+ isDuplicate: true,
63
+ processedAt: record.startedAt
64
+ };
65
+ }
66
+ // Completed or failed record exists
67
+ return {
68
+ isDuplicate: true,
69
+ processedAt: record.completedAt ?? record.startedAt,
70
+ result: record.result
71
+ };
72
+ }
73
+ // No existing record or expired
74
+ if (options.acquireLock !== false) {
75
+ const ttlMs = options.lockTtlMs ?? this.defaultTtlMs;
76
+ const newRecord = {
77
+ eventId: event.id,
78
+ eventType: event.type,
79
+ startedAt: new Date(),
80
+ status: 'processing',
81
+ consumer,
82
+ attempts: 1,
83
+ expiresAt: now + ttlMs
84
+ };
85
+ this.records.set(key, newRecord);
86
+ this.enforceMaxRecords();
87
+ return {
88
+ isDuplicate: false,
89
+ lockAcquired: true
90
+ };
91
+ }
92
+ return {
93
+ isDuplicate: false
94
+ };
95
+ }
96
+ async markProcessing(event, consumer, ttlMs) {
97
+ const result = await this.check(event, consumer, {
98
+ acquireLock: true,
99
+ lockTtlMs: ttlMs ?? this.defaultLockTtlMs
100
+ });
101
+ return result.lockAcquired ?? false;
102
+ }
103
+ async markCompleted(event, consumer, result, ttlMs) {
104
+ const key = this.makeKey(event.id, consumer);
105
+ const record = this.records.get(key);
106
+ const now = Date.now();
107
+ const updated = {
108
+ eventId: event.id,
109
+ eventType: event.type,
110
+ startedAt: record?.startedAt ?? new Date(),
111
+ completedAt: new Date(),
112
+ status: 'completed',
113
+ result,
114
+ consumer,
115
+ attempts: record?.attempts ?? 1,
116
+ expiresAt: now + (ttlMs ?? this.defaultTtlMs)
117
+ };
118
+ this.records.set(key, updated);
119
+ }
120
+ async markFailed(event, consumer, error) {
121
+ const key = this.makeKey(event.id, consumer);
122
+ const record = this.records.get(key);
123
+ const now = Date.now();
124
+ const errorStr = error instanceof Error ? error.message : String(error);
125
+ const updated = {
126
+ eventId: event.id,
127
+ eventType: event.type,
128
+ startedAt: record?.startedAt ?? new Date(),
129
+ completedAt: new Date(),
130
+ status: 'failed',
131
+ error: errorStr,
132
+ consumer,
133
+ attempts: record?.attempts ?? 1,
134
+ expiresAt: now + this.defaultTtlMs
135
+ };
136
+ this.records.set(key, updated);
137
+ }
138
+ async releaseLock(event, consumer) {
139
+ const key = this.makeKey(event.id, consumer);
140
+ this.records.delete(key);
141
+ }
142
+ async getRecord(eventId, consumer) {
143
+ const key = this.makeKey(eventId, consumer);
144
+ const record = this.records.get(key);
145
+ if (!record || record.expiresAt < Date.now()) {
146
+ return null;
147
+ }
148
+ // Return without internal expiresAt field
149
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
150
+ const {
151
+ expiresAt: _,
152
+ ...publicRecord
153
+ } = record;
154
+ return publicRecord;
155
+ }
156
+ async cleanup() {
157
+ const now = Date.now();
158
+ let cleaned = 0;
159
+ for (const [key, record] of Array.from(this.records.entries())) {
160
+ if (record.expiresAt < now) {
161
+ this.records.delete(key);
162
+ cleaned++;
163
+ }
164
+ }
165
+ return cleaned;
166
+ }
167
+ /**
168
+ * Enforce max records limit using LRU-like eviction
169
+ */
170
+ enforceMaxRecords() {
171
+ if (this.records.size <= this.maxRecords) return;
172
+ // Sort by expires time (oldest first)
173
+ const entries = Array.from(this.records.entries()).sort((a, b) => a[1].expiresAt - b[1].expiresAt);
174
+ // Remove oldest 10%
175
+ const toRemove = Math.ceil(this.maxRecords * 0.1);
176
+ for (let i = 0; i < toRemove && i < entries.length; i++) {
177
+ this.records.delete(entries[i][0]);
178
+ }
179
+ }
180
+ /**
181
+ * Stop cleanup timer (for graceful shutdown)
182
+ */
183
+ dispose() {
184
+ if (this.cleanupTimer) {
185
+ clearInterval(this.cleanupTimer);
186
+ this.cleanupTimer = undefined;
187
+ }
188
+ }
189
+ /**
190
+ * Clear all records (for testing)
191
+ */
192
+ clear() {
193
+ this.records.clear();
194
+ }
195
+ /**
196
+ * Get stats (for monitoring)
197
+ */
198
+ getStats() {
199
+ return {
200
+ size: this.records.size,
201
+ maxRecords: this.maxRecords
202
+ };
203
+ }
204
+ }
205
+ /**
206
+ * Create an in-memory idempotency store
207
+ *
208
+ * @example
209
+ * ```typescript
210
+ * const store = createInMemoryIdempotencyStore({
211
+ * defaultTtlMs: 60 * 60 * 1000, // 1 hour
212
+ * maxRecords: 50000,
213
+ * });
214
+ *
215
+ * // In your subscriber
216
+ * const result = await store.check(event, 'my-subscriber');
217
+ * if (result.isDuplicate) {
218
+ * return result.result; // Return cached result
219
+ * }
220
+ *
221
+ * try {
222
+ * const processResult = await processEvent(event);
223
+ * await store.markCompleted(event, 'my-subscriber', processResult);
224
+ * return processResult;
225
+ * } catch (error) {
226
+ * await store.markFailed(event, 'my-subscriber', error);
227
+ * throw error;
228
+ * }
229
+ * ```
230
+ */
231
+ function createInMemoryIdempotencyStore(config) {
232
+ return new InMemoryIdempotencyStore(config);
233
+ }
234
+ /**
235
+ * Generate an idempotency key from event
236
+ * Useful for custom implementations
237
+ */
238
+ function generateIdempotencyKey(event, consumer) {
239
+ return `idempotency:${consumer}:${event.id}`;
240
+ }
241
+ /**
242
+ * Generate an idempotency key from correlation ID
243
+ * Useful for request-level idempotency
244
+ */
245
+ function generateCorrelationKey(correlationId, operation) {
246
+ return `idempotency:correlation:${operation}:${correlationId}`;
247
+ }
248
+
249
+ exports.InMemoryIdempotencyStore = InMemoryIdempotencyStore;
250
+ exports.createInMemoryIdempotencyStore = createInMemoryIdempotencyStore;
251
+ exports.generateCorrelationKey = generateCorrelationKey;
252
+ exports.generateIdempotencyKey = generateIdempotencyKey;