@signaltree/events 7.3.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.
- package/angular.d.ts +1 -0
- package/angular.esm.js +547 -0
- package/factory.esm.js +178 -0
- package/idempotency.esm.js +701 -0
- package/index.d.ts +1 -0
- package/index.esm.js +167 -0
- package/nestjs.d.ts +1 -0
- package/nestjs.esm.js +944 -0
- package/package.json +110 -0
- package/src/angular/handlers.d.ts +132 -0
- package/src/angular/index.d.ts +12 -0
- package/src/angular/optimistic-updates.d.ts +117 -0
- package/src/angular/websocket.service.d.ts +158 -0
- package/src/angular.d.ts +7 -0
- package/src/core/error-classification.d.ts +100 -0
- package/src/core/factory.d.ts +114 -0
- package/src/core/idempotency.d.ts +209 -0
- package/src/core/registry.d.ts +147 -0
- package/src/core/types.d.ts +127 -0
- package/src/core/validation.d.ts +619 -0
- package/src/index.d.ts +56 -0
- package/src/nestjs/base.subscriber.d.ts +169 -0
- package/src/nestjs/decorators.d.ts +37 -0
- package/src/nestjs/dlq.service.d.ts +117 -0
- package/src/nestjs/event-bus.module.d.ts +117 -0
- package/src/nestjs/event-bus.service.d.ts +114 -0
- package/src/nestjs/index.d.ts +16 -0
- package/src/nestjs/tokens.d.ts +8 -0
- package/src/nestjs.d.ts +7 -0
- package/src/testing/assertions.d.ts +113 -0
- package/src/testing/factories.d.ts +106 -0
- package/src/testing/helpers.d.ts +104 -0
- package/src/testing/index.d.ts +13 -0
- package/src/testing/mock-event-bus.d.ts +144 -0
- package/src/testing.d.ts +7 -0
- package/testing.d.ts +1 -0
- package/testing.esm.js +743 -0
package/nestjs.esm.js
ADDED
|
@@ -0,0 +1,944 @@
|
|
|
1
|
+
import { __decorate, __param, __metadata } from 'tslib';
|
|
2
|
+
import { Injectable, Inject, Logger, Module, SetMetadata } from '@nestjs/common';
|
|
3
|
+
import { Queue, QueueEvents, Worker } from 'bullmq';
|
|
4
|
+
import { g as generateEventId, b as generateCorrelationId } from './factory.esm.js';
|
|
5
|
+
import { E as EventRegistry, c as createEventRegistry, e as createInMemoryIdempotencyStore, b as createErrorClassifier } from './idempotency.esm.js';
|
|
6
|
+
import 'reflect-metadata';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Injection tokens for the EventBus module
|
|
10
|
+
*/
|
|
11
|
+
const EVENT_BUS_CONFIG = Symbol('EVENT_BUS_CONFIG');
|
|
12
|
+
const EVENT_REGISTRY = Symbol('EVENT_REGISTRY');
|
|
13
|
+
const IDEMPOTENCY_STORE = Symbol('IDEMPOTENCY_STORE');
|
|
14
|
+
const ERROR_CLASSIFIER = Symbol('ERROR_CLASSIFIER');
|
|
15
|
+
|
|
16
|
+
var DlqService_1;
|
|
17
|
+
let DlqService = DlqService_1 = class DlqService {
|
|
18
|
+
config;
|
|
19
|
+
logger = new Logger(DlqService_1.name);
|
|
20
|
+
queue;
|
|
21
|
+
queueName;
|
|
22
|
+
constructor(config) {
|
|
23
|
+
this.config = config;
|
|
24
|
+
this.queueName = config.dlqQueueName ?? 'dead-letter';
|
|
25
|
+
}
|
|
26
|
+
async onModuleInit() {
|
|
27
|
+
if (!this.config.enableDlq) {
|
|
28
|
+
this.logger.log('DLQ is disabled');
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
this.queue = new Queue(this.queueName, {
|
|
32
|
+
connection: {
|
|
33
|
+
host: this.config.redis.host,
|
|
34
|
+
port: this.config.redis.port,
|
|
35
|
+
password: this.config.redis.password,
|
|
36
|
+
db: this.config.redis.db
|
|
37
|
+
},
|
|
38
|
+
defaultJobOptions: {
|
|
39
|
+
removeOnComplete: false,
|
|
40
|
+
// Keep for inspection
|
|
41
|
+
removeOnFail: false,
|
|
42
|
+
attempts: 1 // No retries in DLQ
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
this.logger.log(`DLQ "${this.queueName}" initialized`);
|
|
46
|
+
}
|
|
47
|
+
async onModuleDestroy() {
|
|
48
|
+
if (this.queue) {
|
|
49
|
+
await this.queue.close();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Send an event to the DLQ
|
|
54
|
+
*/
|
|
55
|
+
async send(entry) {
|
|
56
|
+
if (!this.queue) {
|
|
57
|
+
throw new Error('DLQ is not initialized');
|
|
58
|
+
}
|
|
59
|
+
const job = await this.queue.add(`dlq:${entry.event.type}`, entry, {
|
|
60
|
+
jobId: `dlq:${entry.event.id}:${entry.subscriber}`
|
|
61
|
+
});
|
|
62
|
+
this.logger.debug(`Event ${entry.event.id} sent to DLQ (job: ${job.id})`);
|
|
63
|
+
return job.id ?? `dlq:${entry.event.id}:${entry.subscriber}`;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Get entries from DLQ
|
|
67
|
+
*/
|
|
68
|
+
async getEntries(options = {}) {
|
|
69
|
+
if (!this.queue) {
|
|
70
|
+
return [];
|
|
71
|
+
}
|
|
72
|
+
const start = options.start ?? 0;
|
|
73
|
+
const end = options.end ?? 100;
|
|
74
|
+
const jobs = await this.queue.getJobs(['waiting', 'active', 'delayed', 'failed'], start, end);
|
|
75
|
+
let entries = jobs.map(job => job.data);
|
|
76
|
+
// Apply filters
|
|
77
|
+
if (options.eventType) {
|
|
78
|
+
entries = entries.filter(e => e.event.type === options.eventType);
|
|
79
|
+
}
|
|
80
|
+
if (options.subscriber) {
|
|
81
|
+
entries = entries.filter(e => e.subscriber === options.subscriber);
|
|
82
|
+
}
|
|
83
|
+
if (options.classification) {
|
|
84
|
+
entries = entries.filter(e => e.error.classification === options.classification);
|
|
85
|
+
}
|
|
86
|
+
if (options.from) {
|
|
87
|
+
const fromDate = options.from;
|
|
88
|
+
entries = entries.filter(e => new Date(e.failedAt) >= fromDate);
|
|
89
|
+
}
|
|
90
|
+
if (options.to) {
|
|
91
|
+
const toDate = options.to;
|
|
92
|
+
entries = entries.filter(e => new Date(e.failedAt) <= toDate);
|
|
93
|
+
}
|
|
94
|
+
return entries;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Get a specific entry by event ID
|
|
98
|
+
*/
|
|
99
|
+
async getEntry(eventId, subscriber) {
|
|
100
|
+
if (!this.queue) {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
// Try with subscriber suffix first
|
|
104
|
+
if (subscriber) {
|
|
105
|
+
const job = await this.queue.getJob(`dlq:${eventId}:${subscriber}`);
|
|
106
|
+
if (job) {
|
|
107
|
+
return job.data;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// Search through jobs
|
|
111
|
+
const jobs = await this.queue.getJobs(['waiting', 'active', 'delayed', 'failed']);
|
|
112
|
+
const job = jobs.find(j => j.data.event.id === eventId);
|
|
113
|
+
return job ? job.data : null;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Get DLQ statistics
|
|
117
|
+
*/
|
|
118
|
+
async getStats() {
|
|
119
|
+
const entries = await this.getEntries({
|
|
120
|
+
start: 0,
|
|
121
|
+
end: 10000
|
|
122
|
+
});
|
|
123
|
+
const stats = {
|
|
124
|
+
total: entries.length,
|
|
125
|
+
byEventType: {},
|
|
126
|
+
bySubscriber: {},
|
|
127
|
+
byClassification: {
|
|
128
|
+
transient: 0,
|
|
129
|
+
permanent: 0,
|
|
130
|
+
poison: 0,
|
|
131
|
+
unknown: 0
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
for (const entry of entries) {
|
|
135
|
+
// By event type
|
|
136
|
+
stats.byEventType[entry.event.type] = (stats.byEventType[entry.event.type] ?? 0) + 1;
|
|
137
|
+
// By subscriber
|
|
138
|
+
stats.bySubscriber[entry.subscriber] = (stats.bySubscriber[entry.subscriber] ?? 0) + 1;
|
|
139
|
+
// By classification
|
|
140
|
+
stats.byClassification[entry.error.classification]++;
|
|
141
|
+
// Date tracking
|
|
142
|
+
const failedDate = new Date(entry.failedAt);
|
|
143
|
+
if (!stats.oldestEntry || failedDate < stats.oldestEntry) {
|
|
144
|
+
stats.oldestEntry = failedDate;
|
|
145
|
+
}
|
|
146
|
+
if (!stats.newestEntry || failedDate > stats.newestEntry) {
|
|
147
|
+
stats.newestEntry = failedDate;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return stats;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Replay an event from DLQ
|
|
154
|
+
*
|
|
155
|
+
* This removes the event from DLQ and republishes to original queue
|
|
156
|
+
*/
|
|
157
|
+
async replay(eventId, subscriber, targetQueue) {
|
|
158
|
+
if (!this.queue) {
|
|
159
|
+
throw new Error('DLQ is not initialized');
|
|
160
|
+
}
|
|
161
|
+
const jobId = `dlq:${eventId}:${subscriber}`;
|
|
162
|
+
const job = await this.queue.getJob(jobId);
|
|
163
|
+
if (!job) {
|
|
164
|
+
this.logger.warn(`DLQ entry not found: ${jobId}`);
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
const entry = job.data;
|
|
168
|
+
// Get target queue
|
|
169
|
+
const targetQueueInstance = new Queue(targetQueue, {
|
|
170
|
+
connection: {
|
|
171
|
+
host: this.config.redis.host,
|
|
172
|
+
port: this.config.redis.port,
|
|
173
|
+
password: this.config.redis.password,
|
|
174
|
+
db: this.config.redis.db
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
try {
|
|
178
|
+
// Republish to original queue
|
|
179
|
+
await targetQueueInstance.add(entry.event.type, entry.event, {
|
|
180
|
+
jobId: `replay:${entry.event.id}`
|
|
181
|
+
});
|
|
182
|
+
// Remove from DLQ
|
|
183
|
+
await job.remove();
|
|
184
|
+
this.logger.log(`Replayed event ${eventId} to queue ${targetQueue}`);
|
|
185
|
+
return true;
|
|
186
|
+
} finally {
|
|
187
|
+
await targetQueueInstance.close();
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Replay all events matching criteria
|
|
192
|
+
*/
|
|
193
|
+
async replayBatch(options) {
|
|
194
|
+
const entries = await this.getEntries(options);
|
|
195
|
+
let replayed = 0;
|
|
196
|
+
let failed = 0;
|
|
197
|
+
for (const entry of entries) {
|
|
198
|
+
try {
|
|
199
|
+
const success = await this.replay(entry.event.id, entry.subscriber, options.targetQueue);
|
|
200
|
+
if (success) replayed++;else failed++;
|
|
201
|
+
} catch {
|
|
202
|
+
failed++;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return {
|
|
206
|
+
replayed,
|
|
207
|
+
failed
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Remove an entry from DLQ
|
|
212
|
+
*/
|
|
213
|
+
async remove(eventId, subscriber) {
|
|
214
|
+
if (!this.queue) {
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
const job = await this.queue.getJob(`dlq:${eventId}:${subscriber}`);
|
|
218
|
+
if (job) {
|
|
219
|
+
await job.remove();
|
|
220
|
+
return true;
|
|
221
|
+
}
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Purge old entries from DLQ
|
|
226
|
+
*/
|
|
227
|
+
async purge(olderThan) {
|
|
228
|
+
const entries = await this.getEntries({
|
|
229
|
+
to: olderThan,
|
|
230
|
+
start: 0,
|
|
231
|
+
end: 10000
|
|
232
|
+
});
|
|
233
|
+
let purged = 0;
|
|
234
|
+
for (const entry of entries) {
|
|
235
|
+
const removed = await this.remove(entry.event.id, entry.subscriber);
|
|
236
|
+
if (removed) purged++;
|
|
237
|
+
}
|
|
238
|
+
this.logger.log(`Purged ${purged} entries from DLQ older than ${olderThan.toISOString()}`);
|
|
239
|
+
return purged;
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Clear all entries from DLQ
|
|
243
|
+
*/
|
|
244
|
+
async clear() {
|
|
245
|
+
if (!this.queue) {
|
|
246
|
+
return 0;
|
|
247
|
+
}
|
|
248
|
+
const count = await this.queue.getJobCounts();
|
|
249
|
+
const total = count.waiting + count.active + count.delayed + count.failed;
|
|
250
|
+
await this.queue.obliterate({
|
|
251
|
+
force: true
|
|
252
|
+
});
|
|
253
|
+
this.logger.warn(`Cleared all ${total} entries from DLQ`);
|
|
254
|
+
return total;
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
DlqService = DlqService_1 = __decorate([Injectable(), __param(0, Inject(EVENT_BUS_CONFIG)), __metadata("design:paramtypes", [Object])], DlqService);
|
|
258
|
+
|
|
259
|
+
var EventBusService_1;
|
|
260
|
+
let EventBusService = EventBusService_1 = class EventBusService {
|
|
261
|
+
config;
|
|
262
|
+
registry;
|
|
263
|
+
logger = new Logger(EventBusService_1.name);
|
|
264
|
+
queues = new Map();
|
|
265
|
+
priorityToQueue = new Map();
|
|
266
|
+
connection;
|
|
267
|
+
isReady = false;
|
|
268
|
+
constructor(config, registry) {
|
|
269
|
+
this.config = config;
|
|
270
|
+
this.registry = registry;
|
|
271
|
+
this.connection = {
|
|
272
|
+
host: config.redis.host,
|
|
273
|
+
port: config.redis.port,
|
|
274
|
+
password: config.redis.password,
|
|
275
|
+
db: config.redis.db,
|
|
276
|
+
maxRetriesPerRequest: config.redis.maxRetriesPerRequest ?? 3
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
async onModuleInit() {
|
|
280
|
+
this.logger.log('Initializing EventBus queues...');
|
|
281
|
+
// Create queues
|
|
282
|
+
for (const queueConfig of this.config.queues ?? []) {
|
|
283
|
+
const queue = new Queue(queueConfig.name, {
|
|
284
|
+
connection: this.connection,
|
|
285
|
+
defaultJobOptions: {
|
|
286
|
+
removeOnComplete: 1000,
|
|
287
|
+
// Keep last 1000 completed jobs
|
|
288
|
+
removeOnFail: 5000,
|
|
289
|
+
// Keep last 5000 failed jobs
|
|
290
|
+
attempts: 5,
|
|
291
|
+
backoff: {
|
|
292
|
+
type: 'exponential',
|
|
293
|
+
delay: 1000
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
// Map priorities to this queue
|
|
298
|
+
for (const priority of queueConfig.priorities) {
|
|
299
|
+
this.priorityToQueue.set(priority, queueConfig.name);
|
|
300
|
+
}
|
|
301
|
+
const instance = {
|
|
302
|
+
config: queueConfig,
|
|
303
|
+
queue
|
|
304
|
+
};
|
|
305
|
+
// Optionally create queue events listener for monitoring
|
|
306
|
+
if (this.config.enableMetrics) {
|
|
307
|
+
instance.events = new QueueEvents(queueConfig.name, {
|
|
308
|
+
connection: this.connection
|
|
309
|
+
});
|
|
310
|
+
this.setupQueueEventListeners(instance);
|
|
311
|
+
}
|
|
312
|
+
this.queues.set(queueConfig.name, instance);
|
|
313
|
+
this.logger.log(`Queue "${queueConfig.name}" initialized for priorities: ${queueConfig.priorities.join(', ')}`);
|
|
314
|
+
}
|
|
315
|
+
this.isReady = true;
|
|
316
|
+
this.logger.log(`EventBus ready with ${this.queues.size} queues`);
|
|
317
|
+
}
|
|
318
|
+
async onModuleDestroy() {
|
|
319
|
+
this.logger.log('Shutting down EventBus...');
|
|
320
|
+
const closePromises = [];
|
|
321
|
+
for (const [name, instance] of this.queues) {
|
|
322
|
+
this.logger.debug(`Closing queue "${name}"...`);
|
|
323
|
+
closePromises.push(instance.queue.close());
|
|
324
|
+
if (instance.events) {
|
|
325
|
+
closePromises.push(instance.events.close());
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
await Promise.all(closePromises);
|
|
329
|
+
this.queues.clear();
|
|
330
|
+
this.isReady = false;
|
|
331
|
+
this.logger.log('EventBus shutdown complete');
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Publish an event
|
|
335
|
+
*
|
|
336
|
+
* @example
|
|
337
|
+
* ```typescript
|
|
338
|
+
* await eventBus.publish({
|
|
339
|
+
* type: 'TradeProposalCreated',
|
|
340
|
+
* data: {
|
|
341
|
+
* tradeId: '123',
|
|
342
|
+
* initiatorId: 'user-1',
|
|
343
|
+
* recipientId: 'user-2',
|
|
344
|
+
* },
|
|
345
|
+
* });
|
|
346
|
+
* ```
|
|
347
|
+
*/
|
|
348
|
+
async publish(event, options = {}) {
|
|
349
|
+
if (!this.isReady) {
|
|
350
|
+
throw new Error('EventBus is not ready. Wait for module initialization.');
|
|
351
|
+
}
|
|
352
|
+
// Build complete event
|
|
353
|
+
const eventId = options.id ?? event.id ?? generateEventId();
|
|
354
|
+
const correlationId = options.correlationId ?? event.correlationId ?? generateCorrelationId();
|
|
355
|
+
const timestamp = event.timestamp ?? new Date().toISOString();
|
|
356
|
+
const fullEvent = {
|
|
357
|
+
...event,
|
|
358
|
+
id: eventId,
|
|
359
|
+
correlationId,
|
|
360
|
+
causationId: options.causationId ?? event.causationId,
|
|
361
|
+
timestamp,
|
|
362
|
+
priority: options.priority ?? event.priority ?? 'normal'
|
|
363
|
+
};
|
|
364
|
+
// Validate event against registry
|
|
365
|
+
const validated = this.registry.validate(fullEvent);
|
|
366
|
+
// Determine queue based on priority
|
|
367
|
+
const priority = validated.priority ?? 'normal';
|
|
368
|
+
const queueName = options.queue ?? this.getQueueForPriority(priority);
|
|
369
|
+
const instance = this.queues.get(queueName);
|
|
370
|
+
if (!instance) {
|
|
371
|
+
throw new Error(`Queue "${queueName}" not found. Available: ${Array.from(this.queues.keys()).join(', ')}`);
|
|
372
|
+
}
|
|
373
|
+
// Publish to BullMQ
|
|
374
|
+
const jobId = options.jobId ?? eventId;
|
|
375
|
+
const job = await instance.queue.add(validated.type,
|
|
376
|
+
// Job name = event type
|
|
377
|
+
validated, {
|
|
378
|
+
jobId,
|
|
379
|
+
delay: options.delay,
|
|
380
|
+
priority: this.getPriorityNumber(priority),
|
|
381
|
+
...options.jobOptions
|
|
382
|
+
});
|
|
383
|
+
this.logger.debug(`Published event ${validated.type}:${eventId} to queue ${queueName} (job: ${job.id})`);
|
|
384
|
+
return {
|
|
385
|
+
eventId,
|
|
386
|
+
jobId: job.id ?? eventId,
|
|
387
|
+
queue: queueName,
|
|
388
|
+
correlationId
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Publish multiple events in a batch
|
|
393
|
+
*/
|
|
394
|
+
async publishBatch(events, options = {}) {
|
|
395
|
+
// Use same correlation ID for all events in batch
|
|
396
|
+
const correlationId = options.correlationId ?? generateCorrelationId();
|
|
397
|
+
const results = await Promise.all(events.map((event, index) => this.publish(event, {
|
|
398
|
+
...options,
|
|
399
|
+
correlationId,
|
|
400
|
+
causationId: index > 0 ? events[index - 1].id : options.causationId
|
|
401
|
+
})));
|
|
402
|
+
return results;
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Get queue for a given priority
|
|
406
|
+
*/
|
|
407
|
+
getQueueForPriority(priority) {
|
|
408
|
+
const queueName = this.priorityToQueue.get(priority);
|
|
409
|
+
if (!queueName) {
|
|
410
|
+
// Fall back to normal queue
|
|
411
|
+
return this.priorityToQueue.get('normal') ?? 'events-normal';
|
|
412
|
+
}
|
|
413
|
+
return queueName;
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* Get queue stats
|
|
417
|
+
*/
|
|
418
|
+
async getQueueStats(queueName) {
|
|
419
|
+
const instance = this.queues.get(queueName);
|
|
420
|
+
if (!instance) {
|
|
421
|
+
throw new Error(`Queue "${queueName}" not found`);
|
|
422
|
+
}
|
|
423
|
+
const [waiting, active, completed, failed, delayed] = await Promise.all([instance.queue.getWaitingCount(), instance.queue.getActiveCount(), instance.queue.getCompletedCount(), instance.queue.getFailedCount(), instance.queue.getDelayedCount()]);
|
|
424
|
+
return {
|
|
425
|
+
waiting,
|
|
426
|
+
active,
|
|
427
|
+
completed,
|
|
428
|
+
failed,
|
|
429
|
+
delayed
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Get all queue names
|
|
434
|
+
*/
|
|
435
|
+
getQueueNames() {
|
|
436
|
+
return Array.from(this.queues.keys());
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Get underlying BullMQ queue for advanced operations
|
|
440
|
+
*/
|
|
441
|
+
getQueue(name) {
|
|
442
|
+
return this.queues.get(name)?.queue;
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* Check if service is ready
|
|
446
|
+
*/
|
|
447
|
+
isServiceReady() {
|
|
448
|
+
return this.isReady;
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* Convert priority string to number for BullMQ (lower = higher priority)
|
|
452
|
+
*/
|
|
453
|
+
getPriorityNumber(priority) {
|
|
454
|
+
switch (priority) {
|
|
455
|
+
case 'critical':
|
|
456
|
+
return 1;
|
|
457
|
+
case 'high':
|
|
458
|
+
return 2;
|
|
459
|
+
case 'normal':
|
|
460
|
+
return 3;
|
|
461
|
+
case 'low':
|
|
462
|
+
return 4;
|
|
463
|
+
case 'bulk':
|
|
464
|
+
return 5;
|
|
465
|
+
default:
|
|
466
|
+
return 3;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* Setup event listeners for monitoring
|
|
471
|
+
*/
|
|
472
|
+
setupQueueEventListeners(instance) {
|
|
473
|
+
if (!instance.events) return;
|
|
474
|
+
instance.events.on('completed', ({
|
|
475
|
+
jobId
|
|
476
|
+
}) => {
|
|
477
|
+
this.logger.debug(`Job ${jobId} completed in queue ${instance.config.name}`);
|
|
478
|
+
});
|
|
479
|
+
instance.events.on('failed', ({
|
|
480
|
+
jobId,
|
|
481
|
+
failedReason
|
|
482
|
+
}) => {
|
|
483
|
+
this.logger.warn(`Job ${jobId} failed in queue ${instance.config.name}: ${failedReason}`);
|
|
484
|
+
});
|
|
485
|
+
instance.events.on('stalled', ({
|
|
486
|
+
jobId
|
|
487
|
+
}) => {
|
|
488
|
+
this.logger.warn(`Job ${jobId} stalled in queue ${instance.config.name}`);
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
};
|
|
492
|
+
EventBusService = EventBusService_1 = __decorate([Injectable(), __param(0, Inject(EVENT_BUS_CONFIG)), __param(1, Inject(EVENT_REGISTRY)), __metadata("design:paramtypes", [Object, EventRegistry])], EventBusService);
|
|
493
|
+
|
|
494
|
+
var EventBusModule_1;
|
|
495
|
+
/**
|
|
496
|
+
* Default queue configuration based on priorities
|
|
497
|
+
*/
|
|
498
|
+
const DEFAULT_QUEUES = [{
|
|
499
|
+
name: 'events-critical',
|
|
500
|
+
priorities: ['critical'],
|
|
501
|
+
concurrency: 10
|
|
502
|
+
}, {
|
|
503
|
+
name: 'events-high',
|
|
504
|
+
priorities: ['high'],
|
|
505
|
+
concurrency: 8
|
|
506
|
+
}, {
|
|
507
|
+
name: 'events-normal',
|
|
508
|
+
priorities: ['normal'],
|
|
509
|
+
concurrency: 5
|
|
510
|
+
}, {
|
|
511
|
+
name: 'events-low',
|
|
512
|
+
priorities: ['low'],
|
|
513
|
+
concurrency: 3
|
|
514
|
+
}, {
|
|
515
|
+
name: 'events-bulk',
|
|
516
|
+
priorities: ['bulk'],
|
|
517
|
+
concurrency: 2,
|
|
518
|
+
rateLimit: {
|
|
519
|
+
max: 100,
|
|
520
|
+
duration: 1000
|
|
521
|
+
}
|
|
522
|
+
}];
|
|
523
|
+
let EventBusModule = EventBusModule_1 = class EventBusModule {
|
|
524
|
+
/**
|
|
525
|
+
* Register the EventBus module with configuration
|
|
526
|
+
*
|
|
527
|
+
* @example
|
|
528
|
+
* ```typescript
|
|
529
|
+
* @Module({
|
|
530
|
+
* imports: [
|
|
531
|
+
* EventBusModule.forRoot({
|
|
532
|
+
* redis: { host: 'localhost', port: 6379 },
|
|
533
|
+
* queues: [
|
|
534
|
+
* { name: 'critical', priorities: ['critical'], concurrency: 10 },
|
|
535
|
+
* { name: 'normal', priorities: ['high', 'normal'], concurrency: 5 },
|
|
536
|
+
* ],
|
|
537
|
+
* }),
|
|
538
|
+
* ],
|
|
539
|
+
* })
|
|
540
|
+
* export class AppModule {}
|
|
541
|
+
* ```
|
|
542
|
+
*/
|
|
543
|
+
static forRoot(config) {
|
|
544
|
+
const providers = EventBusModule_1.createProviders(config);
|
|
545
|
+
return {
|
|
546
|
+
module: EventBusModule_1,
|
|
547
|
+
global: true,
|
|
548
|
+
providers,
|
|
549
|
+
exports: [EventBusService, DlqService, EVENT_BUS_CONFIG, EVENT_REGISTRY, IDEMPOTENCY_STORE, ERROR_CLASSIFIER]
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
/**
|
|
553
|
+
* Register the EventBus module with async configuration
|
|
554
|
+
*
|
|
555
|
+
* @example
|
|
556
|
+
* ```typescript
|
|
557
|
+
* @Module({
|
|
558
|
+
* imports: [
|
|
559
|
+
* EventBusModule.forRootAsync({
|
|
560
|
+
* imports: [ConfigModule],
|
|
561
|
+
* useFactory: (configService: ConfigService) => ({
|
|
562
|
+
* redis: {
|
|
563
|
+
* host: configService.get('REDIS_HOST'),
|
|
564
|
+
* port: configService.get('REDIS_PORT'),
|
|
565
|
+
* },
|
|
566
|
+
* }),
|
|
567
|
+
* inject: [ConfigService],
|
|
568
|
+
* }),
|
|
569
|
+
* ],
|
|
570
|
+
* })
|
|
571
|
+
* export class AppModule {}
|
|
572
|
+
* ```
|
|
573
|
+
*/
|
|
574
|
+
static forRootAsync(asyncConfig) {
|
|
575
|
+
const configProvider = {
|
|
576
|
+
provide: EVENT_BUS_CONFIG,
|
|
577
|
+
useFactory: asyncConfig.useFactory,
|
|
578
|
+
inject: asyncConfig.inject ?? []
|
|
579
|
+
};
|
|
580
|
+
const registryProvider = {
|
|
581
|
+
provide: EVENT_REGISTRY,
|
|
582
|
+
useFactory: config => {
|
|
583
|
+
return createEventRegistry(config.registry);
|
|
584
|
+
},
|
|
585
|
+
inject: [EVENT_BUS_CONFIG]
|
|
586
|
+
};
|
|
587
|
+
const idempotencyProvider = {
|
|
588
|
+
provide: IDEMPOTENCY_STORE,
|
|
589
|
+
useFactory: config => {
|
|
590
|
+
return config.idempotencyStore ?? createInMemoryIdempotencyStore();
|
|
591
|
+
},
|
|
592
|
+
inject: [EVENT_BUS_CONFIG]
|
|
593
|
+
};
|
|
594
|
+
const errorClassifierProvider = {
|
|
595
|
+
provide: ERROR_CLASSIFIER,
|
|
596
|
+
useFactory: config => {
|
|
597
|
+
return createErrorClassifier(config.errorClassifier);
|
|
598
|
+
},
|
|
599
|
+
inject: [EVENT_BUS_CONFIG]
|
|
600
|
+
};
|
|
601
|
+
return {
|
|
602
|
+
module: EventBusModule_1,
|
|
603
|
+
global: true,
|
|
604
|
+
imports: asyncConfig.imports ?? [],
|
|
605
|
+
providers: [configProvider, registryProvider, idempotencyProvider, errorClassifierProvider, EventBusService, DlqService],
|
|
606
|
+
exports: [EventBusService, DlqService, EVENT_BUS_CONFIG, EVENT_REGISTRY, IDEMPOTENCY_STORE, ERROR_CLASSIFIER]
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
static createProviders(config) {
|
|
610
|
+
const queues = config.queues ?? DEFAULT_QUEUES;
|
|
611
|
+
const fullConfig = {
|
|
612
|
+
...config,
|
|
613
|
+
queues,
|
|
614
|
+
enableDlq: config.enableDlq ?? true,
|
|
615
|
+
dlqQueueName: config.dlqQueueName ?? 'dead-letter',
|
|
616
|
+
enableMetrics: config.enableMetrics ?? true,
|
|
617
|
+
metricsPrefix: config.metricsPrefix ?? 'signaltree_events'
|
|
618
|
+
};
|
|
619
|
+
return [{
|
|
620
|
+
provide: EVENT_BUS_CONFIG,
|
|
621
|
+
useValue: fullConfig
|
|
622
|
+
}, {
|
|
623
|
+
provide: EVENT_REGISTRY,
|
|
624
|
+
useFactory: () => createEventRegistry(config.registry)
|
|
625
|
+
}, {
|
|
626
|
+
provide: IDEMPOTENCY_STORE,
|
|
627
|
+
useValue: config.idempotencyStore ?? createInMemoryIdempotencyStore()
|
|
628
|
+
}, {
|
|
629
|
+
provide: ERROR_CLASSIFIER,
|
|
630
|
+
useFactory: () => createErrorClassifier(config.errorClassifier)
|
|
631
|
+
}, EventBusService, DlqService];
|
|
632
|
+
}
|
|
633
|
+
};
|
|
634
|
+
EventBusModule = EventBusModule_1 = __decorate([Module({})], EventBusModule);
|
|
635
|
+
|
|
636
|
+
/**
|
|
637
|
+
* Base class for event subscribers
|
|
638
|
+
*
|
|
639
|
+
* @example
|
|
640
|
+
* ```typescript
|
|
641
|
+
* @Injectable()
|
|
642
|
+
* export class TradeSubscriber extends BaseSubscriber {
|
|
643
|
+
* protected readonly config: SubscriberConfig = {
|
|
644
|
+
* name: 'trade-subscriber',
|
|
645
|
+
* eventTypes: ['TradeProposalCreated', 'TradeAccepted'],
|
|
646
|
+
* priority: 'high',
|
|
647
|
+
* concurrency: 5,
|
|
648
|
+
* };
|
|
649
|
+
*
|
|
650
|
+
* async handle(event: TradeProposalCreated | TradeAccepted): Promise<ProcessingResult> {
|
|
651
|
+
* switch (event.type) {
|
|
652
|
+
* case 'TradeProposalCreated':
|
|
653
|
+
* await this.handleTradeCreated(event);
|
|
654
|
+
* break;
|
|
655
|
+
* case 'TradeAccepted':
|
|
656
|
+
* await this.handleTradeAccepted(event);
|
|
657
|
+
* break;
|
|
658
|
+
* }
|
|
659
|
+
* return { success: true };
|
|
660
|
+
* }
|
|
661
|
+
* }
|
|
662
|
+
* ```
|
|
663
|
+
*/
|
|
664
|
+
let BaseSubscriber = class BaseSubscriber {
|
|
665
|
+
busConfig;
|
|
666
|
+
idempotencyStore;
|
|
667
|
+
errorClassifier;
|
|
668
|
+
dlqService;
|
|
669
|
+
logger;
|
|
670
|
+
worker;
|
|
671
|
+
connection;
|
|
672
|
+
metrics = {
|
|
673
|
+
processed: 0,
|
|
674
|
+
succeeded: 0,
|
|
675
|
+
failed: 0,
|
|
676
|
+
retried: 0,
|
|
677
|
+
dlqSent: 0,
|
|
678
|
+
duplicatesSkipped: 0,
|
|
679
|
+
avgProcessingTimeMs: 0
|
|
680
|
+
};
|
|
681
|
+
constructor(busConfig, idempotencyStore, errorClassifier, dlqService) {
|
|
682
|
+
this.busConfig = busConfig;
|
|
683
|
+
this.idempotencyStore = idempotencyStore;
|
|
684
|
+
this.errorClassifier = errorClassifier;
|
|
685
|
+
this.dlqService = dlqService;
|
|
686
|
+
this.logger = new Logger(this.constructor.name);
|
|
687
|
+
this.connection = {
|
|
688
|
+
host: busConfig.redis.host,
|
|
689
|
+
port: busConfig.redis.port,
|
|
690
|
+
password: busConfig.redis.password,
|
|
691
|
+
db: busConfig.redis.db
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
/**
|
|
695
|
+
* Called before processing starts (for setup/validation)
|
|
696
|
+
*/
|
|
697
|
+
async beforeProcess(
|
|
698
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
699
|
+
_event) {
|
|
700
|
+
// Override in subclass if needed
|
|
701
|
+
}
|
|
702
|
+
/**
|
|
703
|
+
* Called after processing completes (for cleanup)
|
|
704
|
+
*/
|
|
705
|
+
async afterProcess(
|
|
706
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
707
|
+
_event,
|
|
708
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
709
|
+
_result) {
|
|
710
|
+
// Override in subclass if needed
|
|
711
|
+
}
|
|
712
|
+
async onModuleInit() {
|
|
713
|
+
const queueName = this.getQueueName();
|
|
714
|
+
this.logger.log(`Initializing subscriber "${this.config.name}" on queue "${queueName}" ` + `for events: ${this.config.eventTypes.join(', ')}`);
|
|
715
|
+
this.worker = new Worker(queueName, async job => this.processJob(job), {
|
|
716
|
+
connection: this.connection,
|
|
717
|
+
concurrency: this.config.concurrency ?? 5,
|
|
718
|
+
lockDuration: this.config.lockDuration ?? 30000,
|
|
719
|
+
stalledInterval: this.config.stalledInterval ?? 30000
|
|
720
|
+
});
|
|
721
|
+
this.setupWorkerListeners();
|
|
722
|
+
this.logger.log(`Subscriber "${this.config.name}" started`);
|
|
723
|
+
}
|
|
724
|
+
async onModuleDestroy() {
|
|
725
|
+
this.logger.log(`Shutting down subscriber "${this.config.name}"...`);
|
|
726
|
+
if (this.worker) {
|
|
727
|
+
await this.worker.close();
|
|
728
|
+
}
|
|
729
|
+
this.logger.log(`Subscriber "${this.config.name}" stopped`);
|
|
730
|
+
}
|
|
731
|
+
/**
|
|
732
|
+
* Get queue name based on config
|
|
733
|
+
*/
|
|
734
|
+
getQueueName() {
|
|
735
|
+
if (this.config.queue) {
|
|
736
|
+
return this.config.queue;
|
|
737
|
+
}
|
|
738
|
+
// Find queue for priority
|
|
739
|
+
const priority = this.config.priority ?? 'normal';
|
|
740
|
+
const queueConfig = this.busConfig.queues?.find(q => q.priorities.includes(priority));
|
|
741
|
+
return queueConfig?.name ?? `events-${priority}`;
|
|
742
|
+
}
|
|
743
|
+
/**
|
|
744
|
+
* Process a job with idempotency and error handling
|
|
745
|
+
*/
|
|
746
|
+
async processJob(job) {
|
|
747
|
+
const event = job.data;
|
|
748
|
+
const startTime = Date.now();
|
|
749
|
+
// Filter by event type
|
|
750
|
+
if (!this.config.eventTypes.includes(event.type)) {
|
|
751
|
+
// Not our event type, skip silently
|
|
752
|
+
return {
|
|
753
|
+
skipped: true,
|
|
754
|
+
reason: 'event_type_not_handled'
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
this.logger.debug(`Processing event ${event.type}:${event.id} (attempt ${job.attemptsMade + 1})`);
|
|
758
|
+
try {
|
|
759
|
+
// Check idempotency
|
|
760
|
+
if (this.config.idempotency !== false) {
|
|
761
|
+
const idempotencyResult = await this.checkIdempotency(event);
|
|
762
|
+
if (idempotencyResult.isDuplicate) {
|
|
763
|
+
this.metrics.duplicatesSkipped++;
|
|
764
|
+
this.logger.debug(`Skipping duplicate event ${event.id}`);
|
|
765
|
+
return idempotencyResult.result ?? {
|
|
766
|
+
skipped: true,
|
|
767
|
+
reason: 'duplicate'
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
// Before hook
|
|
772
|
+
await this.beforeProcess(event);
|
|
773
|
+
// Process event
|
|
774
|
+
const result = await this.handle(event);
|
|
775
|
+
// After hook
|
|
776
|
+
await this.afterProcess(event, result);
|
|
777
|
+
// Update idempotency store
|
|
778
|
+
if (this.config.idempotency !== false) {
|
|
779
|
+
await this.idempotencyStore.markCompleted(event, this.config.name, result.result);
|
|
780
|
+
}
|
|
781
|
+
// Update metrics
|
|
782
|
+
this.updateMetrics(startTime, true);
|
|
783
|
+
return result.result;
|
|
784
|
+
} catch (error) {
|
|
785
|
+
return this.handleError(job, event, error, startTime);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
/**
|
|
789
|
+
* Handle processing error
|
|
790
|
+
*/
|
|
791
|
+
async handleError(job, event, error, startTime) {
|
|
792
|
+
const errorObj = error instanceof Error ? error : new Error(String(error));
|
|
793
|
+
const classification = this.errorClassifier.classify(error);
|
|
794
|
+
this.logger.error(`Error processing event ${event.type}:${event.id}: ${errorObj.message}`, errorObj.stack);
|
|
795
|
+
// Update idempotency store with failure
|
|
796
|
+
if (this.config.idempotency !== false) {
|
|
797
|
+
await this.idempotencyStore.markFailed(event, this.config.name, error);
|
|
798
|
+
}
|
|
799
|
+
// Check if we should retry
|
|
800
|
+
const maxAttempts = classification.retryConfig?.maxAttempts ?? 5;
|
|
801
|
+
const shouldRetry = job.attemptsMade < maxAttempts && classification.classification !== 'poison';
|
|
802
|
+
if (shouldRetry) {
|
|
803
|
+
this.metrics.retried++;
|
|
804
|
+
this.logger.debug(`Will retry event ${event.id}, attempt ${job.attemptsMade + 1}/${maxAttempts}`);
|
|
805
|
+
throw errorObj; // BullMQ will retry
|
|
806
|
+
}
|
|
807
|
+
// Send to DLQ
|
|
808
|
+
if (this.busConfig.enableDlq && !this.config.skipDlq) {
|
|
809
|
+
await this.sendToDlq(event, errorObj, classification, job.attemptsMade + 1);
|
|
810
|
+
}
|
|
811
|
+
// Update metrics
|
|
812
|
+
this.updateMetrics(startTime, false);
|
|
813
|
+
// Don't throw - we've handled it by sending to DLQ
|
|
814
|
+
return {
|
|
815
|
+
failed: true,
|
|
816
|
+
error: errorObj.message,
|
|
817
|
+
sentToDlq: true
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
/**
|
|
821
|
+
* Check idempotency
|
|
822
|
+
*/
|
|
823
|
+
async checkIdempotency(event) {
|
|
824
|
+
return this.idempotencyStore.check(event, this.config.name, {
|
|
825
|
+
acquireLock: true,
|
|
826
|
+
lockTtlMs: this.config.lockDuration ?? 30000
|
|
827
|
+
});
|
|
828
|
+
}
|
|
829
|
+
/**
|
|
830
|
+
* Send failed event to DLQ
|
|
831
|
+
*/
|
|
832
|
+
async sendToDlq(event, error, classification, attempts) {
|
|
833
|
+
await this.dlqService.send({
|
|
834
|
+
event,
|
|
835
|
+
error: {
|
|
836
|
+
message: error.message,
|
|
837
|
+
stack: error.stack,
|
|
838
|
+
classification: classification.classification,
|
|
839
|
+
reason: classification.reason
|
|
840
|
+
},
|
|
841
|
+
subscriber: this.config.name,
|
|
842
|
+
attempts,
|
|
843
|
+
failedAt: new Date().toISOString()
|
|
844
|
+
});
|
|
845
|
+
this.metrics.dlqSent++;
|
|
846
|
+
this.logger.warn(`Event ${event.id} sent to DLQ after ${attempts} attempts. ` + `Classification: ${classification.classification}`);
|
|
847
|
+
}
|
|
848
|
+
/**
|
|
849
|
+
* Update metrics
|
|
850
|
+
*/
|
|
851
|
+
updateMetrics(startTime, success) {
|
|
852
|
+
const duration = Date.now() - startTime;
|
|
853
|
+
this.metrics.processed++;
|
|
854
|
+
if (success) {
|
|
855
|
+
this.metrics.succeeded++;
|
|
856
|
+
} else {
|
|
857
|
+
this.metrics.failed++;
|
|
858
|
+
}
|
|
859
|
+
// Rolling average
|
|
860
|
+
this.metrics.avgProcessingTimeMs = (this.metrics.avgProcessingTimeMs * (this.metrics.processed - 1) + duration) / this.metrics.processed;
|
|
861
|
+
this.metrics.lastProcessedAt = new Date();
|
|
862
|
+
}
|
|
863
|
+
/**
|
|
864
|
+
* Setup worker event listeners
|
|
865
|
+
*/
|
|
866
|
+
setupWorkerListeners() {
|
|
867
|
+
if (!this.worker) return;
|
|
868
|
+
this.worker.on('completed', job => {
|
|
869
|
+
this.logger.debug(`Job ${job.id} completed`);
|
|
870
|
+
});
|
|
871
|
+
this.worker.on('failed', (job, error) => {
|
|
872
|
+
this.logger.warn(`Job ${job?.id} failed: ${error.message}`);
|
|
873
|
+
});
|
|
874
|
+
this.worker.on('error', error => {
|
|
875
|
+
this.logger.error(`Worker error: ${error.message}`, error.stack);
|
|
876
|
+
});
|
|
877
|
+
this.worker.on('stalled', jobId => {
|
|
878
|
+
this.logger.warn(`Job ${jobId} stalled`);
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
/**
|
|
882
|
+
* Get current metrics
|
|
883
|
+
*/
|
|
884
|
+
getMetrics() {
|
|
885
|
+
return {
|
|
886
|
+
...this.metrics
|
|
887
|
+
};
|
|
888
|
+
}
|
|
889
|
+
/**
|
|
890
|
+
* Pause processing
|
|
891
|
+
*/
|
|
892
|
+
async pause() {
|
|
893
|
+
if (this.worker) {
|
|
894
|
+
await this.worker.pause();
|
|
895
|
+
this.logger.log(`Subscriber "${this.config.name}" paused`);
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
/**
|
|
899
|
+
* Resume processing
|
|
900
|
+
*/
|
|
901
|
+
async resume() {
|
|
902
|
+
if (this.worker) {
|
|
903
|
+
this.worker.resume();
|
|
904
|
+
this.logger.log(`Subscriber "${this.config.name}" resumed`);
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
/**
|
|
908
|
+
* Check if worker is running
|
|
909
|
+
*/
|
|
910
|
+
isRunning() {
|
|
911
|
+
return this.worker?.isRunning() ?? false;
|
|
912
|
+
}
|
|
913
|
+
};
|
|
914
|
+
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);
|
|
915
|
+
|
|
916
|
+
/**
|
|
917
|
+
* Decorators for event handling
|
|
918
|
+
*/
|
|
919
|
+
/**
|
|
920
|
+
* Metadata key for event handlers
|
|
921
|
+
*/
|
|
922
|
+
const EVENT_HANDLER_METADATA = 'EVENT_HANDLER_METADATA';
|
|
923
|
+
/**
|
|
924
|
+
* Decorator to mark a method as an event handler
|
|
925
|
+
*
|
|
926
|
+
* @example
|
|
927
|
+
* ```typescript
|
|
928
|
+
* @Injectable()
|
|
929
|
+
* export class TradeSubscriber extends BaseSubscriber {
|
|
930
|
+
* @OnEvent('TradeProposalCreated', { priority: 'high' })
|
|
931
|
+
* async handleTradeCreated(event: TradeProposalCreated) {
|
|
932
|
+
* // Handle the event
|
|
933
|
+
* }
|
|
934
|
+
* }
|
|
935
|
+
* ```
|
|
936
|
+
*/
|
|
937
|
+
function OnEvent(eventType, options) {
|
|
938
|
+
return SetMetadata(EVENT_HANDLER_METADATA, {
|
|
939
|
+
eventType,
|
|
940
|
+
...options
|
|
941
|
+
});
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
export { BaseSubscriber, DlqService, ERROR_CLASSIFIER, EVENT_BUS_CONFIG, EVENT_HANDLER_METADATA, EVENT_REGISTRY, EventBusModule, EventBusService, IDEMPOTENCY_STORE, OnEvent };
|