@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.
- 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/{angular.cjs.js → dist/angular/websocket.service.cjs} +0 -194
- 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/{factory.cjs.js → dist/core/factory.cjs} +3 -40
- 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/{index.cjs.js → dist/core/validation.cjs} +1 -23
- 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 +35 -23
- package/angular.d.ts +0 -1
- package/idempotency.cjs.js +0 -713
- package/idempotency.esm.js +0 -701
- package/index.d.ts +0 -1
- package/nestjs.cjs.js +0 -951
- package/nestjs.d.ts +0 -1
- package/nestjs.esm.js +0 -944
- package/testing.cjs.js +0 -755
- package/testing.d.ts +0 -1
- package/testing.esm.js +0 -743
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { __decorate } from 'tslib';
|
|
2
|
+
import { Module } from '@nestjs/common';
|
|
3
|
+
import { DlqService } from './dlq.service.js';
|
|
4
|
+
import { EventBusService } from './event-bus.service.js';
|
|
5
|
+
import { EVENT_BUS_CONFIG, EVENT_REGISTRY, IDEMPOTENCY_STORE, ERROR_CLASSIFIER } from './tokens.js';
|
|
6
|
+
import { createEventRegistry } from '../core/registry.js';
|
|
7
|
+
import { createInMemoryIdempotencyStore } from '../core/idempotency.js';
|
|
8
|
+
import { createErrorClassifier } from '../core/error-classification.js';
|
|
9
|
+
|
|
10
|
+
var EventBusModule_1;
|
|
11
|
+
/**
|
|
12
|
+
* Default queue configuration based on priorities
|
|
13
|
+
*/
|
|
14
|
+
const DEFAULT_QUEUES = [{
|
|
15
|
+
name: 'events-critical',
|
|
16
|
+
priorities: ['critical'],
|
|
17
|
+
concurrency: 10
|
|
18
|
+
}, {
|
|
19
|
+
name: 'events-high',
|
|
20
|
+
priorities: ['high'],
|
|
21
|
+
concurrency: 8
|
|
22
|
+
}, {
|
|
23
|
+
name: 'events-normal',
|
|
24
|
+
priorities: ['normal'],
|
|
25
|
+
concurrency: 5
|
|
26
|
+
}, {
|
|
27
|
+
name: 'events-low',
|
|
28
|
+
priorities: ['low'],
|
|
29
|
+
concurrency: 3
|
|
30
|
+
}, {
|
|
31
|
+
name: 'events-bulk',
|
|
32
|
+
priorities: ['bulk'],
|
|
33
|
+
concurrency: 2,
|
|
34
|
+
rateLimit: {
|
|
35
|
+
max: 100,
|
|
36
|
+
duration: 1000
|
|
37
|
+
}
|
|
38
|
+
}];
|
|
39
|
+
let EventBusModule = EventBusModule_1 = class EventBusModule {
|
|
40
|
+
/**
|
|
41
|
+
* Register the EventBus module with configuration
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```typescript
|
|
45
|
+
* @Module({
|
|
46
|
+
* imports: [
|
|
47
|
+
* EventBusModule.forRoot({
|
|
48
|
+
* redis: { host: 'localhost', port: 6379 },
|
|
49
|
+
* queues: [
|
|
50
|
+
* { name: 'critical', priorities: ['critical'], concurrency: 10 },
|
|
51
|
+
* { name: 'normal', priorities: ['high', 'normal'], concurrency: 5 },
|
|
52
|
+
* ],
|
|
53
|
+
* }),
|
|
54
|
+
* ],
|
|
55
|
+
* })
|
|
56
|
+
* export class AppModule {}
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
59
|
+
static forRoot(config) {
|
|
60
|
+
const providers = EventBusModule_1.createProviders(config);
|
|
61
|
+
return {
|
|
62
|
+
module: EventBusModule_1,
|
|
63
|
+
global: true,
|
|
64
|
+
providers,
|
|
65
|
+
exports: [EventBusService, DlqService, EVENT_BUS_CONFIG, EVENT_REGISTRY, IDEMPOTENCY_STORE, ERROR_CLASSIFIER]
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Register the EventBus module with async configuration
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* ```typescript
|
|
73
|
+
* @Module({
|
|
74
|
+
* imports: [
|
|
75
|
+
* EventBusModule.forRootAsync({
|
|
76
|
+
* imports: [ConfigModule],
|
|
77
|
+
* useFactory: (configService: ConfigService) => ({
|
|
78
|
+
* redis: {
|
|
79
|
+
* host: configService.get('REDIS_HOST'),
|
|
80
|
+
* port: configService.get('REDIS_PORT'),
|
|
81
|
+
* },
|
|
82
|
+
* }),
|
|
83
|
+
* inject: [ConfigService],
|
|
84
|
+
* }),
|
|
85
|
+
* ],
|
|
86
|
+
* })
|
|
87
|
+
* export class AppModule {}
|
|
88
|
+
* ```
|
|
89
|
+
*/
|
|
90
|
+
static forRootAsync(asyncConfig) {
|
|
91
|
+
const configProvider = {
|
|
92
|
+
provide: EVENT_BUS_CONFIG,
|
|
93
|
+
useFactory: asyncConfig.useFactory,
|
|
94
|
+
inject: asyncConfig.inject ?? []
|
|
95
|
+
};
|
|
96
|
+
const registryProvider = {
|
|
97
|
+
provide: EVENT_REGISTRY,
|
|
98
|
+
useFactory: config => {
|
|
99
|
+
return createEventRegistry(config.registry);
|
|
100
|
+
},
|
|
101
|
+
inject: [EVENT_BUS_CONFIG]
|
|
102
|
+
};
|
|
103
|
+
const idempotencyProvider = {
|
|
104
|
+
provide: IDEMPOTENCY_STORE,
|
|
105
|
+
useFactory: config => {
|
|
106
|
+
return config.idempotencyStore ?? createInMemoryIdempotencyStore();
|
|
107
|
+
},
|
|
108
|
+
inject: [EVENT_BUS_CONFIG]
|
|
109
|
+
};
|
|
110
|
+
const errorClassifierProvider = {
|
|
111
|
+
provide: ERROR_CLASSIFIER,
|
|
112
|
+
useFactory: config => {
|
|
113
|
+
return createErrorClassifier(config.errorClassifier);
|
|
114
|
+
},
|
|
115
|
+
inject: [EVENT_BUS_CONFIG]
|
|
116
|
+
};
|
|
117
|
+
return {
|
|
118
|
+
module: EventBusModule_1,
|
|
119
|
+
global: true,
|
|
120
|
+
imports: asyncConfig.imports ?? [],
|
|
121
|
+
providers: [configProvider, registryProvider, idempotencyProvider, errorClassifierProvider, EventBusService, DlqService],
|
|
122
|
+
exports: [EventBusService, DlqService, EVENT_BUS_CONFIG, EVENT_REGISTRY, IDEMPOTENCY_STORE, ERROR_CLASSIFIER]
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
static createProviders(config) {
|
|
126
|
+
const queues = config.queues ?? DEFAULT_QUEUES;
|
|
127
|
+
const fullConfig = {
|
|
128
|
+
...config,
|
|
129
|
+
queues,
|
|
130
|
+
enableDlq: config.enableDlq ?? true,
|
|
131
|
+
dlqQueueName: config.dlqQueueName ?? 'dead-letter',
|
|
132
|
+
enableMetrics: config.enableMetrics ?? true,
|
|
133
|
+
metricsPrefix: config.metricsPrefix ?? 'signaltree_events'
|
|
134
|
+
};
|
|
135
|
+
return [{
|
|
136
|
+
provide: EVENT_BUS_CONFIG,
|
|
137
|
+
useValue: fullConfig
|
|
138
|
+
}, {
|
|
139
|
+
provide: EVENT_REGISTRY,
|
|
140
|
+
useFactory: () => createEventRegistry(config.registry)
|
|
141
|
+
}, {
|
|
142
|
+
provide: IDEMPOTENCY_STORE,
|
|
143
|
+
useValue: config.idempotencyStore ?? createInMemoryIdempotencyStore()
|
|
144
|
+
}, {
|
|
145
|
+
provide: ERROR_CLASSIFIER,
|
|
146
|
+
useFactory: () => createErrorClassifier(config.errorClassifier)
|
|
147
|
+
}, EventBusService, DlqService];
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
EventBusModule = EventBusModule_1 = __decorate([Module({})], EventBusModule);
|
|
151
|
+
|
|
152
|
+
export { EventBusModule };
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var tslib = require('tslib');
|
|
4
|
+
var common = require('@nestjs/common');
|
|
5
|
+
var bullmq = require('bullmq');
|
|
6
|
+
var factory = require('../core/factory.cjs');
|
|
7
|
+
var registry = require('../core/registry.cjs');
|
|
8
|
+
var tokens = require('./tokens.cjs');
|
|
9
|
+
|
|
10
|
+
var EventBusService_1;
|
|
11
|
+
exports.EventBusService = EventBusService_1 = class EventBusService {
|
|
12
|
+
config;
|
|
13
|
+
registry;
|
|
14
|
+
logger = new common.Logger(EventBusService_1.name);
|
|
15
|
+
queues = new Map();
|
|
16
|
+
priorityToQueue = new Map();
|
|
17
|
+
connection;
|
|
18
|
+
isReady = false;
|
|
19
|
+
constructor(config, registry) {
|
|
20
|
+
this.config = config;
|
|
21
|
+
this.registry = registry;
|
|
22
|
+
this.connection = {
|
|
23
|
+
host: config.redis.host,
|
|
24
|
+
port: config.redis.port,
|
|
25
|
+
password: config.redis.password,
|
|
26
|
+
db: config.redis.db,
|
|
27
|
+
maxRetriesPerRequest: config.redis.maxRetriesPerRequest ?? 3
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
async onModuleInit() {
|
|
31
|
+
this.logger.log('Initializing EventBus queues...');
|
|
32
|
+
// Create queues
|
|
33
|
+
for (const queueConfig of this.config.queues ?? []) {
|
|
34
|
+
const queue = new bullmq.Queue(queueConfig.name, {
|
|
35
|
+
connection: this.connection,
|
|
36
|
+
defaultJobOptions: {
|
|
37
|
+
removeOnComplete: 1000,
|
|
38
|
+
// Keep last 1000 completed jobs
|
|
39
|
+
removeOnFail: 5000,
|
|
40
|
+
// Keep last 5000 failed jobs
|
|
41
|
+
attempts: 5,
|
|
42
|
+
backoff: {
|
|
43
|
+
type: 'exponential',
|
|
44
|
+
delay: 1000
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
// Map priorities to this queue
|
|
49
|
+
for (const priority of queueConfig.priorities) {
|
|
50
|
+
this.priorityToQueue.set(priority, queueConfig.name);
|
|
51
|
+
}
|
|
52
|
+
const instance = {
|
|
53
|
+
config: queueConfig,
|
|
54
|
+
queue
|
|
55
|
+
};
|
|
56
|
+
// Optionally create queue events listener for monitoring
|
|
57
|
+
if (this.config.enableMetrics) {
|
|
58
|
+
instance.events = new bullmq.QueueEvents(queueConfig.name, {
|
|
59
|
+
connection: this.connection
|
|
60
|
+
});
|
|
61
|
+
this.setupQueueEventListeners(instance);
|
|
62
|
+
}
|
|
63
|
+
this.queues.set(queueConfig.name, instance);
|
|
64
|
+
this.logger.log(`Queue "${queueConfig.name}" initialized for priorities: ${queueConfig.priorities.join(', ')}`);
|
|
65
|
+
}
|
|
66
|
+
this.isReady = true;
|
|
67
|
+
this.logger.log(`EventBus ready with ${this.queues.size} queues`);
|
|
68
|
+
}
|
|
69
|
+
async onModuleDestroy() {
|
|
70
|
+
this.logger.log('Shutting down EventBus...');
|
|
71
|
+
const closePromises = [];
|
|
72
|
+
for (const [name, instance] of this.queues) {
|
|
73
|
+
this.logger.debug(`Closing queue "${name}"...`);
|
|
74
|
+
closePromises.push(instance.queue.close());
|
|
75
|
+
if (instance.events) {
|
|
76
|
+
closePromises.push(instance.events.close());
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
await Promise.all(closePromises);
|
|
80
|
+
this.queues.clear();
|
|
81
|
+
this.isReady = false;
|
|
82
|
+
this.logger.log('EventBus shutdown complete');
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Publish an event
|
|
86
|
+
*
|
|
87
|
+
* @example
|
|
88
|
+
* ```typescript
|
|
89
|
+
* await eventBus.publish({
|
|
90
|
+
* type: 'TradeProposalCreated',
|
|
91
|
+
* data: {
|
|
92
|
+
* tradeId: '123',
|
|
93
|
+
* initiatorId: 'user-1',
|
|
94
|
+
* recipientId: 'user-2',
|
|
95
|
+
* },
|
|
96
|
+
* });
|
|
97
|
+
* ```
|
|
98
|
+
*/
|
|
99
|
+
async publish(event, options = {}) {
|
|
100
|
+
if (!this.isReady) {
|
|
101
|
+
throw new Error('EventBus is not ready. Wait for module initialization.');
|
|
102
|
+
}
|
|
103
|
+
// Build complete event
|
|
104
|
+
const eventId = options.id ?? event.id ?? factory.generateEventId();
|
|
105
|
+
const correlationId = options.correlationId ?? event.correlationId ?? factory.generateCorrelationId();
|
|
106
|
+
const timestamp = event.timestamp ?? new Date().toISOString();
|
|
107
|
+
const fullEvent = {
|
|
108
|
+
...event,
|
|
109
|
+
id: eventId,
|
|
110
|
+
correlationId,
|
|
111
|
+
causationId: options.causationId ?? event.causationId,
|
|
112
|
+
timestamp,
|
|
113
|
+
priority: options.priority ?? event.priority ?? 'normal'
|
|
114
|
+
};
|
|
115
|
+
// Validate event against registry
|
|
116
|
+
const validated = this.registry.validate(fullEvent);
|
|
117
|
+
// Determine queue based on priority
|
|
118
|
+
const priority = validated.priority ?? 'normal';
|
|
119
|
+
const queueName = options.queue ?? this.getQueueForPriority(priority);
|
|
120
|
+
const instance = this.queues.get(queueName);
|
|
121
|
+
if (!instance) {
|
|
122
|
+
throw new Error(`Queue "${queueName}" not found. Available: ${Array.from(this.queues.keys()).join(', ')}`);
|
|
123
|
+
}
|
|
124
|
+
// Publish to BullMQ
|
|
125
|
+
const jobId = options.jobId ?? eventId;
|
|
126
|
+
const job = await instance.queue.add(validated.type,
|
|
127
|
+
// Job name = event type
|
|
128
|
+
validated, {
|
|
129
|
+
jobId,
|
|
130
|
+
delay: options.delay,
|
|
131
|
+
priority: this.getPriorityNumber(priority),
|
|
132
|
+
...options.jobOptions
|
|
133
|
+
});
|
|
134
|
+
this.logger.debug(`Published event ${validated.type}:${eventId} to queue ${queueName} (job: ${job.id})`);
|
|
135
|
+
return {
|
|
136
|
+
eventId,
|
|
137
|
+
jobId: job.id ?? eventId,
|
|
138
|
+
queue: queueName,
|
|
139
|
+
correlationId
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Publish multiple events in a batch
|
|
144
|
+
*/
|
|
145
|
+
async publishBatch(events, options = {}) {
|
|
146
|
+
// Use same correlation ID for all events in batch
|
|
147
|
+
const correlationId = options.correlationId ?? factory.generateCorrelationId();
|
|
148
|
+
const results = await Promise.all(events.map((event, index) => this.publish(event, {
|
|
149
|
+
...options,
|
|
150
|
+
correlationId,
|
|
151
|
+
causationId: index > 0 ? events[index - 1].id : options.causationId
|
|
152
|
+
})));
|
|
153
|
+
return results;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Get queue for a given priority
|
|
157
|
+
*/
|
|
158
|
+
getQueueForPriority(priority) {
|
|
159
|
+
const queueName = this.priorityToQueue.get(priority);
|
|
160
|
+
if (!queueName) {
|
|
161
|
+
// Fall back to normal queue
|
|
162
|
+
return this.priorityToQueue.get('normal') ?? 'events-normal';
|
|
163
|
+
}
|
|
164
|
+
return queueName;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Get queue stats
|
|
168
|
+
*/
|
|
169
|
+
async getQueueStats(queueName) {
|
|
170
|
+
const instance = this.queues.get(queueName);
|
|
171
|
+
if (!instance) {
|
|
172
|
+
throw new Error(`Queue "${queueName}" not found`);
|
|
173
|
+
}
|
|
174
|
+
const [waiting, active, completed, failed, delayed] = await Promise.all([instance.queue.getWaitingCount(), instance.queue.getActiveCount(), instance.queue.getCompletedCount(), instance.queue.getFailedCount(), instance.queue.getDelayedCount()]);
|
|
175
|
+
return {
|
|
176
|
+
waiting,
|
|
177
|
+
active,
|
|
178
|
+
completed,
|
|
179
|
+
failed,
|
|
180
|
+
delayed
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Get all queue names
|
|
185
|
+
*/
|
|
186
|
+
getQueueNames() {
|
|
187
|
+
return Array.from(this.queues.keys());
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Get underlying BullMQ queue for advanced operations
|
|
191
|
+
*/
|
|
192
|
+
getQueue(name) {
|
|
193
|
+
return this.queues.get(name)?.queue;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Check if service is ready
|
|
197
|
+
*/
|
|
198
|
+
isServiceReady() {
|
|
199
|
+
return this.isReady;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Convert priority string to number for BullMQ (lower = higher priority)
|
|
203
|
+
*/
|
|
204
|
+
getPriorityNumber(priority) {
|
|
205
|
+
switch (priority) {
|
|
206
|
+
case 'critical':
|
|
207
|
+
return 1;
|
|
208
|
+
case 'high':
|
|
209
|
+
return 2;
|
|
210
|
+
case 'normal':
|
|
211
|
+
return 3;
|
|
212
|
+
case 'low':
|
|
213
|
+
return 4;
|
|
214
|
+
case 'bulk':
|
|
215
|
+
return 5;
|
|
216
|
+
default:
|
|
217
|
+
return 3;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Setup event listeners for monitoring
|
|
222
|
+
*/
|
|
223
|
+
setupQueueEventListeners(instance) {
|
|
224
|
+
if (!instance.events) return;
|
|
225
|
+
instance.events.on('completed', ({
|
|
226
|
+
jobId
|
|
227
|
+
}) => {
|
|
228
|
+
this.logger.debug(`Job ${jobId} completed in queue ${instance.config.name}`);
|
|
229
|
+
});
|
|
230
|
+
instance.events.on('failed', ({
|
|
231
|
+
jobId,
|
|
232
|
+
failedReason
|
|
233
|
+
}) => {
|
|
234
|
+
this.logger.warn(`Job ${jobId} failed in queue ${instance.config.name}: ${failedReason}`);
|
|
235
|
+
});
|
|
236
|
+
instance.events.on('stalled', ({
|
|
237
|
+
jobId
|
|
238
|
+
}) => {
|
|
239
|
+
this.logger.warn(`Job ${jobId} stalled in queue ${instance.config.name}`);
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
exports.EventBusService = EventBusService_1 = tslib.__decorate([common.Injectable(), tslib.__param(0, common.Inject(tokens.EVENT_BUS_CONFIG)), tslib.__param(1, common.Inject(tokens.EVENT_REGISTRY)), tslib.__metadata("design:paramtypes", [Object, registry.EventRegistry])], exports.EventBusService);
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { __decorate, __param, __metadata } from 'tslib';
|
|
2
|
+
import { Injectable, Inject, Logger } from '@nestjs/common';
|
|
3
|
+
import { Queue, QueueEvents } from 'bullmq';
|
|
4
|
+
import { generateEventId, generateCorrelationId } from '../core/factory.js';
|
|
5
|
+
import { EventRegistry } from '../core/registry.js';
|
|
6
|
+
import { EVENT_BUS_CONFIG, EVENT_REGISTRY } from './tokens.js';
|
|
7
|
+
|
|
8
|
+
var EventBusService_1;
|
|
9
|
+
let EventBusService = EventBusService_1 = class EventBusService {
|
|
10
|
+
config;
|
|
11
|
+
registry;
|
|
12
|
+
logger = new Logger(EventBusService_1.name);
|
|
13
|
+
queues = new Map();
|
|
14
|
+
priorityToQueue = new Map();
|
|
15
|
+
connection;
|
|
16
|
+
isReady = false;
|
|
17
|
+
constructor(config, registry) {
|
|
18
|
+
this.config = config;
|
|
19
|
+
this.registry = registry;
|
|
20
|
+
this.connection = {
|
|
21
|
+
host: config.redis.host,
|
|
22
|
+
port: config.redis.port,
|
|
23
|
+
password: config.redis.password,
|
|
24
|
+
db: config.redis.db,
|
|
25
|
+
maxRetriesPerRequest: config.redis.maxRetriesPerRequest ?? 3
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
async onModuleInit() {
|
|
29
|
+
this.logger.log('Initializing EventBus queues...');
|
|
30
|
+
// Create queues
|
|
31
|
+
for (const queueConfig of this.config.queues ?? []) {
|
|
32
|
+
const queue = new Queue(queueConfig.name, {
|
|
33
|
+
connection: this.connection,
|
|
34
|
+
defaultJobOptions: {
|
|
35
|
+
removeOnComplete: 1000,
|
|
36
|
+
// Keep last 1000 completed jobs
|
|
37
|
+
removeOnFail: 5000,
|
|
38
|
+
// Keep last 5000 failed jobs
|
|
39
|
+
attempts: 5,
|
|
40
|
+
backoff: {
|
|
41
|
+
type: 'exponential',
|
|
42
|
+
delay: 1000
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
// Map priorities to this queue
|
|
47
|
+
for (const priority of queueConfig.priorities) {
|
|
48
|
+
this.priorityToQueue.set(priority, queueConfig.name);
|
|
49
|
+
}
|
|
50
|
+
const instance = {
|
|
51
|
+
config: queueConfig,
|
|
52
|
+
queue
|
|
53
|
+
};
|
|
54
|
+
// Optionally create queue events listener for monitoring
|
|
55
|
+
if (this.config.enableMetrics) {
|
|
56
|
+
instance.events = new QueueEvents(queueConfig.name, {
|
|
57
|
+
connection: this.connection
|
|
58
|
+
});
|
|
59
|
+
this.setupQueueEventListeners(instance);
|
|
60
|
+
}
|
|
61
|
+
this.queues.set(queueConfig.name, instance);
|
|
62
|
+
this.logger.log(`Queue "${queueConfig.name}" initialized for priorities: ${queueConfig.priorities.join(', ')}`);
|
|
63
|
+
}
|
|
64
|
+
this.isReady = true;
|
|
65
|
+
this.logger.log(`EventBus ready with ${this.queues.size} queues`);
|
|
66
|
+
}
|
|
67
|
+
async onModuleDestroy() {
|
|
68
|
+
this.logger.log('Shutting down EventBus...');
|
|
69
|
+
const closePromises = [];
|
|
70
|
+
for (const [name, instance] of this.queues) {
|
|
71
|
+
this.logger.debug(`Closing queue "${name}"...`);
|
|
72
|
+
closePromises.push(instance.queue.close());
|
|
73
|
+
if (instance.events) {
|
|
74
|
+
closePromises.push(instance.events.close());
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
await Promise.all(closePromises);
|
|
78
|
+
this.queues.clear();
|
|
79
|
+
this.isReady = false;
|
|
80
|
+
this.logger.log('EventBus shutdown complete');
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Publish an event
|
|
84
|
+
*
|
|
85
|
+
* @example
|
|
86
|
+
* ```typescript
|
|
87
|
+
* await eventBus.publish({
|
|
88
|
+
* type: 'TradeProposalCreated',
|
|
89
|
+
* data: {
|
|
90
|
+
* tradeId: '123',
|
|
91
|
+
* initiatorId: 'user-1',
|
|
92
|
+
* recipientId: 'user-2',
|
|
93
|
+
* },
|
|
94
|
+
* });
|
|
95
|
+
* ```
|
|
96
|
+
*/
|
|
97
|
+
async publish(event, options = {}) {
|
|
98
|
+
if (!this.isReady) {
|
|
99
|
+
throw new Error('EventBus is not ready. Wait for module initialization.');
|
|
100
|
+
}
|
|
101
|
+
// Build complete event
|
|
102
|
+
const eventId = options.id ?? event.id ?? generateEventId();
|
|
103
|
+
const correlationId = options.correlationId ?? event.correlationId ?? generateCorrelationId();
|
|
104
|
+
const timestamp = event.timestamp ?? new Date().toISOString();
|
|
105
|
+
const fullEvent = {
|
|
106
|
+
...event,
|
|
107
|
+
id: eventId,
|
|
108
|
+
correlationId,
|
|
109
|
+
causationId: options.causationId ?? event.causationId,
|
|
110
|
+
timestamp,
|
|
111
|
+
priority: options.priority ?? event.priority ?? 'normal'
|
|
112
|
+
};
|
|
113
|
+
// Validate event against registry
|
|
114
|
+
const validated = this.registry.validate(fullEvent);
|
|
115
|
+
// Determine queue based on priority
|
|
116
|
+
const priority = validated.priority ?? 'normal';
|
|
117
|
+
const queueName = options.queue ?? this.getQueueForPriority(priority);
|
|
118
|
+
const instance = this.queues.get(queueName);
|
|
119
|
+
if (!instance) {
|
|
120
|
+
throw new Error(`Queue "${queueName}" not found. Available: ${Array.from(this.queues.keys()).join(', ')}`);
|
|
121
|
+
}
|
|
122
|
+
// Publish to BullMQ
|
|
123
|
+
const jobId = options.jobId ?? eventId;
|
|
124
|
+
const job = await instance.queue.add(validated.type,
|
|
125
|
+
// Job name = event type
|
|
126
|
+
validated, {
|
|
127
|
+
jobId,
|
|
128
|
+
delay: options.delay,
|
|
129
|
+
priority: this.getPriorityNumber(priority),
|
|
130
|
+
...options.jobOptions
|
|
131
|
+
});
|
|
132
|
+
this.logger.debug(`Published event ${validated.type}:${eventId} to queue ${queueName} (job: ${job.id})`);
|
|
133
|
+
return {
|
|
134
|
+
eventId,
|
|
135
|
+
jobId: job.id ?? eventId,
|
|
136
|
+
queue: queueName,
|
|
137
|
+
correlationId
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Publish multiple events in a batch
|
|
142
|
+
*/
|
|
143
|
+
async publishBatch(events, options = {}) {
|
|
144
|
+
// Use same correlation ID for all events in batch
|
|
145
|
+
const correlationId = options.correlationId ?? generateCorrelationId();
|
|
146
|
+
const results = await Promise.all(events.map((event, index) => this.publish(event, {
|
|
147
|
+
...options,
|
|
148
|
+
correlationId,
|
|
149
|
+
causationId: index > 0 ? events[index - 1].id : options.causationId
|
|
150
|
+
})));
|
|
151
|
+
return results;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Get queue for a given priority
|
|
155
|
+
*/
|
|
156
|
+
getQueueForPriority(priority) {
|
|
157
|
+
const queueName = this.priorityToQueue.get(priority);
|
|
158
|
+
if (!queueName) {
|
|
159
|
+
// Fall back to normal queue
|
|
160
|
+
return this.priorityToQueue.get('normal') ?? 'events-normal';
|
|
161
|
+
}
|
|
162
|
+
return queueName;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Get queue stats
|
|
166
|
+
*/
|
|
167
|
+
async getQueueStats(queueName) {
|
|
168
|
+
const instance = this.queues.get(queueName);
|
|
169
|
+
if (!instance) {
|
|
170
|
+
throw new Error(`Queue "${queueName}" not found`);
|
|
171
|
+
}
|
|
172
|
+
const [waiting, active, completed, failed, delayed] = await Promise.all([instance.queue.getWaitingCount(), instance.queue.getActiveCount(), instance.queue.getCompletedCount(), instance.queue.getFailedCount(), instance.queue.getDelayedCount()]);
|
|
173
|
+
return {
|
|
174
|
+
waiting,
|
|
175
|
+
active,
|
|
176
|
+
completed,
|
|
177
|
+
failed,
|
|
178
|
+
delayed
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Get all queue names
|
|
183
|
+
*/
|
|
184
|
+
getQueueNames() {
|
|
185
|
+
return Array.from(this.queues.keys());
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Get underlying BullMQ queue for advanced operations
|
|
189
|
+
*/
|
|
190
|
+
getQueue(name) {
|
|
191
|
+
return this.queues.get(name)?.queue;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Check if service is ready
|
|
195
|
+
*/
|
|
196
|
+
isServiceReady() {
|
|
197
|
+
return this.isReady;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Convert priority string to number for BullMQ (lower = higher priority)
|
|
201
|
+
*/
|
|
202
|
+
getPriorityNumber(priority) {
|
|
203
|
+
switch (priority) {
|
|
204
|
+
case 'critical':
|
|
205
|
+
return 1;
|
|
206
|
+
case 'high':
|
|
207
|
+
return 2;
|
|
208
|
+
case 'normal':
|
|
209
|
+
return 3;
|
|
210
|
+
case 'low':
|
|
211
|
+
return 4;
|
|
212
|
+
case 'bulk':
|
|
213
|
+
return 5;
|
|
214
|
+
default:
|
|
215
|
+
return 3;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Setup event listeners for monitoring
|
|
220
|
+
*/
|
|
221
|
+
setupQueueEventListeners(instance) {
|
|
222
|
+
if (!instance.events) return;
|
|
223
|
+
instance.events.on('completed', ({
|
|
224
|
+
jobId
|
|
225
|
+
}) => {
|
|
226
|
+
this.logger.debug(`Job ${jobId} completed in queue ${instance.config.name}`);
|
|
227
|
+
});
|
|
228
|
+
instance.events.on('failed', ({
|
|
229
|
+
jobId,
|
|
230
|
+
failedReason
|
|
231
|
+
}) => {
|
|
232
|
+
this.logger.warn(`Job ${jobId} failed in queue ${instance.config.name}: ${failedReason}`);
|
|
233
|
+
});
|
|
234
|
+
instance.events.on('stalled', ({
|
|
235
|
+
jobId
|
|
236
|
+
}) => {
|
|
237
|
+
this.logger.warn(`Job ${jobId} stalled in queue ${instance.config.name}`);
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
EventBusService = EventBusService_1 = __decorate([Injectable(), __param(0, Inject(EVENT_BUS_CONFIG)), __param(1, Inject(EVENT_REGISTRY)), __metadata("design:paramtypes", [Object, EventRegistry])], EventBusService);
|
|
242
|
+
|
|
243
|
+
export { EventBusService };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var eventBus_module = require('./event-bus.module.cjs');
|
|
4
|
+
var eventBus_service = require('./event-bus.service.cjs');
|
|
5
|
+
var base_subscriber = require('./base.subscriber.cjs');
|
|
6
|
+
var dlq_service = require('./dlq.service.cjs');
|
|
7
|
+
var decorators = require('./decorators.cjs');
|
|
8
|
+
var tokens = require('./tokens.cjs');
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
Object.defineProperty(exports, "EventBusModule", {
|
|
13
|
+
enumerable: true,
|
|
14
|
+
get: function () { return eventBus_module.EventBusModule; }
|
|
15
|
+
});
|
|
16
|
+
Object.defineProperty(exports, "EventBusService", {
|
|
17
|
+
enumerable: true,
|
|
18
|
+
get: function () { return eventBus_service.EventBusService; }
|
|
19
|
+
});
|
|
20
|
+
Object.defineProperty(exports, "BaseSubscriber", {
|
|
21
|
+
enumerable: true,
|
|
22
|
+
get: function () { return base_subscriber.BaseSubscriber; }
|
|
23
|
+
});
|
|
24
|
+
Object.defineProperty(exports, "DlqService", {
|
|
25
|
+
enumerable: true,
|
|
26
|
+
get: function () { return dlq_service.DlqService; }
|
|
27
|
+
});
|
|
28
|
+
exports.EVENT_HANDLER_METADATA = decorators.EVENT_HANDLER_METADATA;
|
|
29
|
+
exports.OnEvent = decorators.OnEvent;
|
|
30
|
+
exports.ERROR_CLASSIFIER = tokens.ERROR_CLASSIFIER;
|
|
31
|
+
exports.EVENT_BUS_CONFIG = tokens.EVENT_BUS_CONFIG;
|
|
32
|
+
exports.EVENT_REGISTRY = tokens.EVENT_REGISTRY;
|
|
33
|
+
exports.IDEMPOTENCY_STORE = tokens.IDEMPOTENCY_STORE;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { EventBusModule } from './event-bus.module.js';
|
|
2
|
+
export { EventBusService } from './event-bus.service.js';
|
|
3
|
+
export { BaseSubscriber } from './base.subscriber.js';
|
|
4
|
+
export { DlqService } from './dlq.service.js';
|
|
5
|
+
export { EVENT_HANDLER_METADATA, OnEvent } from './decorators.js';
|
|
6
|
+
export { ERROR_CLASSIFIER, EVENT_BUS_CONFIG, EVENT_REGISTRY, IDEMPOTENCY_STORE } from './tokens.js';
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Injection tokens for the EventBus module
|
|
5
|
+
*/
|
|
6
|
+
const EVENT_BUS_CONFIG = Symbol('EVENT_BUS_CONFIG');
|
|
7
|
+
const EVENT_REGISTRY = Symbol('EVENT_REGISTRY');
|
|
8
|
+
const IDEMPOTENCY_STORE = Symbol('IDEMPOTENCY_STORE');
|
|
9
|
+
const ERROR_CLASSIFIER = Symbol('ERROR_CLASSIFIER');
|
|
10
|
+
|
|
11
|
+
exports.ERROR_CLASSIFIER = ERROR_CLASSIFIER;
|
|
12
|
+
exports.EVENT_BUS_CONFIG = EVENT_BUS_CONFIG;
|
|
13
|
+
exports.EVENT_REGISTRY = EVENT_REGISTRY;
|
|
14
|
+
exports.IDEMPOTENCY_STORE = IDEMPOTENCY_STORE;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Injection tokens for the EventBus module
|
|
3
|
+
*/
|
|
4
|
+
const EVENT_BUS_CONFIG = Symbol('EVENT_BUS_CONFIG');
|
|
5
|
+
const EVENT_REGISTRY = Symbol('EVENT_REGISTRY');
|
|
6
|
+
const IDEMPOTENCY_STORE = Symbol('IDEMPOTENCY_STORE');
|
|
7
|
+
const ERROR_CLASSIFIER = Symbol('ERROR_CLASSIFIER');
|
|
8
|
+
|
|
9
|
+
export { ERROR_CLASSIFIER, EVENT_BUS_CONFIG, EVENT_REGISTRY, IDEMPOTENCY_STORE };
|