@signaltree/events 7.3.5 → 7.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/angular/handlers.cjs +38 -0
- package/dist/angular/handlers.js +35 -0
- package/dist/angular/index.cjs +15 -0
- package/dist/angular/index.js +3 -0
- package/dist/angular/optimistic-updates.cjs +161 -0
- package/dist/angular/optimistic-updates.js +159 -0
- package/dist/angular/websocket.service.cjs +357 -0
- package/{angular.esm.js → dist/angular/websocket.service.js} +1 -191
- package/dist/core/error-classification.cjs +282 -0
- package/dist/core/error-classification.js +276 -0
- package/dist/core/factory.cjs +148 -0
- package/{factory.esm.js → dist/core/factory.js} +2 -37
- package/dist/core/idempotency.cjs +252 -0
- package/dist/core/idempotency.js +247 -0
- package/dist/core/registry.cjs +183 -0
- package/dist/core/registry.js +180 -0
- package/dist/core/types.cjs +41 -0
- package/dist/core/types.js +38 -0
- package/dist/core/validation.cjs +185 -0
- package/{index.esm.js → dist/core/validation.js} +1 -4
- package/dist/index.cjs +43 -0
- package/dist/index.js +7 -0
- package/dist/nestjs/base.subscriber.cjs +287 -0
- package/dist/nestjs/base.subscriber.js +287 -0
- package/dist/nestjs/decorators.cjs +35 -0
- package/dist/nestjs/decorators.js +32 -0
- package/dist/nestjs/dlq.service.cjs +249 -0
- package/dist/nestjs/dlq.service.js +249 -0
- package/dist/nestjs/event-bus.module.cjs +152 -0
- package/dist/nestjs/event-bus.module.js +152 -0
- package/dist/nestjs/event-bus.service.cjs +243 -0
- package/dist/nestjs/event-bus.service.js +243 -0
- package/dist/nestjs/index.cjs +33 -0
- package/dist/nestjs/index.js +6 -0
- package/dist/nestjs/tokens.cjs +14 -0
- package/dist/nestjs/tokens.js +9 -0
- package/dist/testing/assertions.cjs +172 -0
- package/dist/testing/assertions.js +169 -0
- package/dist/testing/factories.cjs +122 -0
- package/dist/testing/factories.js +119 -0
- package/dist/testing/helpers.cjs +233 -0
- package/dist/testing/helpers.js +227 -0
- package/dist/testing/index.cjs +20 -0
- package/dist/testing/index.js +4 -0
- package/dist/testing/mock-event-bus.cjs +237 -0
- package/dist/testing/mock-event-bus.js +234 -0
- package/package.json +22 -22
- package/angular.d.ts +0 -1
- package/idempotency.esm.js +0 -701
- package/index.d.ts +0 -1
- package/nestjs.d.ts +0 -1
- package/nestjs.esm.js +0 -944
- package/testing.d.ts +0 -1
- package/testing.esm.js +0 -743
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var tslib = require('tslib');
|
|
4
|
+
var common = require('@nestjs/common');
|
|
5
|
+
var bullmq = require('bullmq');
|
|
6
|
+
var tokens = require('./tokens.cjs');
|
|
7
|
+
|
|
8
|
+
var DlqService_1;
|
|
9
|
+
exports.DlqService = DlqService_1 = class DlqService {
|
|
10
|
+
config;
|
|
11
|
+
logger = new common.Logger(DlqService_1.name);
|
|
12
|
+
queue;
|
|
13
|
+
queueName;
|
|
14
|
+
constructor(config) {
|
|
15
|
+
this.config = config;
|
|
16
|
+
this.queueName = config.dlqQueueName ?? 'dead-letter';
|
|
17
|
+
}
|
|
18
|
+
async onModuleInit() {
|
|
19
|
+
if (!this.config.enableDlq) {
|
|
20
|
+
this.logger.log('DLQ is disabled');
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
this.queue = new bullmq.Queue(this.queueName, {
|
|
24
|
+
connection: {
|
|
25
|
+
host: this.config.redis.host,
|
|
26
|
+
port: this.config.redis.port,
|
|
27
|
+
password: this.config.redis.password,
|
|
28
|
+
db: this.config.redis.db
|
|
29
|
+
},
|
|
30
|
+
defaultJobOptions: {
|
|
31
|
+
removeOnComplete: false,
|
|
32
|
+
// Keep for inspection
|
|
33
|
+
removeOnFail: false,
|
|
34
|
+
attempts: 1 // No retries in DLQ
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
this.logger.log(`DLQ "${this.queueName}" initialized`);
|
|
38
|
+
}
|
|
39
|
+
async onModuleDestroy() {
|
|
40
|
+
if (this.queue) {
|
|
41
|
+
await this.queue.close();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Send an event to the DLQ
|
|
46
|
+
*/
|
|
47
|
+
async send(entry) {
|
|
48
|
+
if (!this.queue) {
|
|
49
|
+
throw new Error('DLQ is not initialized');
|
|
50
|
+
}
|
|
51
|
+
const job = await this.queue.add(`dlq:${entry.event.type}`, entry, {
|
|
52
|
+
jobId: `dlq:${entry.event.id}:${entry.subscriber}`
|
|
53
|
+
});
|
|
54
|
+
this.logger.debug(`Event ${entry.event.id} sent to DLQ (job: ${job.id})`);
|
|
55
|
+
return job.id ?? `dlq:${entry.event.id}:${entry.subscriber}`;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Get entries from DLQ
|
|
59
|
+
*/
|
|
60
|
+
async getEntries(options = {}) {
|
|
61
|
+
if (!this.queue) {
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
const start = options.start ?? 0;
|
|
65
|
+
const end = options.end ?? 100;
|
|
66
|
+
const jobs = await this.queue.getJobs(['waiting', 'active', 'delayed', 'failed'], start, end);
|
|
67
|
+
let entries = jobs.map(job => job.data);
|
|
68
|
+
// Apply filters
|
|
69
|
+
if (options.eventType) {
|
|
70
|
+
entries = entries.filter(e => e.event.type === options.eventType);
|
|
71
|
+
}
|
|
72
|
+
if (options.subscriber) {
|
|
73
|
+
entries = entries.filter(e => e.subscriber === options.subscriber);
|
|
74
|
+
}
|
|
75
|
+
if (options.classification) {
|
|
76
|
+
entries = entries.filter(e => e.error.classification === options.classification);
|
|
77
|
+
}
|
|
78
|
+
if (options.from) {
|
|
79
|
+
const fromDate = options.from;
|
|
80
|
+
entries = entries.filter(e => new Date(e.failedAt) >= fromDate);
|
|
81
|
+
}
|
|
82
|
+
if (options.to) {
|
|
83
|
+
const toDate = options.to;
|
|
84
|
+
entries = entries.filter(e => new Date(e.failedAt) <= toDate);
|
|
85
|
+
}
|
|
86
|
+
return entries;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Get a specific entry by event ID
|
|
90
|
+
*/
|
|
91
|
+
async getEntry(eventId, subscriber) {
|
|
92
|
+
if (!this.queue) {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
// Try with subscriber suffix first
|
|
96
|
+
if (subscriber) {
|
|
97
|
+
const job = await this.queue.getJob(`dlq:${eventId}:${subscriber}`);
|
|
98
|
+
if (job) {
|
|
99
|
+
return job.data;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// Search through jobs
|
|
103
|
+
const jobs = await this.queue.getJobs(['waiting', 'active', 'delayed', 'failed']);
|
|
104
|
+
const job = jobs.find(j => j.data.event.id === eventId);
|
|
105
|
+
return job ? job.data : null;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Get DLQ statistics
|
|
109
|
+
*/
|
|
110
|
+
async getStats() {
|
|
111
|
+
const entries = await this.getEntries({
|
|
112
|
+
start: 0,
|
|
113
|
+
end: 10000
|
|
114
|
+
});
|
|
115
|
+
const stats = {
|
|
116
|
+
total: entries.length,
|
|
117
|
+
byEventType: {},
|
|
118
|
+
bySubscriber: {},
|
|
119
|
+
byClassification: {
|
|
120
|
+
transient: 0,
|
|
121
|
+
permanent: 0,
|
|
122
|
+
poison: 0,
|
|
123
|
+
unknown: 0
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
for (const entry of entries) {
|
|
127
|
+
// By event type
|
|
128
|
+
stats.byEventType[entry.event.type] = (stats.byEventType[entry.event.type] ?? 0) + 1;
|
|
129
|
+
// By subscriber
|
|
130
|
+
stats.bySubscriber[entry.subscriber] = (stats.bySubscriber[entry.subscriber] ?? 0) + 1;
|
|
131
|
+
// By classification
|
|
132
|
+
stats.byClassification[entry.error.classification]++;
|
|
133
|
+
// Date tracking
|
|
134
|
+
const failedDate = new Date(entry.failedAt);
|
|
135
|
+
if (!stats.oldestEntry || failedDate < stats.oldestEntry) {
|
|
136
|
+
stats.oldestEntry = failedDate;
|
|
137
|
+
}
|
|
138
|
+
if (!stats.newestEntry || failedDate > stats.newestEntry) {
|
|
139
|
+
stats.newestEntry = failedDate;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return stats;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Replay an event from DLQ
|
|
146
|
+
*
|
|
147
|
+
* This removes the event from DLQ and republishes to original queue
|
|
148
|
+
*/
|
|
149
|
+
async replay(eventId, subscriber, targetQueue) {
|
|
150
|
+
if (!this.queue) {
|
|
151
|
+
throw new Error('DLQ is not initialized');
|
|
152
|
+
}
|
|
153
|
+
const jobId = `dlq:${eventId}:${subscriber}`;
|
|
154
|
+
const job = await this.queue.getJob(jobId);
|
|
155
|
+
if (!job) {
|
|
156
|
+
this.logger.warn(`DLQ entry not found: ${jobId}`);
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
const entry = job.data;
|
|
160
|
+
// Get target queue
|
|
161
|
+
const targetQueueInstance = new bullmq.Queue(targetQueue, {
|
|
162
|
+
connection: {
|
|
163
|
+
host: this.config.redis.host,
|
|
164
|
+
port: this.config.redis.port,
|
|
165
|
+
password: this.config.redis.password,
|
|
166
|
+
db: this.config.redis.db
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
try {
|
|
170
|
+
// Republish to original queue
|
|
171
|
+
await targetQueueInstance.add(entry.event.type, entry.event, {
|
|
172
|
+
jobId: `replay:${entry.event.id}`
|
|
173
|
+
});
|
|
174
|
+
// Remove from DLQ
|
|
175
|
+
await job.remove();
|
|
176
|
+
this.logger.log(`Replayed event ${eventId} to queue ${targetQueue}`);
|
|
177
|
+
return true;
|
|
178
|
+
} finally {
|
|
179
|
+
await targetQueueInstance.close();
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Replay all events matching criteria
|
|
184
|
+
*/
|
|
185
|
+
async replayBatch(options) {
|
|
186
|
+
const entries = await this.getEntries(options);
|
|
187
|
+
let replayed = 0;
|
|
188
|
+
let failed = 0;
|
|
189
|
+
for (const entry of entries) {
|
|
190
|
+
try {
|
|
191
|
+
const success = await this.replay(entry.event.id, entry.subscriber, options.targetQueue);
|
|
192
|
+
if (success) replayed++;else failed++;
|
|
193
|
+
} catch {
|
|
194
|
+
failed++;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return {
|
|
198
|
+
replayed,
|
|
199
|
+
failed
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Remove an entry from DLQ
|
|
204
|
+
*/
|
|
205
|
+
async remove(eventId, subscriber) {
|
|
206
|
+
if (!this.queue) {
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
const job = await this.queue.getJob(`dlq:${eventId}:${subscriber}`);
|
|
210
|
+
if (job) {
|
|
211
|
+
await job.remove();
|
|
212
|
+
return true;
|
|
213
|
+
}
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Purge old entries from DLQ
|
|
218
|
+
*/
|
|
219
|
+
async purge(olderThan) {
|
|
220
|
+
const entries = await this.getEntries({
|
|
221
|
+
to: olderThan,
|
|
222
|
+
start: 0,
|
|
223
|
+
end: 10000
|
|
224
|
+
});
|
|
225
|
+
let purged = 0;
|
|
226
|
+
for (const entry of entries) {
|
|
227
|
+
const removed = await this.remove(entry.event.id, entry.subscriber);
|
|
228
|
+
if (removed) purged++;
|
|
229
|
+
}
|
|
230
|
+
this.logger.log(`Purged ${purged} entries from DLQ older than ${olderThan.toISOString()}`);
|
|
231
|
+
return purged;
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Clear all entries from DLQ
|
|
235
|
+
*/
|
|
236
|
+
async clear() {
|
|
237
|
+
if (!this.queue) {
|
|
238
|
+
return 0;
|
|
239
|
+
}
|
|
240
|
+
const count = await this.queue.getJobCounts();
|
|
241
|
+
const total = count.waiting + count.active + count.delayed + count.failed;
|
|
242
|
+
await this.queue.obliterate({
|
|
243
|
+
force: true
|
|
244
|
+
});
|
|
245
|
+
this.logger.warn(`Cleared all ${total} entries from DLQ`);
|
|
246
|
+
return total;
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
exports.DlqService = DlqService_1 = tslib.__decorate([common.Injectable(), tslib.__param(0, common.Inject(tokens.EVENT_BUS_CONFIG)), tslib.__metadata("design:paramtypes", [Object])], exports.DlqService);
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { __decorate, __param, __metadata } from 'tslib';
|
|
2
|
+
import { Injectable, Inject, Logger } from '@nestjs/common';
|
|
3
|
+
import { Queue } from 'bullmq';
|
|
4
|
+
import { EVENT_BUS_CONFIG } from './tokens.js';
|
|
5
|
+
|
|
6
|
+
var DlqService_1;
|
|
7
|
+
let DlqService = DlqService_1 = class DlqService {
|
|
8
|
+
config;
|
|
9
|
+
logger = new Logger(DlqService_1.name);
|
|
10
|
+
queue;
|
|
11
|
+
queueName;
|
|
12
|
+
constructor(config) {
|
|
13
|
+
this.config = config;
|
|
14
|
+
this.queueName = config.dlqQueueName ?? 'dead-letter';
|
|
15
|
+
}
|
|
16
|
+
async onModuleInit() {
|
|
17
|
+
if (!this.config.enableDlq) {
|
|
18
|
+
this.logger.log('DLQ is disabled');
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
this.queue = new Queue(this.queueName, {
|
|
22
|
+
connection: {
|
|
23
|
+
host: this.config.redis.host,
|
|
24
|
+
port: this.config.redis.port,
|
|
25
|
+
password: this.config.redis.password,
|
|
26
|
+
db: this.config.redis.db
|
|
27
|
+
},
|
|
28
|
+
defaultJobOptions: {
|
|
29
|
+
removeOnComplete: false,
|
|
30
|
+
// Keep for inspection
|
|
31
|
+
removeOnFail: false,
|
|
32
|
+
attempts: 1 // No retries in DLQ
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
this.logger.log(`DLQ "${this.queueName}" initialized`);
|
|
36
|
+
}
|
|
37
|
+
async onModuleDestroy() {
|
|
38
|
+
if (this.queue) {
|
|
39
|
+
await this.queue.close();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Send an event to the DLQ
|
|
44
|
+
*/
|
|
45
|
+
async send(entry) {
|
|
46
|
+
if (!this.queue) {
|
|
47
|
+
throw new Error('DLQ is not initialized');
|
|
48
|
+
}
|
|
49
|
+
const job = await this.queue.add(`dlq:${entry.event.type}`, entry, {
|
|
50
|
+
jobId: `dlq:${entry.event.id}:${entry.subscriber}`
|
|
51
|
+
});
|
|
52
|
+
this.logger.debug(`Event ${entry.event.id} sent to DLQ (job: ${job.id})`);
|
|
53
|
+
return job.id ?? `dlq:${entry.event.id}:${entry.subscriber}`;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Get entries from DLQ
|
|
57
|
+
*/
|
|
58
|
+
async getEntries(options = {}) {
|
|
59
|
+
if (!this.queue) {
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
const start = options.start ?? 0;
|
|
63
|
+
const end = options.end ?? 100;
|
|
64
|
+
const jobs = await this.queue.getJobs(['waiting', 'active', 'delayed', 'failed'], start, end);
|
|
65
|
+
let entries = jobs.map(job => job.data);
|
|
66
|
+
// Apply filters
|
|
67
|
+
if (options.eventType) {
|
|
68
|
+
entries = entries.filter(e => e.event.type === options.eventType);
|
|
69
|
+
}
|
|
70
|
+
if (options.subscriber) {
|
|
71
|
+
entries = entries.filter(e => e.subscriber === options.subscriber);
|
|
72
|
+
}
|
|
73
|
+
if (options.classification) {
|
|
74
|
+
entries = entries.filter(e => e.error.classification === options.classification);
|
|
75
|
+
}
|
|
76
|
+
if (options.from) {
|
|
77
|
+
const fromDate = options.from;
|
|
78
|
+
entries = entries.filter(e => new Date(e.failedAt) >= fromDate);
|
|
79
|
+
}
|
|
80
|
+
if (options.to) {
|
|
81
|
+
const toDate = options.to;
|
|
82
|
+
entries = entries.filter(e => new Date(e.failedAt) <= toDate);
|
|
83
|
+
}
|
|
84
|
+
return entries;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Get a specific entry by event ID
|
|
88
|
+
*/
|
|
89
|
+
async getEntry(eventId, subscriber) {
|
|
90
|
+
if (!this.queue) {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
// Try with subscriber suffix first
|
|
94
|
+
if (subscriber) {
|
|
95
|
+
const job = await this.queue.getJob(`dlq:${eventId}:${subscriber}`);
|
|
96
|
+
if (job) {
|
|
97
|
+
return job.data;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// Search through jobs
|
|
101
|
+
const jobs = await this.queue.getJobs(['waiting', 'active', 'delayed', 'failed']);
|
|
102
|
+
const job = jobs.find(j => j.data.event.id === eventId);
|
|
103
|
+
return job ? job.data : null;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Get DLQ statistics
|
|
107
|
+
*/
|
|
108
|
+
async getStats() {
|
|
109
|
+
const entries = await this.getEntries({
|
|
110
|
+
start: 0,
|
|
111
|
+
end: 10000
|
|
112
|
+
});
|
|
113
|
+
const stats = {
|
|
114
|
+
total: entries.length,
|
|
115
|
+
byEventType: {},
|
|
116
|
+
bySubscriber: {},
|
|
117
|
+
byClassification: {
|
|
118
|
+
transient: 0,
|
|
119
|
+
permanent: 0,
|
|
120
|
+
poison: 0,
|
|
121
|
+
unknown: 0
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
for (const entry of entries) {
|
|
125
|
+
// By event type
|
|
126
|
+
stats.byEventType[entry.event.type] = (stats.byEventType[entry.event.type] ?? 0) + 1;
|
|
127
|
+
// By subscriber
|
|
128
|
+
stats.bySubscriber[entry.subscriber] = (stats.bySubscriber[entry.subscriber] ?? 0) + 1;
|
|
129
|
+
// By classification
|
|
130
|
+
stats.byClassification[entry.error.classification]++;
|
|
131
|
+
// Date tracking
|
|
132
|
+
const failedDate = new Date(entry.failedAt);
|
|
133
|
+
if (!stats.oldestEntry || failedDate < stats.oldestEntry) {
|
|
134
|
+
stats.oldestEntry = failedDate;
|
|
135
|
+
}
|
|
136
|
+
if (!stats.newestEntry || failedDate > stats.newestEntry) {
|
|
137
|
+
stats.newestEntry = failedDate;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return stats;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Replay an event from DLQ
|
|
144
|
+
*
|
|
145
|
+
* This removes the event from DLQ and republishes to original queue
|
|
146
|
+
*/
|
|
147
|
+
async replay(eventId, subscriber, targetQueue) {
|
|
148
|
+
if (!this.queue) {
|
|
149
|
+
throw new Error('DLQ is not initialized');
|
|
150
|
+
}
|
|
151
|
+
const jobId = `dlq:${eventId}:${subscriber}`;
|
|
152
|
+
const job = await this.queue.getJob(jobId);
|
|
153
|
+
if (!job) {
|
|
154
|
+
this.logger.warn(`DLQ entry not found: ${jobId}`);
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
const entry = job.data;
|
|
158
|
+
// Get target queue
|
|
159
|
+
const targetQueueInstance = new Queue(targetQueue, {
|
|
160
|
+
connection: {
|
|
161
|
+
host: this.config.redis.host,
|
|
162
|
+
port: this.config.redis.port,
|
|
163
|
+
password: this.config.redis.password,
|
|
164
|
+
db: this.config.redis.db
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
try {
|
|
168
|
+
// Republish to original queue
|
|
169
|
+
await targetQueueInstance.add(entry.event.type, entry.event, {
|
|
170
|
+
jobId: `replay:${entry.event.id}`
|
|
171
|
+
});
|
|
172
|
+
// Remove from DLQ
|
|
173
|
+
await job.remove();
|
|
174
|
+
this.logger.log(`Replayed event ${eventId} to queue ${targetQueue}`);
|
|
175
|
+
return true;
|
|
176
|
+
} finally {
|
|
177
|
+
await targetQueueInstance.close();
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Replay all events matching criteria
|
|
182
|
+
*/
|
|
183
|
+
async replayBatch(options) {
|
|
184
|
+
const entries = await this.getEntries(options);
|
|
185
|
+
let replayed = 0;
|
|
186
|
+
let failed = 0;
|
|
187
|
+
for (const entry of entries) {
|
|
188
|
+
try {
|
|
189
|
+
const success = await this.replay(entry.event.id, entry.subscriber, options.targetQueue);
|
|
190
|
+
if (success) replayed++;else failed++;
|
|
191
|
+
} catch {
|
|
192
|
+
failed++;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return {
|
|
196
|
+
replayed,
|
|
197
|
+
failed
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Remove an entry from DLQ
|
|
202
|
+
*/
|
|
203
|
+
async remove(eventId, subscriber) {
|
|
204
|
+
if (!this.queue) {
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
const job = await this.queue.getJob(`dlq:${eventId}:${subscriber}`);
|
|
208
|
+
if (job) {
|
|
209
|
+
await job.remove();
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Purge old entries from DLQ
|
|
216
|
+
*/
|
|
217
|
+
async purge(olderThan) {
|
|
218
|
+
const entries = await this.getEntries({
|
|
219
|
+
to: olderThan,
|
|
220
|
+
start: 0,
|
|
221
|
+
end: 10000
|
|
222
|
+
});
|
|
223
|
+
let purged = 0;
|
|
224
|
+
for (const entry of entries) {
|
|
225
|
+
const removed = await this.remove(entry.event.id, entry.subscriber);
|
|
226
|
+
if (removed) purged++;
|
|
227
|
+
}
|
|
228
|
+
this.logger.log(`Purged ${purged} entries from DLQ older than ${olderThan.toISOString()}`);
|
|
229
|
+
return purged;
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Clear all entries from DLQ
|
|
233
|
+
*/
|
|
234
|
+
async clear() {
|
|
235
|
+
if (!this.queue) {
|
|
236
|
+
return 0;
|
|
237
|
+
}
|
|
238
|
+
const count = await this.queue.getJobCounts();
|
|
239
|
+
const total = count.waiting + count.active + count.delayed + count.failed;
|
|
240
|
+
await this.queue.obliterate({
|
|
241
|
+
force: true
|
|
242
|
+
});
|
|
243
|
+
this.logger.warn(`Cleared all ${total} entries from DLQ`);
|
|
244
|
+
return total;
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
DlqService = DlqService_1 = __decorate([Injectable(), __param(0, Inject(EVENT_BUS_CONFIG)), __metadata("design:paramtypes", [Object])], DlqService);
|
|
248
|
+
|
|
249
|
+
export { DlqService };
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var tslib = require('tslib');
|
|
4
|
+
var common = require('@nestjs/common');
|
|
5
|
+
var dlq_service = require('./dlq.service.cjs');
|
|
6
|
+
var eventBus_service = require('./event-bus.service.cjs');
|
|
7
|
+
var tokens = require('./tokens.cjs');
|
|
8
|
+
var registry = require('../core/registry.cjs');
|
|
9
|
+
var idempotency = require('../core/idempotency.cjs');
|
|
10
|
+
var errorClassification = require('../core/error-classification.cjs');
|
|
11
|
+
|
|
12
|
+
var EventBusModule_1;
|
|
13
|
+
/**
|
|
14
|
+
* Default queue configuration based on priorities
|
|
15
|
+
*/
|
|
16
|
+
const DEFAULT_QUEUES = [{
|
|
17
|
+
name: 'events-critical',
|
|
18
|
+
priorities: ['critical'],
|
|
19
|
+
concurrency: 10
|
|
20
|
+
}, {
|
|
21
|
+
name: 'events-high',
|
|
22
|
+
priorities: ['high'],
|
|
23
|
+
concurrency: 8
|
|
24
|
+
}, {
|
|
25
|
+
name: 'events-normal',
|
|
26
|
+
priorities: ['normal'],
|
|
27
|
+
concurrency: 5
|
|
28
|
+
}, {
|
|
29
|
+
name: 'events-low',
|
|
30
|
+
priorities: ['low'],
|
|
31
|
+
concurrency: 3
|
|
32
|
+
}, {
|
|
33
|
+
name: 'events-bulk',
|
|
34
|
+
priorities: ['bulk'],
|
|
35
|
+
concurrency: 2,
|
|
36
|
+
rateLimit: {
|
|
37
|
+
max: 100,
|
|
38
|
+
duration: 1000
|
|
39
|
+
}
|
|
40
|
+
}];
|
|
41
|
+
exports.EventBusModule = EventBusModule_1 = class EventBusModule {
|
|
42
|
+
/**
|
|
43
|
+
* Register the EventBus module with configuration
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* ```typescript
|
|
47
|
+
* @Module({
|
|
48
|
+
* imports: [
|
|
49
|
+
* EventBusModule.forRoot({
|
|
50
|
+
* redis: { host: 'localhost', port: 6379 },
|
|
51
|
+
* queues: [
|
|
52
|
+
* { name: 'critical', priorities: ['critical'], concurrency: 10 },
|
|
53
|
+
* { name: 'normal', priorities: ['high', 'normal'], concurrency: 5 },
|
|
54
|
+
* ],
|
|
55
|
+
* }),
|
|
56
|
+
* ],
|
|
57
|
+
* })
|
|
58
|
+
* export class AppModule {}
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
static forRoot(config) {
|
|
62
|
+
const providers = EventBusModule_1.createProviders(config);
|
|
63
|
+
return {
|
|
64
|
+
module: EventBusModule_1,
|
|
65
|
+
global: true,
|
|
66
|
+
providers,
|
|
67
|
+
exports: [eventBus_service.EventBusService, dlq_service.DlqService, tokens.EVENT_BUS_CONFIG, tokens.EVENT_REGISTRY, tokens.IDEMPOTENCY_STORE, tokens.ERROR_CLASSIFIER]
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Register the EventBus module with async configuration
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* ```typescript
|
|
75
|
+
* @Module({
|
|
76
|
+
* imports: [
|
|
77
|
+
* EventBusModule.forRootAsync({
|
|
78
|
+
* imports: [ConfigModule],
|
|
79
|
+
* useFactory: (configService: ConfigService) => ({
|
|
80
|
+
* redis: {
|
|
81
|
+
* host: configService.get('REDIS_HOST'),
|
|
82
|
+
* port: configService.get('REDIS_PORT'),
|
|
83
|
+
* },
|
|
84
|
+
* }),
|
|
85
|
+
* inject: [ConfigService],
|
|
86
|
+
* }),
|
|
87
|
+
* ],
|
|
88
|
+
* })
|
|
89
|
+
* export class AppModule {}
|
|
90
|
+
* ```
|
|
91
|
+
*/
|
|
92
|
+
static forRootAsync(asyncConfig) {
|
|
93
|
+
const configProvider = {
|
|
94
|
+
provide: tokens.EVENT_BUS_CONFIG,
|
|
95
|
+
useFactory: asyncConfig.useFactory,
|
|
96
|
+
inject: asyncConfig.inject ?? []
|
|
97
|
+
};
|
|
98
|
+
const registryProvider = {
|
|
99
|
+
provide: tokens.EVENT_REGISTRY,
|
|
100
|
+
useFactory: config => {
|
|
101
|
+
return registry.createEventRegistry(config.registry);
|
|
102
|
+
},
|
|
103
|
+
inject: [tokens.EVENT_BUS_CONFIG]
|
|
104
|
+
};
|
|
105
|
+
const idempotencyProvider = {
|
|
106
|
+
provide: tokens.IDEMPOTENCY_STORE,
|
|
107
|
+
useFactory: config => {
|
|
108
|
+
return config.idempotencyStore ?? idempotency.createInMemoryIdempotencyStore();
|
|
109
|
+
},
|
|
110
|
+
inject: [tokens.EVENT_BUS_CONFIG]
|
|
111
|
+
};
|
|
112
|
+
const errorClassifierProvider = {
|
|
113
|
+
provide: tokens.ERROR_CLASSIFIER,
|
|
114
|
+
useFactory: config => {
|
|
115
|
+
return errorClassification.createErrorClassifier(config.errorClassifier);
|
|
116
|
+
},
|
|
117
|
+
inject: [tokens.EVENT_BUS_CONFIG]
|
|
118
|
+
};
|
|
119
|
+
return {
|
|
120
|
+
module: EventBusModule_1,
|
|
121
|
+
global: true,
|
|
122
|
+
imports: asyncConfig.imports ?? [],
|
|
123
|
+
providers: [configProvider, registryProvider, idempotencyProvider, errorClassifierProvider, eventBus_service.EventBusService, dlq_service.DlqService],
|
|
124
|
+
exports: [eventBus_service.EventBusService, dlq_service.DlqService, tokens.EVENT_BUS_CONFIG, tokens.EVENT_REGISTRY, tokens.IDEMPOTENCY_STORE, tokens.ERROR_CLASSIFIER]
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
static createProviders(config) {
|
|
128
|
+
const queues = config.queues ?? DEFAULT_QUEUES;
|
|
129
|
+
const fullConfig = {
|
|
130
|
+
...config,
|
|
131
|
+
queues,
|
|
132
|
+
enableDlq: config.enableDlq ?? true,
|
|
133
|
+
dlqQueueName: config.dlqQueueName ?? 'dead-letter',
|
|
134
|
+
enableMetrics: config.enableMetrics ?? true,
|
|
135
|
+
metricsPrefix: config.metricsPrefix ?? 'signaltree_events'
|
|
136
|
+
};
|
|
137
|
+
return [{
|
|
138
|
+
provide: tokens.EVENT_BUS_CONFIG,
|
|
139
|
+
useValue: fullConfig
|
|
140
|
+
}, {
|
|
141
|
+
provide: tokens.EVENT_REGISTRY,
|
|
142
|
+
useFactory: () => registry.createEventRegistry(config.registry)
|
|
143
|
+
}, {
|
|
144
|
+
provide: tokens.IDEMPOTENCY_STORE,
|
|
145
|
+
useValue: config.idempotencyStore ?? idempotency.createInMemoryIdempotencyStore()
|
|
146
|
+
}, {
|
|
147
|
+
provide: tokens.ERROR_CLASSIFIER,
|
|
148
|
+
useFactory: () => errorClassification.createErrorClassifier(config.errorClassifier)
|
|
149
|
+
}, eventBus_service.EventBusService, dlq_service.DlqService];
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
exports.EventBusModule = EventBusModule_1 = tslib.__decorate([common.Module({})], exports.EventBusModule);
|