@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
@@ -1,8 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var factory = require('./factory.cjs.js');
4
3
  var zod = require('zod');
5
- var idempotency = require('./idempotency.cjs.js');
6
4
 
7
5
  /**
8
6
  * Event validation using Zod schemas
@@ -174,27 +172,7 @@ function parseEvent(schema, event) {
174
172
  return schema.safeParse(event);
175
173
  }
176
174
 
177
- exports.DEFAULT_EVENT_VERSION = factory.DEFAULT_EVENT_VERSION;
178
- exports.EVENT_PRIORITIES = factory.EVENT_PRIORITIES;
179
- exports.createEvent = factory.createEvent;
180
- exports.createEventFactory = factory.createEventFactory;
181
- exports.generateCorrelationId = factory.generateCorrelationId;
182
- exports.generateEventId = factory.generateEventId;
183
- Object.defineProperty(exports, "z", {
184
- enumerable: true,
185
- get: function () { return zod.z; }
186
- });
187
- exports.DEFAULT_RETRY_CONFIGS = idempotency.DEFAULT_RETRY_CONFIGS;
188
- exports.EventRegistry = idempotency.EventRegistry;
189
- exports.InMemoryIdempotencyStore = idempotency.InMemoryIdempotencyStore;
190
- exports.classifyError = idempotency.classifyError;
191
- exports.createErrorClassifier = idempotency.createErrorClassifier;
192
- exports.createEventRegistry = idempotency.createEventRegistry;
193
- exports.createInMemoryIdempotencyStore = idempotency.createInMemoryIdempotencyStore;
194
- exports.defaultErrorClassifier = idempotency.defaultErrorClassifier;
195
- exports.generateCorrelationKey = idempotency.generateCorrelationKey;
196
- exports.generateIdempotencyKey = idempotency.generateIdempotencyKey;
197
- exports.isRetryableError = idempotency.isRetryableError;
175
+ exports.AggregateSchema = AggregateSchema;
198
176
  exports.BaseEventSchema = BaseEventSchema;
199
177
  exports.EventActorSchema = EventActorSchema;
200
178
  exports.EventMetadataSchema = EventMetadataSchema;
@@ -1,7 +1,4 @@
1
- export { D as DEFAULT_EVENT_VERSION, E as EVENT_PRIORITIES, c as createEvent, a as createEventFactory, b as generateCorrelationId, g as generateEventId } from './factory.esm.js';
2
1
  import { z } from 'zod';
3
- export { z } from 'zod';
4
- export { D as DEFAULT_RETRY_CONFIGS, E as EventRegistry, I as InMemoryIdempotencyStore, a as classifyError, b as createErrorClassifier, c as createEventRegistry, e as createInMemoryIdempotencyStore, d as defaultErrorClassifier, f as generateCorrelationKey, g as generateIdempotencyKey, i as isRetryableError } from './idempotency.esm.js';
5
2
 
6
3
  /**
7
4
  * Event validation using Zod schemas
@@ -173,4 +170,4 @@ function parseEvent(schema, event) {
173
170
  return schema.safeParse(event);
174
171
  }
175
172
 
176
- export { BaseEventSchema, EventActorSchema, EventMetadataSchema, EventValidationError, EventVersionSchema, createEventSchema, createEventSchemaFromZod, isValidEvent, parseEvent, validateEvent };
173
+ export { AggregateSchema, BaseEventSchema, EventActorSchema, EventMetadataSchema, EventValidationError, EventVersionSchema, createEventSchema, createEventSchemaFromZod, isValidEvent, parseEvent, validateEvent };
package/dist/index.cjs ADDED
@@ -0,0 +1,43 @@
1
+ 'use strict';
2
+
3
+ var types = require('./core/types.cjs');
4
+ var validation = require('./core/validation.cjs');
5
+ var registry = require('./core/registry.cjs');
6
+ var factory = require('./core/factory.cjs');
7
+ var errorClassification = require('./core/error-classification.cjs');
8
+ var idempotency = require('./core/idempotency.cjs');
9
+ var zod = require('zod');
10
+
11
+
12
+
13
+ exports.DEFAULT_EVENT_VERSION = types.DEFAULT_EVENT_VERSION;
14
+ exports.EVENT_PRIORITIES = types.EVENT_PRIORITIES;
15
+ exports.BaseEventSchema = validation.BaseEventSchema;
16
+ exports.EventActorSchema = validation.EventActorSchema;
17
+ exports.EventMetadataSchema = validation.EventMetadataSchema;
18
+ exports.EventValidationError = validation.EventValidationError;
19
+ exports.EventVersionSchema = validation.EventVersionSchema;
20
+ exports.createEventSchema = validation.createEventSchema;
21
+ exports.createEventSchemaFromZod = validation.createEventSchemaFromZod;
22
+ exports.isValidEvent = validation.isValidEvent;
23
+ exports.parseEvent = validation.parseEvent;
24
+ exports.validateEvent = validation.validateEvent;
25
+ exports.EventRegistry = registry.EventRegistry;
26
+ exports.createEventRegistry = registry.createEventRegistry;
27
+ exports.createEvent = factory.createEvent;
28
+ exports.createEventFactory = factory.createEventFactory;
29
+ exports.generateCorrelationId = factory.generateCorrelationId;
30
+ exports.generateEventId = factory.generateEventId;
31
+ exports.DEFAULT_RETRY_CONFIGS = errorClassification.DEFAULT_RETRY_CONFIGS;
32
+ exports.classifyError = errorClassification.classifyError;
33
+ exports.createErrorClassifier = errorClassification.createErrorClassifier;
34
+ exports.defaultErrorClassifier = errorClassification.defaultErrorClassifier;
35
+ exports.isRetryableError = errorClassification.isRetryableError;
36
+ exports.InMemoryIdempotencyStore = idempotency.InMemoryIdempotencyStore;
37
+ exports.createInMemoryIdempotencyStore = idempotency.createInMemoryIdempotencyStore;
38
+ exports.generateCorrelationKey = idempotency.generateCorrelationKey;
39
+ exports.generateIdempotencyKey = idempotency.generateIdempotencyKey;
40
+ Object.defineProperty(exports, "z", {
41
+ enumerable: true,
42
+ get: function () { return zod.z; }
43
+ });
package/dist/index.js ADDED
@@ -0,0 +1,7 @@
1
+ export { DEFAULT_EVENT_VERSION, EVENT_PRIORITIES } from './core/types.js';
2
+ export { BaseEventSchema, EventActorSchema, EventMetadataSchema, EventValidationError, EventVersionSchema, createEventSchema, createEventSchemaFromZod, isValidEvent, parseEvent, validateEvent } from './core/validation.js';
3
+ export { EventRegistry, createEventRegistry } from './core/registry.js';
4
+ export { createEvent, createEventFactory, generateCorrelationId, generateEventId } from './core/factory.js';
5
+ export { DEFAULT_RETRY_CONFIGS, classifyError, createErrorClassifier, defaultErrorClassifier, isRetryableError } from './core/error-classification.js';
6
+ export { InMemoryIdempotencyStore, createInMemoryIdempotencyStore, generateCorrelationKey, generateIdempotencyKey } from './core/idempotency.js';
7
+ export { z } from 'zod';
@@ -0,0 +1,287 @@
1
+ 'use strict';
2
+
3
+ var tslib = require('tslib');
4
+ var common = require('@nestjs/common');
5
+ var bullmq = require('bullmq');
6
+ var dlq_service = require('./dlq.service.cjs');
7
+ var tokens = require('./tokens.cjs');
8
+
9
+ /**
10
+ * Base class for event subscribers
11
+ *
12
+ * @example
13
+ * ```typescript
14
+ * @Injectable()
15
+ * export class TradeSubscriber extends BaseSubscriber {
16
+ * protected readonly config: SubscriberConfig = {
17
+ * name: 'trade-subscriber',
18
+ * eventTypes: ['TradeProposalCreated', 'TradeAccepted'],
19
+ * priority: 'high',
20
+ * concurrency: 5,
21
+ * };
22
+ *
23
+ * async handle(event: TradeProposalCreated | TradeAccepted): Promise<ProcessingResult> {
24
+ * switch (event.type) {
25
+ * case 'TradeProposalCreated':
26
+ * await this.handleTradeCreated(event);
27
+ * break;
28
+ * case 'TradeAccepted':
29
+ * await this.handleTradeAccepted(event);
30
+ * break;
31
+ * }
32
+ * return { success: true };
33
+ * }
34
+ * }
35
+ * ```
36
+ */
37
+ exports.BaseSubscriber = class BaseSubscriber {
38
+ busConfig;
39
+ idempotencyStore;
40
+ errorClassifier;
41
+ dlqService;
42
+ logger;
43
+ worker;
44
+ connection;
45
+ metrics = {
46
+ processed: 0,
47
+ succeeded: 0,
48
+ failed: 0,
49
+ retried: 0,
50
+ dlqSent: 0,
51
+ duplicatesSkipped: 0,
52
+ avgProcessingTimeMs: 0
53
+ };
54
+ constructor(busConfig, idempotencyStore, errorClassifier, dlqService) {
55
+ this.busConfig = busConfig;
56
+ this.idempotencyStore = idempotencyStore;
57
+ this.errorClassifier = errorClassifier;
58
+ this.dlqService = dlqService;
59
+ this.logger = new common.Logger(this.constructor.name);
60
+ this.connection = {
61
+ host: busConfig.redis.host,
62
+ port: busConfig.redis.port,
63
+ password: busConfig.redis.password,
64
+ db: busConfig.redis.db
65
+ };
66
+ }
67
+ /**
68
+ * Called before processing starts (for setup/validation)
69
+ */
70
+ async beforeProcess(
71
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
72
+ _event) {
73
+ // Override in subclass if needed
74
+ }
75
+ /**
76
+ * Called after processing completes (for cleanup)
77
+ */
78
+ async afterProcess(
79
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
80
+ _event,
81
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
82
+ _result) {
83
+ // Override in subclass if needed
84
+ }
85
+ async onModuleInit() {
86
+ const queueName = this.getQueueName();
87
+ this.logger.log(`Initializing subscriber "${this.config.name}" on queue "${queueName}" ` + `for events: ${this.config.eventTypes.join(', ')}`);
88
+ this.worker = new bullmq.Worker(queueName, async job => this.processJob(job), {
89
+ connection: this.connection,
90
+ concurrency: this.config.concurrency ?? 5,
91
+ lockDuration: this.config.lockDuration ?? 30000,
92
+ stalledInterval: this.config.stalledInterval ?? 30000
93
+ });
94
+ this.setupWorkerListeners();
95
+ this.logger.log(`Subscriber "${this.config.name}" started`);
96
+ }
97
+ async onModuleDestroy() {
98
+ this.logger.log(`Shutting down subscriber "${this.config.name}"...`);
99
+ if (this.worker) {
100
+ await this.worker.close();
101
+ }
102
+ this.logger.log(`Subscriber "${this.config.name}" stopped`);
103
+ }
104
+ /**
105
+ * Get queue name based on config
106
+ */
107
+ getQueueName() {
108
+ if (this.config.queue) {
109
+ return this.config.queue;
110
+ }
111
+ // Find queue for priority
112
+ const priority = this.config.priority ?? 'normal';
113
+ const queueConfig = this.busConfig.queues?.find(q => q.priorities.includes(priority));
114
+ return queueConfig?.name ?? `events-${priority}`;
115
+ }
116
+ /**
117
+ * Process a job with idempotency and error handling
118
+ */
119
+ async processJob(job) {
120
+ const event = job.data;
121
+ const startTime = Date.now();
122
+ // Filter by event type
123
+ if (!this.config.eventTypes.includes(event.type)) {
124
+ // Not our event type, skip silently
125
+ return {
126
+ skipped: true,
127
+ reason: 'event_type_not_handled'
128
+ };
129
+ }
130
+ this.logger.debug(`Processing event ${event.type}:${event.id} (attempt ${job.attemptsMade + 1})`);
131
+ try {
132
+ // Check idempotency
133
+ if (this.config.idempotency !== false) {
134
+ const idempotencyResult = await this.checkIdempotency(event);
135
+ if (idempotencyResult.isDuplicate) {
136
+ this.metrics.duplicatesSkipped++;
137
+ this.logger.debug(`Skipping duplicate event ${event.id}`);
138
+ return idempotencyResult.result ?? {
139
+ skipped: true,
140
+ reason: 'duplicate'
141
+ };
142
+ }
143
+ }
144
+ // Before hook
145
+ await this.beforeProcess(event);
146
+ // Process event
147
+ const result = await this.handle(event);
148
+ // After hook
149
+ await this.afterProcess(event, result);
150
+ // Update idempotency store
151
+ if (this.config.idempotency !== false) {
152
+ await this.idempotencyStore.markCompleted(event, this.config.name, result.result);
153
+ }
154
+ // Update metrics
155
+ this.updateMetrics(startTime, true);
156
+ return result.result;
157
+ } catch (error) {
158
+ return this.handleError(job, event, error, startTime);
159
+ }
160
+ }
161
+ /**
162
+ * Handle processing error
163
+ */
164
+ async handleError(job, event, error, startTime) {
165
+ const errorObj = error instanceof Error ? error : new Error(String(error));
166
+ const classification = this.errorClassifier.classify(error);
167
+ this.logger.error(`Error processing event ${event.type}:${event.id}: ${errorObj.message}`, errorObj.stack);
168
+ // Update idempotency store with failure
169
+ if (this.config.idempotency !== false) {
170
+ await this.idempotencyStore.markFailed(event, this.config.name, error);
171
+ }
172
+ // Check if we should retry
173
+ const maxAttempts = classification.retryConfig?.maxAttempts ?? 5;
174
+ const shouldRetry = job.attemptsMade < maxAttempts && classification.classification !== 'poison';
175
+ if (shouldRetry) {
176
+ this.metrics.retried++;
177
+ this.logger.debug(`Will retry event ${event.id}, attempt ${job.attemptsMade + 1}/${maxAttempts}`);
178
+ throw errorObj; // BullMQ will retry
179
+ }
180
+ // Send to DLQ
181
+ if (this.busConfig.enableDlq && !this.config.skipDlq) {
182
+ await this.sendToDlq(event, errorObj, classification, job.attemptsMade + 1);
183
+ }
184
+ // Update metrics
185
+ this.updateMetrics(startTime, false);
186
+ // Don't throw - we've handled it by sending to DLQ
187
+ return {
188
+ failed: true,
189
+ error: errorObj.message,
190
+ sentToDlq: true
191
+ };
192
+ }
193
+ /**
194
+ * Check idempotency
195
+ */
196
+ async checkIdempotency(event) {
197
+ return this.idempotencyStore.check(event, this.config.name, {
198
+ acquireLock: true,
199
+ lockTtlMs: this.config.lockDuration ?? 30000
200
+ });
201
+ }
202
+ /**
203
+ * Send failed event to DLQ
204
+ */
205
+ async sendToDlq(event, error, classification, attempts) {
206
+ await this.dlqService.send({
207
+ event,
208
+ error: {
209
+ message: error.message,
210
+ stack: error.stack,
211
+ classification: classification.classification,
212
+ reason: classification.reason
213
+ },
214
+ subscriber: this.config.name,
215
+ attempts,
216
+ failedAt: new Date().toISOString()
217
+ });
218
+ this.metrics.dlqSent++;
219
+ this.logger.warn(`Event ${event.id} sent to DLQ after ${attempts} attempts. ` + `Classification: ${classification.classification}`);
220
+ }
221
+ /**
222
+ * Update metrics
223
+ */
224
+ updateMetrics(startTime, success) {
225
+ const duration = Date.now() - startTime;
226
+ this.metrics.processed++;
227
+ if (success) {
228
+ this.metrics.succeeded++;
229
+ } else {
230
+ this.metrics.failed++;
231
+ }
232
+ // Rolling average
233
+ this.metrics.avgProcessingTimeMs = (this.metrics.avgProcessingTimeMs * (this.metrics.processed - 1) + duration) / this.metrics.processed;
234
+ this.metrics.lastProcessedAt = new Date();
235
+ }
236
+ /**
237
+ * Setup worker event listeners
238
+ */
239
+ setupWorkerListeners() {
240
+ if (!this.worker) return;
241
+ this.worker.on('completed', job => {
242
+ this.logger.debug(`Job ${job.id} completed`);
243
+ });
244
+ this.worker.on('failed', (job, error) => {
245
+ this.logger.warn(`Job ${job?.id} failed: ${error.message}`);
246
+ });
247
+ this.worker.on('error', error => {
248
+ this.logger.error(`Worker error: ${error.message}`, error.stack);
249
+ });
250
+ this.worker.on('stalled', jobId => {
251
+ this.logger.warn(`Job ${jobId} stalled`);
252
+ });
253
+ }
254
+ /**
255
+ * Get current metrics
256
+ */
257
+ getMetrics() {
258
+ return {
259
+ ...this.metrics
260
+ };
261
+ }
262
+ /**
263
+ * Pause processing
264
+ */
265
+ async pause() {
266
+ if (this.worker) {
267
+ await this.worker.pause();
268
+ this.logger.log(`Subscriber "${this.config.name}" paused`);
269
+ }
270
+ }
271
+ /**
272
+ * Resume processing
273
+ */
274
+ async resume() {
275
+ if (this.worker) {
276
+ this.worker.resume();
277
+ this.logger.log(`Subscriber "${this.config.name}" resumed`);
278
+ }
279
+ }
280
+ /**
281
+ * Check if worker is running
282
+ */
283
+ isRunning() {
284
+ return this.worker?.isRunning() ?? false;
285
+ }
286
+ };
287
+ exports.BaseSubscriber = tslib.__decorate([common.Injectable(), tslib.__param(0, common.Inject(tokens.EVENT_BUS_CONFIG)), tslib.__param(1, common.Inject(tokens.IDEMPOTENCY_STORE)), tslib.__param(2, common.Inject(tokens.ERROR_CLASSIFIER)), tslib.__metadata("design:paramtypes", [Object, Object, Object, dlq_service.DlqService])], exports.BaseSubscriber);
@@ -0,0 +1,287 @@
1
+ import { __decorate, __param, __metadata } from 'tslib';
2
+ import { Injectable, Inject, Logger } from '@nestjs/common';
3
+ import { Worker } from 'bullmq';
4
+ import { DlqService } from './dlq.service.js';
5
+ import { EVENT_BUS_CONFIG, IDEMPOTENCY_STORE, ERROR_CLASSIFIER } from './tokens.js';
6
+
7
+ /**
8
+ * Base class for event subscribers
9
+ *
10
+ * @example
11
+ * ```typescript
12
+ * @Injectable()
13
+ * export class TradeSubscriber extends BaseSubscriber {
14
+ * protected readonly config: SubscriberConfig = {
15
+ * name: 'trade-subscriber',
16
+ * eventTypes: ['TradeProposalCreated', 'TradeAccepted'],
17
+ * priority: 'high',
18
+ * concurrency: 5,
19
+ * };
20
+ *
21
+ * async handle(event: TradeProposalCreated | TradeAccepted): Promise<ProcessingResult> {
22
+ * switch (event.type) {
23
+ * case 'TradeProposalCreated':
24
+ * await this.handleTradeCreated(event);
25
+ * break;
26
+ * case 'TradeAccepted':
27
+ * await this.handleTradeAccepted(event);
28
+ * break;
29
+ * }
30
+ * return { success: true };
31
+ * }
32
+ * }
33
+ * ```
34
+ */
35
+ let BaseSubscriber = class BaseSubscriber {
36
+ busConfig;
37
+ idempotencyStore;
38
+ errorClassifier;
39
+ dlqService;
40
+ logger;
41
+ worker;
42
+ connection;
43
+ metrics = {
44
+ processed: 0,
45
+ succeeded: 0,
46
+ failed: 0,
47
+ retried: 0,
48
+ dlqSent: 0,
49
+ duplicatesSkipped: 0,
50
+ avgProcessingTimeMs: 0
51
+ };
52
+ constructor(busConfig, idempotencyStore, errorClassifier, dlqService) {
53
+ this.busConfig = busConfig;
54
+ this.idempotencyStore = idempotencyStore;
55
+ this.errorClassifier = errorClassifier;
56
+ this.dlqService = dlqService;
57
+ this.logger = new Logger(this.constructor.name);
58
+ this.connection = {
59
+ host: busConfig.redis.host,
60
+ port: busConfig.redis.port,
61
+ password: busConfig.redis.password,
62
+ db: busConfig.redis.db
63
+ };
64
+ }
65
+ /**
66
+ * Called before processing starts (for setup/validation)
67
+ */
68
+ async beforeProcess(
69
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
70
+ _event) {
71
+ // Override in subclass if needed
72
+ }
73
+ /**
74
+ * Called after processing completes (for cleanup)
75
+ */
76
+ async afterProcess(
77
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
78
+ _event,
79
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
80
+ _result) {
81
+ // Override in subclass if needed
82
+ }
83
+ async onModuleInit() {
84
+ const queueName = this.getQueueName();
85
+ this.logger.log(`Initializing subscriber "${this.config.name}" on queue "${queueName}" ` + `for events: ${this.config.eventTypes.join(', ')}`);
86
+ this.worker = new Worker(queueName, async job => this.processJob(job), {
87
+ connection: this.connection,
88
+ concurrency: this.config.concurrency ?? 5,
89
+ lockDuration: this.config.lockDuration ?? 30000,
90
+ stalledInterval: this.config.stalledInterval ?? 30000
91
+ });
92
+ this.setupWorkerListeners();
93
+ this.logger.log(`Subscriber "${this.config.name}" started`);
94
+ }
95
+ async onModuleDestroy() {
96
+ this.logger.log(`Shutting down subscriber "${this.config.name}"...`);
97
+ if (this.worker) {
98
+ await this.worker.close();
99
+ }
100
+ this.logger.log(`Subscriber "${this.config.name}" stopped`);
101
+ }
102
+ /**
103
+ * Get queue name based on config
104
+ */
105
+ getQueueName() {
106
+ if (this.config.queue) {
107
+ return this.config.queue;
108
+ }
109
+ // Find queue for priority
110
+ const priority = this.config.priority ?? 'normal';
111
+ const queueConfig = this.busConfig.queues?.find(q => q.priorities.includes(priority));
112
+ return queueConfig?.name ?? `events-${priority}`;
113
+ }
114
+ /**
115
+ * Process a job with idempotency and error handling
116
+ */
117
+ async processJob(job) {
118
+ const event = job.data;
119
+ const startTime = Date.now();
120
+ // Filter by event type
121
+ if (!this.config.eventTypes.includes(event.type)) {
122
+ // Not our event type, skip silently
123
+ return {
124
+ skipped: true,
125
+ reason: 'event_type_not_handled'
126
+ };
127
+ }
128
+ this.logger.debug(`Processing event ${event.type}:${event.id} (attempt ${job.attemptsMade + 1})`);
129
+ try {
130
+ // Check idempotency
131
+ if (this.config.idempotency !== false) {
132
+ const idempotencyResult = await this.checkIdempotency(event);
133
+ if (idempotencyResult.isDuplicate) {
134
+ this.metrics.duplicatesSkipped++;
135
+ this.logger.debug(`Skipping duplicate event ${event.id}`);
136
+ return idempotencyResult.result ?? {
137
+ skipped: true,
138
+ reason: 'duplicate'
139
+ };
140
+ }
141
+ }
142
+ // Before hook
143
+ await this.beforeProcess(event);
144
+ // Process event
145
+ const result = await this.handle(event);
146
+ // After hook
147
+ await this.afterProcess(event, result);
148
+ // Update idempotency store
149
+ if (this.config.idempotency !== false) {
150
+ await this.idempotencyStore.markCompleted(event, this.config.name, result.result);
151
+ }
152
+ // Update metrics
153
+ this.updateMetrics(startTime, true);
154
+ return result.result;
155
+ } catch (error) {
156
+ return this.handleError(job, event, error, startTime);
157
+ }
158
+ }
159
+ /**
160
+ * Handle processing error
161
+ */
162
+ async handleError(job, event, error, startTime) {
163
+ const errorObj = error instanceof Error ? error : new Error(String(error));
164
+ const classification = this.errorClassifier.classify(error);
165
+ this.logger.error(`Error processing event ${event.type}:${event.id}: ${errorObj.message}`, errorObj.stack);
166
+ // Update idempotency store with failure
167
+ if (this.config.idempotency !== false) {
168
+ await this.idempotencyStore.markFailed(event, this.config.name, error);
169
+ }
170
+ // Check if we should retry
171
+ const maxAttempts = classification.retryConfig?.maxAttempts ?? 5;
172
+ const shouldRetry = job.attemptsMade < maxAttempts && classification.classification !== 'poison';
173
+ if (shouldRetry) {
174
+ this.metrics.retried++;
175
+ this.logger.debug(`Will retry event ${event.id}, attempt ${job.attemptsMade + 1}/${maxAttempts}`);
176
+ throw errorObj; // BullMQ will retry
177
+ }
178
+ // Send to DLQ
179
+ if (this.busConfig.enableDlq && !this.config.skipDlq) {
180
+ await this.sendToDlq(event, errorObj, classification, job.attemptsMade + 1);
181
+ }
182
+ // Update metrics
183
+ this.updateMetrics(startTime, false);
184
+ // Don't throw - we've handled it by sending to DLQ
185
+ return {
186
+ failed: true,
187
+ error: errorObj.message,
188
+ sentToDlq: true
189
+ };
190
+ }
191
+ /**
192
+ * Check idempotency
193
+ */
194
+ async checkIdempotency(event) {
195
+ return this.idempotencyStore.check(event, this.config.name, {
196
+ acquireLock: true,
197
+ lockTtlMs: this.config.lockDuration ?? 30000
198
+ });
199
+ }
200
+ /**
201
+ * Send failed event to DLQ
202
+ */
203
+ async sendToDlq(event, error, classification, attempts) {
204
+ await this.dlqService.send({
205
+ event,
206
+ error: {
207
+ message: error.message,
208
+ stack: error.stack,
209
+ classification: classification.classification,
210
+ reason: classification.reason
211
+ },
212
+ subscriber: this.config.name,
213
+ attempts,
214
+ failedAt: new Date().toISOString()
215
+ });
216
+ this.metrics.dlqSent++;
217
+ this.logger.warn(`Event ${event.id} sent to DLQ after ${attempts} attempts. ` + `Classification: ${classification.classification}`);
218
+ }
219
+ /**
220
+ * Update metrics
221
+ */
222
+ updateMetrics(startTime, success) {
223
+ const duration = Date.now() - startTime;
224
+ this.metrics.processed++;
225
+ if (success) {
226
+ this.metrics.succeeded++;
227
+ } else {
228
+ this.metrics.failed++;
229
+ }
230
+ // Rolling average
231
+ this.metrics.avgProcessingTimeMs = (this.metrics.avgProcessingTimeMs * (this.metrics.processed - 1) + duration) / this.metrics.processed;
232
+ this.metrics.lastProcessedAt = new Date();
233
+ }
234
+ /**
235
+ * Setup worker event listeners
236
+ */
237
+ setupWorkerListeners() {
238
+ if (!this.worker) return;
239
+ this.worker.on('completed', job => {
240
+ this.logger.debug(`Job ${job.id} completed`);
241
+ });
242
+ this.worker.on('failed', (job, error) => {
243
+ this.logger.warn(`Job ${job?.id} failed: ${error.message}`);
244
+ });
245
+ this.worker.on('error', error => {
246
+ this.logger.error(`Worker error: ${error.message}`, error.stack);
247
+ });
248
+ this.worker.on('stalled', jobId => {
249
+ this.logger.warn(`Job ${jobId} stalled`);
250
+ });
251
+ }
252
+ /**
253
+ * Get current metrics
254
+ */
255
+ getMetrics() {
256
+ return {
257
+ ...this.metrics
258
+ };
259
+ }
260
+ /**
261
+ * Pause processing
262
+ */
263
+ async pause() {
264
+ if (this.worker) {
265
+ await this.worker.pause();
266
+ this.logger.log(`Subscriber "${this.config.name}" paused`);
267
+ }
268
+ }
269
+ /**
270
+ * Resume processing
271
+ */
272
+ async resume() {
273
+ if (this.worker) {
274
+ this.worker.resume();
275
+ this.logger.log(`Subscriber "${this.config.name}" resumed`);
276
+ }
277
+ }
278
+ /**
279
+ * Check if worker is running
280
+ */
281
+ isRunning() {
282
+ return this.worker?.isRunning() ?? false;
283
+ }
284
+ };
285
+ BaseSubscriber = __decorate([Injectable(), __param(0, Inject(EVENT_BUS_CONFIG)), __param(1, Inject(IDEMPOTENCY_STORE)), __param(2, Inject(ERROR_CLASSIFIER)), __metadata("design:paramtypes", [Object, Object, Object, DlqService])], BaseSubscriber);
286
+
287
+ export { BaseSubscriber };