@signaltree/events 7.3.6 → 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.
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 +22 -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
@@ -354,194 +354,4 @@ let WebSocketService = class WebSocketService {
354
354
  };
355
355
  WebSocketService = __decorate([Injectable(), __metadata("design:paramtypes", [Object])], WebSocketService);
356
356
 
357
- /**
358
- * Optimistic Update Manager
359
- *
360
- * Tracks optimistic updates and handles confirmation/rollback.
361
- *
362
- * @example
363
- * ```typescript
364
- * const manager = new OptimisticUpdateManager();
365
- *
366
- * // Apply optimistic update
367
- * manager.apply({
368
- * id: 'update-1',
369
- * correlationId: 'corr-123',
370
- * type: 'UpdateTradeStatus',
371
- * data: { status: 'accepted' },
372
- * previousData: { status: 'pending' },
373
- * timeoutMs: 5000,
374
- * rollback: () => store.$.trade.status.set('pending'),
375
- * });
376
- *
377
- * // When server confirms
378
- * manager.confirm('corr-123');
379
- *
380
- * // Or when server rejects
381
- * manager.rollback('corr-123', new Error('Server rejected'));
382
- * ```
383
- */
384
- class OptimisticUpdateManager {
385
- _updates = signal(new Map());
386
- timeouts = new Map();
387
- /**
388
- * Number of pending updates
389
- */
390
- pendingCount = computed(() => this._updates().size);
391
- /**
392
- * Whether there are any pending updates
393
- */
394
- hasPending = computed(() => this._updates().size > 0);
395
- /**
396
- * Get all pending updates
397
- */
398
- pending = computed(() => Array.from(this._updates().values()));
399
- /**
400
- * Apply an optimistic update
401
- */
402
- apply(update) {
403
- // Store the update
404
- this._updates.update(map => {
405
- const newMap = new Map(map);
406
- newMap.set(update.correlationId, update);
407
- return newMap;
408
- });
409
- // Set timeout for automatic rollback
410
- const timeout = setTimeout(() => {
411
- this.rollback(update.correlationId, new Error(`Optimistic update timeout after ${update.timeoutMs}ms`));
412
- }, update.timeoutMs);
413
- this.timeouts.set(update.correlationId, timeout);
414
- }
415
- /**
416
- * Confirm an optimistic update (server accepted)
417
- */
418
- confirm(correlationId) {
419
- const update = this._updates().get(correlationId);
420
- if (!update) {
421
- return false;
422
- }
423
- // Clear timeout
424
- const timeout = this.timeouts.get(correlationId);
425
- if (timeout) {
426
- clearTimeout(timeout);
427
- this.timeouts.delete(correlationId);
428
- }
429
- // Remove from pending
430
- this._updates.update(map => {
431
- const newMap = new Map(map);
432
- newMap.delete(correlationId);
433
- return newMap;
434
- });
435
- return true;
436
- }
437
- /**
438
- * Rollback an optimistic update (server rejected or timeout)
439
- */
440
- rollback(correlationId, error) {
441
- const update = this._updates().get(correlationId);
442
- if (!update) {
443
- return false;
444
- }
445
- // Clear timeout
446
- const timeout = this.timeouts.get(correlationId);
447
- if (timeout) {
448
- clearTimeout(timeout);
449
- this.timeouts.delete(correlationId);
450
- }
451
- // Execute rollback
452
- try {
453
- update.rollback();
454
- } catch (rollbackError) {
455
- console.error('Rollback failed:', rollbackError);
456
- }
457
- // Remove from pending
458
- this._updates.update(map => {
459
- const newMap = new Map(map);
460
- newMap.delete(correlationId);
461
- return newMap;
462
- });
463
- if (error) {
464
- console.warn(`Optimistic update rolled back: ${error.message}`);
465
- }
466
- return true;
467
- }
468
- /**
469
- * Rollback all pending updates
470
- */
471
- rollbackAll(error) {
472
- const updates = Array.from(this._updates().keys());
473
- let count = 0;
474
- for (const correlationId of updates) {
475
- if (this.rollback(correlationId, error)) {
476
- count++;
477
- }
478
- }
479
- return count;
480
- }
481
- /**
482
- * Get update by correlation ID
483
- */
484
- get(correlationId) {
485
- return this._updates().get(correlationId);
486
- }
487
- /**
488
- * Check if an update is pending
489
- */
490
- isPending(correlationId) {
491
- return this._updates().has(correlationId);
492
- }
493
- /**
494
- * Clear all updates without rollback (use with caution)
495
- */
496
- clear() {
497
- // Clear all timeouts
498
- for (const timeout of this.timeouts.values()) {
499
- clearTimeout(timeout);
500
- }
501
- this.timeouts.clear();
502
- // Clear updates
503
- this._updates.set(new Map());
504
- }
505
- /**
506
- * Dispose the manager
507
- */
508
- dispose() {
509
- this.clear();
510
- }
511
- }
512
-
513
- /**
514
- * Create a simple event handler
515
- *
516
- * @example
517
- * ```typescript
518
- * const handler = createEventHandler<TradeProposalCreated>(event => {
519
- * store.$.trades.entities.upsertOne(event.data);
520
- * });
521
- * ```
522
- */
523
- function createEventHandler(handler) {
524
- return handler;
525
- }
526
- /**
527
- * Create a typed event handler with metadata
528
- *
529
- * @example
530
- * ```typescript
531
- * const handler = createTypedHandler('TradeProposalCreated', {
532
- * handle: (event) => {
533
- * store.$.trades.entities.upsertOne(event.data);
534
- * },
535
- * priority: 1,
536
- * });
537
- * ```
538
- */
539
- function createTypedHandler(eventType, options) {
540
- return {
541
- eventType,
542
- handle: options.handle,
543
- priority: options.priority ?? 10
544
- };
545
- }
546
-
547
- export { OptimisticUpdateManager, WebSocketService, createEventHandler, createTypedHandler };
357
+ export { WebSocketService };
@@ -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;