@signaltree/events 7.3.5 → 7.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/angular/handlers.cjs +38 -0
- package/dist/angular/handlers.js +35 -0
- package/dist/angular/index.cjs +15 -0
- package/dist/angular/index.js +3 -0
- package/dist/angular/optimistic-updates.cjs +161 -0
- package/dist/angular/optimistic-updates.js +159 -0
- package/dist/angular/websocket.service.cjs +357 -0
- package/{angular.esm.js → dist/angular/websocket.service.js} +1 -191
- package/dist/core/error-classification.cjs +282 -0
- package/dist/core/error-classification.js +276 -0
- package/dist/core/factory.cjs +148 -0
- package/{factory.esm.js → dist/core/factory.js} +2 -37
- package/dist/core/idempotency.cjs +252 -0
- package/dist/core/idempotency.js +247 -0
- package/dist/core/registry.cjs +183 -0
- package/dist/core/registry.js +180 -0
- package/dist/core/types.cjs +41 -0
- package/dist/core/types.js +38 -0
- package/dist/core/validation.cjs +185 -0
- package/{index.esm.js → dist/core/validation.js} +1 -4
- package/dist/index.cjs +43 -0
- package/dist/index.js +7 -0
- package/dist/nestjs/base.subscriber.cjs +287 -0
- package/dist/nestjs/base.subscriber.js +287 -0
- package/dist/nestjs/decorators.cjs +35 -0
- package/dist/nestjs/decorators.js +32 -0
- package/dist/nestjs/dlq.service.cjs +249 -0
- package/dist/nestjs/dlq.service.js +249 -0
- package/dist/nestjs/event-bus.module.cjs +152 -0
- package/dist/nestjs/event-bus.module.js +152 -0
- package/dist/nestjs/event-bus.service.cjs +243 -0
- package/dist/nestjs/event-bus.service.js +243 -0
- package/dist/nestjs/index.cjs +33 -0
- package/dist/nestjs/index.js +6 -0
- package/dist/nestjs/tokens.cjs +14 -0
- package/dist/nestjs/tokens.js +9 -0
- package/dist/testing/assertions.cjs +172 -0
- package/dist/testing/assertions.js +169 -0
- package/dist/testing/factories.cjs +122 -0
- package/dist/testing/factories.js +119 -0
- package/dist/testing/helpers.cjs +233 -0
- package/dist/testing/helpers.js +227 -0
- package/dist/testing/index.cjs +20 -0
- package/dist/testing/index.js +4 -0
- package/dist/testing/mock-event-bus.cjs +237 -0
- package/dist/testing/mock-event-bus.js +234 -0
- package/package.json +22 -22
- package/angular.d.ts +0 -1
- package/idempotency.esm.js +0 -701
- package/index.d.ts +0 -1
- package/nestjs.d.ts +0 -1
- package/nestjs.esm.js +0 -944
- package/testing.d.ts +0 -1
- package/testing.esm.js +0 -743
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Error Classification - Determine retry behavior for errors
|
|
5
|
+
*
|
|
6
|
+
* Provides:
|
|
7
|
+
* - Retryable vs non-retryable error classification
|
|
8
|
+
* - Error categories (transient, permanent, poison)
|
|
9
|
+
* - Retry configuration per error type
|
|
10
|
+
* - Custom error classifiers
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Default retry configurations by classification
|
|
14
|
+
*/
|
|
15
|
+
const DEFAULT_RETRY_CONFIGS = {
|
|
16
|
+
transient: {
|
|
17
|
+
maxAttempts: 5,
|
|
18
|
+
initialDelayMs: 1000,
|
|
19
|
+
maxDelayMs: 60000,
|
|
20
|
+
backoffMultiplier: 2,
|
|
21
|
+
jitter: 0.1
|
|
22
|
+
},
|
|
23
|
+
permanent: {
|
|
24
|
+
maxAttempts: 0,
|
|
25
|
+
initialDelayMs: 0,
|
|
26
|
+
maxDelayMs: 0,
|
|
27
|
+
backoffMultiplier: 1,
|
|
28
|
+
jitter: 0
|
|
29
|
+
},
|
|
30
|
+
poison: {
|
|
31
|
+
maxAttempts: 0,
|
|
32
|
+
initialDelayMs: 0,
|
|
33
|
+
maxDelayMs: 0,
|
|
34
|
+
backoffMultiplier: 1,
|
|
35
|
+
jitter: 0
|
|
36
|
+
},
|
|
37
|
+
unknown: {
|
|
38
|
+
maxAttempts: 3,
|
|
39
|
+
initialDelayMs: 2000,
|
|
40
|
+
maxDelayMs: 30000,
|
|
41
|
+
backoffMultiplier: 2,
|
|
42
|
+
jitter: 0.2
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* Known transient error patterns
|
|
47
|
+
*/
|
|
48
|
+
const TRANSIENT_ERROR_PATTERNS = [
|
|
49
|
+
// Network errors
|
|
50
|
+
/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,
|
|
51
|
+
// Database transient
|
|
52
|
+
/deadlock/i, /lock wait timeout/i, /too many connections/i, /connection pool exhausted/i, /temporarily unavailable/i,
|
|
53
|
+
// HTTP transient
|
|
54
|
+
/502 bad gateway/i, /503 service unavailable/i, /504 gateway timeout/i, /429 too many requests/i,
|
|
55
|
+
// Redis/Queue transient
|
|
56
|
+
/BUSY/i, /LOADING/i, /CLUSTERDOWN/i, /READONLY/i,
|
|
57
|
+
// Generic transient
|
|
58
|
+
/temporary failure/i, /try again/i, /retry/i, /throttl/i, /rate limit/i, /circuit breaker/i];
|
|
59
|
+
/**
|
|
60
|
+
* Known permanent error patterns
|
|
61
|
+
*/
|
|
62
|
+
const PERMANENT_ERROR_PATTERNS = [
|
|
63
|
+
// Auth errors
|
|
64
|
+
/unauthorized/i, /forbidden/i, /access denied/i, /permission denied/i, /invalid token/i, /token expired/i,
|
|
65
|
+
// Business logic
|
|
66
|
+
/not found/i, /already exists/i, /duplicate/i, /conflict/i, /invalid state/i, /precondition failed/i,
|
|
67
|
+
// HTTP permanent
|
|
68
|
+
/400 bad request/i, /401 unauthorized/i, /403 forbidden/i, /404 not found/i, /409 conflict/i, /422 unprocessable/i];
|
|
69
|
+
/**
|
|
70
|
+
* Known poison error patterns (send to DLQ immediately)
|
|
71
|
+
*/
|
|
72
|
+
const POISON_ERROR_PATTERNS = [
|
|
73
|
+
// Schema/Serialization
|
|
74
|
+
/invalid json/i, /json parse error/i, /unexpected token/i, /schema validation/i, /invalid event schema/i, /deserialization/i, /malformed/i,
|
|
75
|
+
// Data corruption
|
|
76
|
+
/data corruption/i, /checksum mismatch/i, /integrity error/i];
|
|
77
|
+
/**
|
|
78
|
+
* Error codes that indicate transient failures
|
|
79
|
+
*/
|
|
80
|
+
const TRANSIENT_ERROR_CODES = new Set(['ECONNREFUSED', 'ECONNRESET', 'ETIMEDOUT', 'ENETUNREACH', 'EHOSTUNREACH', 'ENOTFOUND', 'EPIPE', 'EAI_AGAIN']);
|
|
81
|
+
/**
|
|
82
|
+
* HTTP status codes that indicate transient failures
|
|
83
|
+
*/
|
|
84
|
+
const TRANSIENT_HTTP_STATUS = new Set([408, 429, 500, 502, 503, 504]);
|
|
85
|
+
/**
|
|
86
|
+
* HTTP status codes that indicate permanent failures
|
|
87
|
+
*/
|
|
88
|
+
const PERMANENT_HTTP_STATUS = new Set([400, 401, 403, 404, 405, 409, 410, 422]);
|
|
89
|
+
/**
|
|
90
|
+
* Create an error classifier
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* ```typescript
|
|
94
|
+
* const classifier = createErrorClassifier({
|
|
95
|
+
* customClassifiers: [
|
|
96
|
+
* (error) => {
|
|
97
|
+
* if (error instanceof MyCustomTransientError) return 'transient';
|
|
98
|
+
* return null; // Let default classification handle it
|
|
99
|
+
* }
|
|
100
|
+
* ],
|
|
101
|
+
* retryConfigs: {
|
|
102
|
+
* transient: { maxAttempts: 10 }, // Override max attempts
|
|
103
|
+
* },
|
|
104
|
+
* });
|
|
105
|
+
*
|
|
106
|
+
* const result = classifier.classify(error);
|
|
107
|
+
* if (result.sendToDlq) {
|
|
108
|
+
* await dlqService.send(event, error, result.reason);
|
|
109
|
+
* }
|
|
110
|
+
* ```
|
|
111
|
+
*/
|
|
112
|
+
function createErrorClassifier(config = {}) {
|
|
113
|
+
const customClassifiers = config.customClassifiers ?? [];
|
|
114
|
+
const defaultClassification = config.defaultClassification ?? 'unknown';
|
|
115
|
+
// Merge retry configs
|
|
116
|
+
const retryConfigs = {
|
|
117
|
+
transient: {
|
|
118
|
+
...DEFAULT_RETRY_CONFIGS.transient,
|
|
119
|
+
...config.retryConfigs?.transient
|
|
120
|
+
},
|
|
121
|
+
permanent: {
|
|
122
|
+
...DEFAULT_RETRY_CONFIGS.permanent,
|
|
123
|
+
...config.retryConfigs?.permanent
|
|
124
|
+
},
|
|
125
|
+
poison: {
|
|
126
|
+
...DEFAULT_RETRY_CONFIGS.poison,
|
|
127
|
+
...config.retryConfigs?.poison
|
|
128
|
+
},
|
|
129
|
+
unknown: {
|
|
130
|
+
...DEFAULT_RETRY_CONFIGS.unknown,
|
|
131
|
+
...config.retryConfigs?.unknown
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
function extractErrorInfo(error) {
|
|
135
|
+
if (error instanceof Error) {
|
|
136
|
+
const errWithCode = error;
|
|
137
|
+
return {
|
|
138
|
+
message: error.message,
|
|
139
|
+
name: error.name,
|
|
140
|
+
code: errWithCode.code,
|
|
141
|
+
status: errWithCode.status ?? errWithCode.statusCode ?? errWithCode.response?.status
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
if (typeof error === 'object' && error !== null) {
|
|
145
|
+
const obj = error;
|
|
146
|
+
return {
|
|
147
|
+
message: String(obj['message'] ?? obj['error'] ?? ''),
|
|
148
|
+
code: obj['code'],
|
|
149
|
+
status: obj['status'] ?? obj['statusCode']
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
return {
|
|
153
|
+
message: String(error)
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
function classifyByPatterns(message) {
|
|
157
|
+
// Check poison patterns first (most specific)
|
|
158
|
+
for (const pattern of POISON_ERROR_PATTERNS) {
|
|
159
|
+
if (pattern.test(message)) {
|
|
160
|
+
return 'poison';
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
// Check permanent patterns
|
|
164
|
+
for (const pattern of PERMANENT_ERROR_PATTERNS) {
|
|
165
|
+
if (pattern.test(message)) {
|
|
166
|
+
return 'permanent';
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
// Check transient patterns
|
|
170
|
+
for (const pattern of TRANSIENT_ERROR_PATTERNS) {
|
|
171
|
+
if (pattern.test(message)) {
|
|
172
|
+
return 'transient';
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
function classify(error) {
|
|
178
|
+
// 1. Try custom classifiers first
|
|
179
|
+
for (const classifier of customClassifiers) {
|
|
180
|
+
const result = classifier(error);
|
|
181
|
+
if (result !== null) {
|
|
182
|
+
return {
|
|
183
|
+
classification: result,
|
|
184
|
+
retryConfig: retryConfigs[result],
|
|
185
|
+
sendToDlq: result === 'poison' || result === 'permanent',
|
|
186
|
+
reason: `Custom classifier: ${result}`
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
const {
|
|
191
|
+
message,
|
|
192
|
+
code,
|
|
193
|
+
status,
|
|
194
|
+
name
|
|
195
|
+
} = extractErrorInfo(error);
|
|
196
|
+
// 2. Check error code
|
|
197
|
+
if (code && TRANSIENT_ERROR_CODES.has(code)) {
|
|
198
|
+
return {
|
|
199
|
+
classification: 'transient',
|
|
200
|
+
retryConfig: retryConfigs.transient,
|
|
201
|
+
sendToDlq: false,
|
|
202
|
+
reason: `Error code: ${code}`
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
// 3. Check HTTP status
|
|
206
|
+
if (status !== undefined) {
|
|
207
|
+
if (TRANSIENT_HTTP_STATUS.has(status)) {
|
|
208
|
+
return {
|
|
209
|
+
classification: 'transient',
|
|
210
|
+
retryConfig: retryConfigs.transient,
|
|
211
|
+
sendToDlq: false,
|
|
212
|
+
reason: `HTTP status: ${status}`
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
if (PERMANENT_HTTP_STATUS.has(status)) {
|
|
216
|
+
return {
|
|
217
|
+
classification: 'permanent',
|
|
218
|
+
retryConfig: retryConfigs.permanent,
|
|
219
|
+
sendToDlq: true,
|
|
220
|
+
reason: `HTTP status: ${status}`
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
// 4. Check error patterns
|
|
225
|
+
const patternResult = classifyByPatterns(message) ?? classifyByPatterns(name ?? '');
|
|
226
|
+
if (patternResult) {
|
|
227
|
+
return {
|
|
228
|
+
classification: patternResult,
|
|
229
|
+
retryConfig: retryConfigs[patternResult],
|
|
230
|
+
sendToDlq: patternResult === 'poison' || patternResult === 'permanent',
|
|
231
|
+
reason: `Pattern match: ${message.slice(0, 50)}`
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
// 5. Use default classification
|
|
235
|
+
return {
|
|
236
|
+
classification: defaultClassification,
|
|
237
|
+
retryConfig: retryConfigs[defaultClassification],
|
|
238
|
+
sendToDlq: defaultClassification === 'poison' || defaultClassification === 'permanent',
|
|
239
|
+
reason: 'No matching pattern, using default'
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
function isRetryable(error) {
|
|
243
|
+
const result = classify(error);
|
|
244
|
+
return result.classification === 'transient' || result.classification === 'unknown';
|
|
245
|
+
}
|
|
246
|
+
function calculateDelay(attempt, retryConfig) {
|
|
247
|
+
// Exponential backoff: initialDelay * multiplier^attempt
|
|
248
|
+
const baseDelay = retryConfig.initialDelayMs * Math.pow(retryConfig.backoffMultiplier, attempt);
|
|
249
|
+
// Cap at maxDelay
|
|
250
|
+
const cappedDelay = Math.min(baseDelay, retryConfig.maxDelayMs);
|
|
251
|
+
// Add jitter to prevent thundering herd
|
|
252
|
+
const jitter = cappedDelay * retryConfig.jitter * Math.random();
|
|
253
|
+
return Math.round(cappedDelay + jitter);
|
|
254
|
+
}
|
|
255
|
+
return {
|
|
256
|
+
classify,
|
|
257
|
+
isRetryable,
|
|
258
|
+
calculateDelay
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Pre-configured error classifier instance
|
|
263
|
+
*/
|
|
264
|
+
const defaultErrorClassifier = createErrorClassifier();
|
|
265
|
+
/**
|
|
266
|
+
* Quick helper to check if error is retryable
|
|
267
|
+
*/
|
|
268
|
+
function isRetryableError(error) {
|
|
269
|
+
return defaultErrorClassifier.isRetryable(error);
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Quick helper to classify error
|
|
273
|
+
*/
|
|
274
|
+
function classifyError(error) {
|
|
275
|
+
return defaultErrorClassifier.classify(error);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
exports.DEFAULT_RETRY_CONFIGS = DEFAULT_RETRY_CONFIGS;
|
|
279
|
+
exports.classifyError = classifyError;
|
|
280
|
+
exports.createErrorClassifier = createErrorClassifier;
|
|
281
|
+
exports.defaultErrorClassifier = defaultErrorClassifier;
|
|
282
|
+
exports.isRetryableError = isRetryableError;
|
|
@@ -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 };
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var types = require('./types.cjs');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Generate a UUID v7 (time-sortable)
|
|
7
|
+
*
|
|
8
|
+
* UUID v7 format: timestamp (48 bits) + version (4 bits) + random (12 bits) + variant (2 bits) + random (62 bits)
|
|
9
|
+
*/
|
|
10
|
+
function generateEventId() {
|
|
11
|
+
// Get timestamp in milliseconds
|
|
12
|
+
const timestamp = Date.now();
|
|
13
|
+
// Convert to hex (12 characters for 48 bits)
|
|
14
|
+
const timestampHex = timestamp.toString(16).padStart(12, '0');
|
|
15
|
+
// Generate random bytes
|
|
16
|
+
const randomBytes = new Uint8Array(10);
|
|
17
|
+
crypto.getRandomValues(randomBytes);
|
|
18
|
+
// Convert to hex
|
|
19
|
+
const randomHex = Array.from(randomBytes).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
20
|
+
// Construct UUID v7
|
|
21
|
+
// Format: xxxxxxxx-xxxx-7xxx-yxxx-xxxxxxxxxxxx
|
|
22
|
+
// Where x is timestamp/random and 7 is version, y is variant (8, 9, a, or b)
|
|
23
|
+
const uuid = [timestampHex.slice(0, 8),
|
|
24
|
+
// time_low
|
|
25
|
+
timestampHex.slice(8, 12),
|
|
26
|
+
// time_mid
|
|
27
|
+
'7' + randomHex.slice(0, 3),
|
|
28
|
+
// version (7) + random
|
|
29
|
+
(parseInt(randomHex.slice(3, 5), 16) & 0x3f | 0x80).toString(16).padStart(2, '0') + randomHex.slice(5, 7),
|
|
30
|
+
// variant + random
|
|
31
|
+
randomHex.slice(7, 19) // random
|
|
32
|
+
].join('-');
|
|
33
|
+
return uuid;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Generate a correlation ID (also UUID v7 for traceability)
|
|
37
|
+
*/
|
|
38
|
+
function generateCorrelationId() {
|
|
39
|
+
return generateEventId();
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Create a single event
|
|
43
|
+
*/
|
|
44
|
+
function createEvent(type, data, options) {
|
|
45
|
+
const id = options.id ?? generateEventId();
|
|
46
|
+
const correlationId = options.correlationId ?? generateCorrelationId();
|
|
47
|
+
const timestamp = options.timestamp ?? new Date().toISOString();
|
|
48
|
+
const actor = options.actor ?? {
|
|
49
|
+
id: 'system',
|
|
50
|
+
type: 'system'
|
|
51
|
+
};
|
|
52
|
+
const metadata = {
|
|
53
|
+
source: options.source,
|
|
54
|
+
environment: options.environment,
|
|
55
|
+
...options.metadata
|
|
56
|
+
};
|
|
57
|
+
return {
|
|
58
|
+
id,
|
|
59
|
+
type,
|
|
60
|
+
version: options.version ?? types.DEFAULT_EVENT_VERSION,
|
|
61
|
+
timestamp,
|
|
62
|
+
correlationId,
|
|
63
|
+
causationId: options.causationId,
|
|
64
|
+
actor,
|
|
65
|
+
metadata,
|
|
66
|
+
data,
|
|
67
|
+
priority: options.priority,
|
|
68
|
+
aggregate: options.aggregate
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Create an event factory with default configuration
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* ```typescript
|
|
76
|
+
* const factory = createEventFactory({
|
|
77
|
+
* source: 'trade-service',
|
|
78
|
+
* environment: process.env.NODE_ENV || 'development',
|
|
79
|
+
* });
|
|
80
|
+
*
|
|
81
|
+
* const event = factory.create('TradeProposalCreated', {
|
|
82
|
+
* tradeId: '123',
|
|
83
|
+
* initiatorId: 'user-1',
|
|
84
|
+
* recipientId: 'user-2',
|
|
85
|
+
* }, {
|
|
86
|
+
* actor: { id: 'user-1', type: 'user' },
|
|
87
|
+
* priority: 'high',
|
|
88
|
+
* });
|
|
89
|
+
* ```
|
|
90
|
+
*/
|
|
91
|
+
function createEventFactory(config) {
|
|
92
|
+
// Thread-local-like storage for correlation ID
|
|
93
|
+
let currentCorrelationId;
|
|
94
|
+
const systemActor = config.systemActor ?? {
|
|
95
|
+
id: 'system',
|
|
96
|
+
type: 'system',
|
|
97
|
+
name: config.source
|
|
98
|
+
};
|
|
99
|
+
const generateId = config.generateId ?? generateEventId;
|
|
100
|
+
const generateCorrelation = config.generateCorrelationId ?? generateCorrelationId;
|
|
101
|
+
return {
|
|
102
|
+
create(type, data, options = {}) {
|
|
103
|
+
const id = options.id ?? generateId();
|
|
104
|
+
const correlationId = options.correlationId ?? currentCorrelationId ?? generateCorrelation();
|
|
105
|
+
const timestamp = options.timestamp ?? new Date().toISOString();
|
|
106
|
+
const actor = options.actor ?? systemActor;
|
|
107
|
+
const metadata = {
|
|
108
|
+
source: config.source,
|
|
109
|
+
environment: config.environment,
|
|
110
|
+
...options.metadata
|
|
111
|
+
};
|
|
112
|
+
return {
|
|
113
|
+
id,
|
|
114
|
+
type,
|
|
115
|
+
version: options.version ?? types.DEFAULT_EVENT_VERSION,
|
|
116
|
+
timestamp,
|
|
117
|
+
correlationId,
|
|
118
|
+
causationId: options.causationId,
|
|
119
|
+
actor,
|
|
120
|
+
metadata,
|
|
121
|
+
data,
|
|
122
|
+
priority: options.priority,
|
|
123
|
+
aggregate: options.aggregate
|
|
124
|
+
};
|
|
125
|
+
},
|
|
126
|
+
createFromCause(type, data, cause, options = {}) {
|
|
127
|
+
return this.create(type, data, {
|
|
128
|
+
...options,
|
|
129
|
+
correlationId: cause.correlationId,
|
|
130
|
+
causationId: cause.id
|
|
131
|
+
});
|
|
132
|
+
},
|
|
133
|
+
getCorrelationId() {
|
|
134
|
+
return currentCorrelationId;
|
|
135
|
+
},
|
|
136
|
+
setCorrelationId(id) {
|
|
137
|
+
currentCorrelationId = id;
|
|
138
|
+
},
|
|
139
|
+
clearCorrelationId() {
|
|
140
|
+
currentCorrelationId = undefined;
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
exports.createEvent = createEvent;
|
|
146
|
+
exports.createEventFactory = createEventFactory;
|
|
147
|
+
exports.generateCorrelationId = generateCorrelationId;
|
|
148
|
+
exports.generateEventId = generateEventId;
|