@signaltree/events 7.3.5 → 7.3.6

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.
@@ -0,0 +1,713 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Event Registry class - manages all registered event types
5
+ *
6
+ * @example
7
+ * ```typescript
8
+ * const registry = createEventRegistry();
9
+ *
10
+ * // Register events
11
+ * registry.register({
12
+ * type: 'TradeProposalCreated',
13
+ * schema: TradeProposalCreatedSchema,
14
+ * priority: 'high',
15
+ * description: 'Emitted when a user proposes a trade',
16
+ * category: 'trade',
17
+ * });
18
+ *
19
+ * // Validate events
20
+ * const validated = registry.validate(rawEvent);
21
+ *
22
+ * // Get schema for event type
23
+ * const schema = registry.getSchema('TradeProposalCreated');
24
+ * ```
25
+ */
26
+ class EventRegistry {
27
+ events = new Map();
28
+ config;
29
+ constructor(config = {}) {
30
+ this.config = {
31
+ strict: config.strict ?? false,
32
+ warnOnDeprecated: config.warnOnDeprecated ?? true
33
+ };
34
+ }
35
+ /**
36
+ * Register an event type with its schema
37
+ */
38
+ register(event) {
39
+ if (this.events.has(event.type)) {
40
+ throw new Error(`Event type '${event.type}' is already registered`);
41
+ }
42
+ this.events.set(event.type, event);
43
+ return this;
44
+ }
45
+ /**
46
+ * Register multiple events at once
47
+ */
48
+ registerMany(events) {
49
+ for (const event of events) {
50
+ this.register(event);
51
+ }
52
+ return this;
53
+ }
54
+ /**
55
+ * Get schema for an event type
56
+ */
57
+ getSchema(type) {
58
+ return this.events.get(type)?.schema;
59
+ }
60
+ /**
61
+ * Get registered event info
62
+ */
63
+ getEvent(type) {
64
+ return this.events.get(type);
65
+ }
66
+ /**
67
+ * Check if event type is registered
68
+ */
69
+ has(type) {
70
+ return this.events.has(type);
71
+ }
72
+ /**
73
+ * Get default priority for event type
74
+ */
75
+ getPriority(type) {
76
+ return this.events.get(type)?.priority ?? 'normal';
77
+ }
78
+ /**
79
+ * Validate an event against its registered schema
80
+ *
81
+ * @throws Error if event type is unknown (in strict mode) or validation fails
82
+ */
83
+ validate(event) {
84
+ if (typeof event !== 'object' || event === null) {
85
+ throw new Error('Event must be an object');
86
+ }
87
+ const eventObj = event;
88
+ const type = eventObj['type'];
89
+ if (!type) {
90
+ throw new Error('Event must have a type field');
91
+ }
92
+ const registered = this.events.get(type);
93
+ if (!registered) {
94
+ if (this.config.strict) {
95
+ throw new Error(`Unknown event type: ${type}`);
96
+ }
97
+ // In non-strict mode, return as-is (no validation)
98
+ return event;
99
+ }
100
+ // Warn about deprecated events
101
+ if (registered.deprecated && this.config.warnOnDeprecated) {
102
+ console.warn(`[EventRegistry] Event '${type}' is deprecated. ${registered.deprecationMessage ?? ''}`);
103
+ }
104
+ const result = registered.schema.safeParse(event);
105
+ if (!result.success) {
106
+ const errors = result.error.issues.map(i => `${i.path.join('.')}: ${i.message}`);
107
+ throw new Error(`Event validation failed for '${type}': ${errors.join(', ')}`);
108
+ }
109
+ return result.data;
110
+ }
111
+ /**
112
+ * Check if event is valid without throwing
113
+ */
114
+ isValid(event) {
115
+ try {
116
+ this.validate(event);
117
+ return true;
118
+ } catch {
119
+ return false;
120
+ }
121
+ }
122
+ /**
123
+ * Get all registered event types
124
+ */
125
+ getAllTypes() {
126
+ return Array.from(this.events.keys());
127
+ }
128
+ /**
129
+ * Get all registered events
130
+ */
131
+ getAll() {
132
+ return Array.from(this.events.values());
133
+ }
134
+ /**
135
+ * Get events by category
136
+ */
137
+ getByCategory(category) {
138
+ return this.getAll().filter(e => e.category === category);
139
+ }
140
+ /**
141
+ * Get event catalog for documentation
142
+ */
143
+ getCatalog() {
144
+ return this.getAll().map(e => ({
145
+ type: e.type,
146
+ category: e.category,
147
+ priority: e.priority,
148
+ description: e.description,
149
+ deprecated: e.deprecated ?? false
150
+ }));
151
+ }
152
+ /**
153
+ * Export registry as JSON schema (for external tools)
154
+ */
155
+ toJSONSchema() {
156
+ const schemas = {};
157
+ for (const [type, event] of Array.from(this.events.entries())) {
158
+ // Note: This is a simplified JSON schema export
159
+ // For full compatibility, use zod-to-json-schema package
160
+ schemas[type] = {
161
+ type: 'object',
162
+ description: event.description,
163
+ deprecated: event.deprecated,
164
+ priority: event.priority,
165
+ category: event.category
166
+ };
167
+ }
168
+ return {
169
+ $schema: 'http://json-schema.org/draft-07/schema#',
170
+ title: 'Event Registry',
171
+ definitions: schemas
172
+ };
173
+ }
174
+ }
175
+ /**
176
+ * Create a new event registry
177
+ */
178
+ function createEventRegistry(config) {
179
+ return new EventRegistry(config);
180
+ }
181
+
182
+ /**
183
+ * Error Classification - Determine retry behavior for errors
184
+ *
185
+ * Provides:
186
+ * - Retryable vs non-retryable error classification
187
+ * - Error categories (transient, permanent, poison)
188
+ * - Retry configuration per error type
189
+ * - Custom error classifiers
190
+ */
191
+ /**
192
+ * Default retry configurations by classification
193
+ */
194
+ const DEFAULT_RETRY_CONFIGS = {
195
+ transient: {
196
+ maxAttempts: 5,
197
+ initialDelayMs: 1000,
198
+ maxDelayMs: 60000,
199
+ backoffMultiplier: 2,
200
+ jitter: 0.1
201
+ },
202
+ permanent: {
203
+ maxAttempts: 0,
204
+ initialDelayMs: 0,
205
+ maxDelayMs: 0,
206
+ backoffMultiplier: 1,
207
+ jitter: 0
208
+ },
209
+ poison: {
210
+ maxAttempts: 0,
211
+ initialDelayMs: 0,
212
+ maxDelayMs: 0,
213
+ backoffMultiplier: 1,
214
+ jitter: 0
215
+ },
216
+ unknown: {
217
+ maxAttempts: 3,
218
+ initialDelayMs: 2000,
219
+ maxDelayMs: 30000,
220
+ backoffMultiplier: 2,
221
+ jitter: 0.2
222
+ }
223
+ };
224
+ /**
225
+ * Known transient error patterns
226
+ */
227
+ const TRANSIENT_ERROR_PATTERNS = [
228
+ // Network errors
229
+ /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,
230
+ // Database transient
231
+ /deadlock/i, /lock wait timeout/i, /too many connections/i, /connection pool exhausted/i, /temporarily unavailable/i,
232
+ // HTTP transient
233
+ /502 bad gateway/i, /503 service unavailable/i, /504 gateway timeout/i, /429 too many requests/i,
234
+ // Redis/Queue transient
235
+ /BUSY/i, /LOADING/i, /CLUSTERDOWN/i, /READONLY/i,
236
+ // Generic transient
237
+ /temporary failure/i, /try again/i, /retry/i, /throttl/i, /rate limit/i, /circuit breaker/i];
238
+ /**
239
+ * Known permanent error patterns
240
+ */
241
+ const PERMANENT_ERROR_PATTERNS = [
242
+ // Auth errors
243
+ /unauthorized/i, /forbidden/i, /access denied/i, /permission denied/i, /invalid token/i, /token expired/i,
244
+ // Business logic
245
+ /not found/i, /already exists/i, /duplicate/i, /conflict/i, /invalid state/i, /precondition failed/i,
246
+ // HTTP permanent
247
+ /400 bad request/i, /401 unauthorized/i, /403 forbidden/i, /404 not found/i, /409 conflict/i, /422 unprocessable/i];
248
+ /**
249
+ * Known poison error patterns (send to DLQ immediately)
250
+ */
251
+ const POISON_ERROR_PATTERNS = [
252
+ // Schema/Serialization
253
+ /invalid json/i, /json parse error/i, /unexpected token/i, /schema validation/i, /invalid event schema/i, /deserialization/i, /malformed/i,
254
+ // Data corruption
255
+ /data corruption/i, /checksum mismatch/i, /integrity error/i];
256
+ /**
257
+ * Error codes that indicate transient failures
258
+ */
259
+ const TRANSIENT_ERROR_CODES = new Set(['ECONNREFUSED', 'ECONNRESET', 'ETIMEDOUT', 'ENETUNREACH', 'EHOSTUNREACH', 'ENOTFOUND', 'EPIPE', 'EAI_AGAIN']);
260
+ /**
261
+ * HTTP status codes that indicate transient failures
262
+ */
263
+ const TRANSIENT_HTTP_STATUS = new Set([408, 429, 500, 502, 503, 504]);
264
+ /**
265
+ * HTTP status codes that indicate permanent failures
266
+ */
267
+ const PERMANENT_HTTP_STATUS = new Set([400, 401, 403, 404, 405, 409, 410, 422]);
268
+ /**
269
+ * Create an error classifier
270
+ *
271
+ * @example
272
+ * ```typescript
273
+ * const classifier = createErrorClassifier({
274
+ * customClassifiers: [
275
+ * (error) => {
276
+ * if (error instanceof MyCustomTransientError) return 'transient';
277
+ * return null; // Let default classification handle it
278
+ * }
279
+ * ],
280
+ * retryConfigs: {
281
+ * transient: { maxAttempts: 10 }, // Override max attempts
282
+ * },
283
+ * });
284
+ *
285
+ * const result = classifier.classify(error);
286
+ * if (result.sendToDlq) {
287
+ * await dlqService.send(event, error, result.reason);
288
+ * }
289
+ * ```
290
+ */
291
+ function createErrorClassifier(config = {}) {
292
+ const customClassifiers = config.customClassifiers ?? [];
293
+ const defaultClassification = config.defaultClassification ?? 'unknown';
294
+ // Merge retry configs
295
+ const retryConfigs = {
296
+ transient: {
297
+ ...DEFAULT_RETRY_CONFIGS.transient,
298
+ ...config.retryConfigs?.transient
299
+ },
300
+ permanent: {
301
+ ...DEFAULT_RETRY_CONFIGS.permanent,
302
+ ...config.retryConfigs?.permanent
303
+ },
304
+ poison: {
305
+ ...DEFAULT_RETRY_CONFIGS.poison,
306
+ ...config.retryConfigs?.poison
307
+ },
308
+ unknown: {
309
+ ...DEFAULT_RETRY_CONFIGS.unknown,
310
+ ...config.retryConfigs?.unknown
311
+ }
312
+ };
313
+ function extractErrorInfo(error) {
314
+ if (error instanceof Error) {
315
+ const errWithCode = error;
316
+ return {
317
+ message: error.message,
318
+ name: error.name,
319
+ code: errWithCode.code,
320
+ status: errWithCode.status ?? errWithCode.statusCode ?? errWithCode.response?.status
321
+ };
322
+ }
323
+ if (typeof error === 'object' && error !== null) {
324
+ const obj = error;
325
+ return {
326
+ message: String(obj['message'] ?? obj['error'] ?? ''),
327
+ code: obj['code'],
328
+ status: obj['status'] ?? obj['statusCode']
329
+ };
330
+ }
331
+ return {
332
+ message: String(error)
333
+ };
334
+ }
335
+ function classifyByPatterns(message) {
336
+ // Check poison patterns first (most specific)
337
+ for (const pattern of POISON_ERROR_PATTERNS) {
338
+ if (pattern.test(message)) {
339
+ return 'poison';
340
+ }
341
+ }
342
+ // Check permanent patterns
343
+ for (const pattern of PERMANENT_ERROR_PATTERNS) {
344
+ if (pattern.test(message)) {
345
+ return 'permanent';
346
+ }
347
+ }
348
+ // Check transient patterns
349
+ for (const pattern of TRANSIENT_ERROR_PATTERNS) {
350
+ if (pattern.test(message)) {
351
+ return 'transient';
352
+ }
353
+ }
354
+ return null;
355
+ }
356
+ function classify(error) {
357
+ // 1. Try custom classifiers first
358
+ for (const classifier of customClassifiers) {
359
+ const result = classifier(error);
360
+ if (result !== null) {
361
+ return {
362
+ classification: result,
363
+ retryConfig: retryConfigs[result],
364
+ sendToDlq: result === 'poison' || result === 'permanent',
365
+ reason: `Custom classifier: ${result}`
366
+ };
367
+ }
368
+ }
369
+ const {
370
+ message,
371
+ code,
372
+ status,
373
+ name
374
+ } = extractErrorInfo(error);
375
+ // 2. Check error code
376
+ if (code && TRANSIENT_ERROR_CODES.has(code)) {
377
+ return {
378
+ classification: 'transient',
379
+ retryConfig: retryConfigs.transient,
380
+ sendToDlq: false,
381
+ reason: `Error code: ${code}`
382
+ };
383
+ }
384
+ // 3. Check HTTP status
385
+ if (status !== undefined) {
386
+ if (TRANSIENT_HTTP_STATUS.has(status)) {
387
+ return {
388
+ classification: 'transient',
389
+ retryConfig: retryConfigs.transient,
390
+ sendToDlq: false,
391
+ reason: `HTTP status: ${status}`
392
+ };
393
+ }
394
+ if (PERMANENT_HTTP_STATUS.has(status)) {
395
+ return {
396
+ classification: 'permanent',
397
+ retryConfig: retryConfigs.permanent,
398
+ sendToDlq: true,
399
+ reason: `HTTP status: ${status}`
400
+ };
401
+ }
402
+ }
403
+ // 4. Check error patterns
404
+ const patternResult = classifyByPatterns(message) ?? classifyByPatterns(name ?? '');
405
+ if (patternResult) {
406
+ return {
407
+ classification: patternResult,
408
+ retryConfig: retryConfigs[patternResult],
409
+ sendToDlq: patternResult === 'poison' || patternResult === 'permanent',
410
+ reason: `Pattern match: ${message.slice(0, 50)}`
411
+ };
412
+ }
413
+ // 5. Use default classification
414
+ return {
415
+ classification: defaultClassification,
416
+ retryConfig: retryConfigs[defaultClassification],
417
+ sendToDlq: defaultClassification === 'poison' || defaultClassification === 'permanent',
418
+ reason: 'No matching pattern, using default'
419
+ };
420
+ }
421
+ function isRetryable(error) {
422
+ const result = classify(error);
423
+ return result.classification === 'transient' || result.classification === 'unknown';
424
+ }
425
+ function calculateDelay(attempt, retryConfig) {
426
+ // Exponential backoff: initialDelay * multiplier^attempt
427
+ const baseDelay = retryConfig.initialDelayMs * Math.pow(retryConfig.backoffMultiplier, attempt);
428
+ // Cap at maxDelay
429
+ const cappedDelay = Math.min(baseDelay, retryConfig.maxDelayMs);
430
+ // Add jitter to prevent thundering herd
431
+ const jitter = cappedDelay * retryConfig.jitter * Math.random();
432
+ return Math.round(cappedDelay + jitter);
433
+ }
434
+ return {
435
+ classify,
436
+ isRetryable,
437
+ calculateDelay
438
+ };
439
+ }
440
+ /**
441
+ * Pre-configured error classifier instance
442
+ */
443
+ const defaultErrorClassifier = createErrorClassifier();
444
+ /**
445
+ * Quick helper to check if error is retryable
446
+ */
447
+ function isRetryableError(error) {
448
+ return defaultErrorClassifier.isRetryable(error);
449
+ }
450
+ /**
451
+ * Quick helper to classify error
452
+ */
453
+ function classifyError(error) {
454
+ return defaultErrorClassifier.classify(error);
455
+ }
456
+
457
+ /**
458
+ * In-memory idempotency store
459
+ *
460
+ * Best for:
461
+ * - Development and testing
462
+ * - Single-instance deployments
463
+ * - Short-lived processes
464
+ *
465
+ * NOT recommended for:
466
+ * - Production multi-instance deployments
467
+ * - Long-running processes with many events
468
+ */
469
+ class InMemoryIdempotencyStore {
470
+ records = new Map();
471
+ cleanupTimer;
472
+ defaultTtlMs;
473
+ defaultLockTtlMs;
474
+ maxRecords;
475
+ constructor(config = {}) {
476
+ this.defaultTtlMs = config.defaultTtlMs ?? 24 * 60 * 60 * 1000; // 24 hours
477
+ this.defaultLockTtlMs = config.defaultLockTtlMs ?? 30 * 1000; // 30 seconds
478
+ this.maxRecords = config.maxRecords ?? 100000;
479
+ if (config.cleanupIntervalMs !== 0) {
480
+ const interval = config.cleanupIntervalMs ?? 60 * 1000;
481
+ this.cleanupTimer = setInterval(() => this.cleanup(), interval);
482
+ }
483
+ }
484
+ makeKey(eventId, consumer) {
485
+ return `${consumer}:${eventId}`;
486
+ }
487
+ async check(event, consumer, options = {}) {
488
+ const key = this.makeKey(event.id, consumer);
489
+ const record = this.records.get(key);
490
+ const now = Date.now();
491
+ // Check for existing record
492
+ if (record && record.expiresAt > now) {
493
+ // If processing and lock expired, treat as stale and allow reprocessing
494
+ if (record.status === 'processing') {
495
+ const lockExpired = record.startedAt.getTime() + this.defaultLockTtlMs < now;
496
+ if (lockExpired) {
497
+ // Lock expired, allow new processing attempt
498
+ if (options.acquireLock) {
499
+ const updated = {
500
+ ...record,
501
+ startedAt: new Date(),
502
+ attempts: record.attempts + 1
503
+ };
504
+ this.records.set(key, updated);
505
+ return {
506
+ isDuplicate: false,
507
+ lockAcquired: true
508
+ };
509
+ }
510
+ return {
511
+ isDuplicate: false
512
+ };
513
+ }
514
+ // Still processing within lock period - treat as duplicate
515
+ return {
516
+ isDuplicate: true,
517
+ processedAt: record.startedAt
518
+ };
519
+ }
520
+ // Completed or failed record exists
521
+ return {
522
+ isDuplicate: true,
523
+ processedAt: record.completedAt ?? record.startedAt,
524
+ result: record.result
525
+ };
526
+ }
527
+ // No existing record or expired
528
+ if (options.acquireLock !== false) {
529
+ const ttlMs = options.lockTtlMs ?? this.defaultTtlMs;
530
+ const newRecord = {
531
+ eventId: event.id,
532
+ eventType: event.type,
533
+ startedAt: new Date(),
534
+ status: 'processing',
535
+ consumer,
536
+ attempts: 1,
537
+ expiresAt: now + ttlMs
538
+ };
539
+ this.records.set(key, newRecord);
540
+ this.enforceMaxRecords();
541
+ return {
542
+ isDuplicate: false,
543
+ lockAcquired: true
544
+ };
545
+ }
546
+ return {
547
+ isDuplicate: false
548
+ };
549
+ }
550
+ async markProcessing(event, consumer, ttlMs) {
551
+ const result = await this.check(event, consumer, {
552
+ acquireLock: true,
553
+ lockTtlMs: ttlMs ?? this.defaultLockTtlMs
554
+ });
555
+ return result.lockAcquired ?? false;
556
+ }
557
+ async markCompleted(event, consumer, result, ttlMs) {
558
+ const key = this.makeKey(event.id, consumer);
559
+ const record = this.records.get(key);
560
+ const now = Date.now();
561
+ const updated = {
562
+ eventId: event.id,
563
+ eventType: event.type,
564
+ startedAt: record?.startedAt ?? new Date(),
565
+ completedAt: new Date(),
566
+ status: 'completed',
567
+ result,
568
+ consumer,
569
+ attempts: record?.attempts ?? 1,
570
+ expiresAt: now + (ttlMs ?? this.defaultTtlMs)
571
+ };
572
+ this.records.set(key, updated);
573
+ }
574
+ async markFailed(event, consumer, error) {
575
+ const key = this.makeKey(event.id, consumer);
576
+ const record = this.records.get(key);
577
+ const now = Date.now();
578
+ const errorStr = error instanceof Error ? error.message : String(error);
579
+ const updated = {
580
+ eventId: event.id,
581
+ eventType: event.type,
582
+ startedAt: record?.startedAt ?? new Date(),
583
+ completedAt: new Date(),
584
+ status: 'failed',
585
+ error: errorStr,
586
+ consumer,
587
+ attempts: record?.attempts ?? 1,
588
+ expiresAt: now + this.defaultTtlMs
589
+ };
590
+ this.records.set(key, updated);
591
+ }
592
+ async releaseLock(event, consumer) {
593
+ const key = this.makeKey(event.id, consumer);
594
+ this.records.delete(key);
595
+ }
596
+ async getRecord(eventId, consumer) {
597
+ const key = this.makeKey(eventId, consumer);
598
+ const record = this.records.get(key);
599
+ if (!record || record.expiresAt < Date.now()) {
600
+ return null;
601
+ }
602
+ // Return without internal expiresAt field
603
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
604
+ const {
605
+ expiresAt: _,
606
+ ...publicRecord
607
+ } = record;
608
+ return publicRecord;
609
+ }
610
+ async cleanup() {
611
+ const now = Date.now();
612
+ let cleaned = 0;
613
+ for (const [key, record] of Array.from(this.records.entries())) {
614
+ if (record.expiresAt < now) {
615
+ this.records.delete(key);
616
+ cleaned++;
617
+ }
618
+ }
619
+ return cleaned;
620
+ }
621
+ /**
622
+ * Enforce max records limit using LRU-like eviction
623
+ */
624
+ enforceMaxRecords() {
625
+ if (this.records.size <= this.maxRecords) return;
626
+ // Sort by expires time (oldest first)
627
+ const entries = Array.from(this.records.entries()).sort((a, b) => a[1].expiresAt - b[1].expiresAt);
628
+ // Remove oldest 10%
629
+ const toRemove = Math.ceil(this.maxRecords * 0.1);
630
+ for (let i = 0; i < toRemove && i < entries.length; i++) {
631
+ this.records.delete(entries[i][0]);
632
+ }
633
+ }
634
+ /**
635
+ * Stop cleanup timer (for graceful shutdown)
636
+ */
637
+ dispose() {
638
+ if (this.cleanupTimer) {
639
+ clearInterval(this.cleanupTimer);
640
+ this.cleanupTimer = undefined;
641
+ }
642
+ }
643
+ /**
644
+ * Clear all records (for testing)
645
+ */
646
+ clear() {
647
+ this.records.clear();
648
+ }
649
+ /**
650
+ * Get stats (for monitoring)
651
+ */
652
+ getStats() {
653
+ return {
654
+ size: this.records.size,
655
+ maxRecords: this.maxRecords
656
+ };
657
+ }
658
+ }
659
+ /**
660
+ * Create an in-memory idempotency store
661
+ *
662
+ * @example
663
+ * ```typescript
664
+ * const store = createInMemoryIdempotencyStore({
665
+ * defaultTtlMs: 60 * 60 * 1000, // 1 hour
666
+ * maxRecords: 50000,
667
+ * });
668
+ *
669
+ * // In your subscriber
670
+ * const result = await store.check(event, 'my-subscriber');
671
+ * if (result.isDuplicate) {
672
+ * return result.result; // Return cached result
673
+ * }
674
+ *
675
+ * try {
676
+ * const processResult = await processEvent(event);
677
+ * await store.markCompleted(event, 'my-subscriber', processResult);
678
+ * return processResult;
679
+ * } catch (error) {
680
+ * await store.markFailed(event, 'my-subscriber', error);
681
+ * throw error;
682
+ * }
683
+ * ```
684
+ */
685
+ function createInMemoryIdempotencyStore(config) {
686
+ return new InMemoryIdempotencyStore(config);
687
+ }
688
+ /**
689
+ * Generate an idempotency key from event
690
+ * Useful for custom implementations
691
+ */
692
+ function generateIdempotencyKey(event, consumer) {
693
+ return `idempotency:${consumer}:${event.id}`;
694
+ }
695
+ /**
696
+ * Generate an idempotency key from correlation ID
697
+ * Useful for request-level idempotency
698
+ */
699
+ function generateCorrelationKey(correlationId, operation) {
700
+ return `idempotency:correlation:${operation}:${correlationId}`;
701
+ }
702
+
703
+ exports.DEFAULT_RETRY_CONFIGS = DEFAULT_RETRY_CONFIGS;
704
+ exports.EventRegistry = EventRegistry;
705
+ exports.InMemoryIdempotencyStore = InMemoryIdempotencyStore;
706
+ exports.classifyError = classifyError;
707
+ exports.createErrorClassifier = createErrorClassifier;
708
+ exports.createEventRegistry = createEventRegistry;
709
+ exports.createInMemoryIdempotencyStore = createInMemoryIdempotencyStore;
710
+ exports.defaultErrorClassifier = defaultErrorClassifier;
711
+ exports.generateCorrelationKey = generateCorrelationKey;
712
+ exports.generateIdempotencyKey = generateIdempotencyKey;
713
+ exports.isRetryableError = isRetryableError;