runmq 0.0.1 → 1.0.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/LICENSE +21 -0
- package/README.md +470 -0
- package/dist/index.cjs +796 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +133 -0
- package/dist/index.d.ts +133 -0
- package/dist/index.js +762 -0
- package/dist/index.js.map +1 -0
- package/package.json +56 -8
- package/.vscode/settings.json +0 -5
package/dist/index.js
ADDED
|
@@ -0,0 +1,762 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __defProps = Object.defineProperties;
|
|
3
|
+
var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
|
|
4
|
+
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __propIsEnum = Object.prototype.propertyIsEnumerable;
|
|
7
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
8
|
+
var __spreadValues = (a, b) => {
|
|
9
|
+
for (var prop in b || (b = {}))
|
|
10
|
+
if (__hasOwnProp.call(b, prop))
|
|
11
|
+
__defNormalProp(a, prop, b[prop]);
|
|
12
|
+
if (__getOwnPropSymbols)
|
|
13
|
+
for (var prop of __getOwnPropSymbols(b)) {
|
|
14
|
+
if (__propIsEnum.call(b, prop))
|
|
15
|
+
__defNormalProp(a, prop, b[prop]);
|
|
16
|
+
}
|
|
17
|
+
return a;
|
|
18
|
+
};
|
|
19
|
+
var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
|
|
20
|
+
|
|
21
|
+
// src/core/exceptions/RunMQException.ts
|
|
22
|
+
var RunMQException = class extends Error {
|
|
23
|
+
constructor(exception, details) {
|
|
24
|
+
super(`RunMQ Exception: ${exception}`);
|
|
25
|
+
this.exception = exception;
|
|
26
|
+
this.details = details;
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// src/core/clients/AmqplibClient.ts
|
|
31
|
+
import * as amqp from "amqplib";
|
|
32
|
+
|
|
33
|
+
// src/core/exceptions/Exceptions.ts
|
|
34
|
+
var Exceptions = class {
|
|
35
|
+
};
|
|
36
|
+
Exceptions.EXCEEDING_CONNECTION_ATTEMPTS = "EXCEEDING_CONNECTION_ATTEMPTS";
|
|
37
|
+
Exceptions.CONNECTION_NOT_ESTABLISHED = "CONNECTION_NOT_ESTABLISHED";
|
|
38
|
+
Exceptions.NOT_INITIALIZED = "NOT_INITIALIZED";
|
|
39
|
+
Exceptions.INVALID_MESSAGE_FORMAT = "MESSAGE_SHOULD_BE_VALID_RECORD";
|
|
40
|
+
Exceptions.UNSUPPORTED_SCHEMA = "UNSUPPORTED_SCHEMA";
|
|
41
|
+
|
|
42
|
+
// src/core/clients/AmqplibClient.ts
|
|
43
|
+
var AmqplibClient = class {
|
|
44
|
+
constructor(config) {
|
|
45
|
+
this.config = config;
|
|
46
|
+
this.isConnected = false;
|
|
47
|
+
this.config = config;
|
|
48
|
+
}
|
|
49
|
+
async connect() {
|
|
50
|
+
try {
|
|
51
|
+
if (this.isConnected && this.channelModel) {
|
|
52
|
+
return this.channelModel;
|
|
53
|
+
}
|
|
54
|
+
this.channelModel = await amqp.connect(this.config.url);
|
|
55
|
+
this.isConnected = true;
|
|
56
|
+
if (this.isConnected) {
|
|
57
|
+
this.channelModel.on("error", () => {
|
|
58
|
+
this.isConnected = false;
|
|
59
|
+
});
|
|
60
|
+
this.channelModel.on("close", () => {
|
|
61
|
+
this.isConnected = false;
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
return this.channelModel;
|
|
65
|
+
} catch (error) {
|
|
66
|
+
this.isConnected = false;
|
|
67
|
+
throw new RunMQException(
|
|
68
|
+
Exceptions.CONNECTION_NOT_ESTABLISHED,
|
|
69
|
+
{
|
|
70
|
+
error: error instanceof Error ? error.message : String(error)
|
|
71
|
+
}
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
async getChannel() {
|
|
76
|
+
return await (await this.connect()).createChannel();
|
|
77
|
+
}
|
|
78
|
+
async disconnect() {
|
|
79
|
+
try {
|
|
80
|
+
if (this.channelModel && this.isConnected) {
|
|
81
|
+
await this.channelModel.close();
|
|
82
|
+
this.isConnected = false;
|
|
83
|
+
}
|
|
84
|
+
} catch (error) {
|
|
85
|
+
throw new RunMQException(
|
|
86
|
+
Exceptions.CONNECTION_NOT_ESTABLISHED,
|
|
87
|
+
{
|
|
88
|
+
error: error instanceof Error ? error.message : String(error)
|
|
89
|
+
}
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
isActive() {
|
|
94
|
+
return this.isConnected && this.channelModel !== void 0;
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
// src/core/utils/Utils.ts
|
|
99
|
+
import { randomUUID } from "crypto";
|
|
100
|
+
var RunMQUtils = class {
|
|
101
|
+
static delay(ms) {
|
|
102
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
103
|
+
}
|
|
104
|
+
static generateUUID() {
|
|
105
|
+
return randomUUID();
|
|
106
|
+
}
|
|
107
|
+
static assertRecord(message) {
|
|
108
|
+
if (typeof message !== "object" || message === null || Array.isArray(message)) {
|
|
109
|
+
throw new RunMQException(Exceptions.INVALID_MESSAGE_FORMAT, {});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
// src/core/constants/index.ts
|
|
115
|
+
var RUNMQ_PREFIX = "_runmq_";
|
|
116
|
+
var Constants = {
|
|
117
|
+
ROUTER_EXCHANGE_NAME: RUNMQ_PREFIX + "router",
|
|
118
|
+
DEAD_LETTER_ROUTER_EXCHANGE_NAME: RUNMQ_PREFIX + "dead_letter_router",
|
|
119
|
+
RETRY_DELAY_QUEUE_PREFIX: RUNMQ_PREFIX + "retry_delay_",
|
|
120
|
+
DLQ_QUEUE_PREFIX: RUNMQ_PREFIX + "dlq_"
|
|
121
|
+
};
|
|
122
|
+
var DEFAULTS = {
|
|
123
|
+
RECONNECT_DELAY: 5e3,
|
|
124
|
+
MAX_RECONNECT_ATTEMPTS: 5,
|
|
125
|
+
PREFETCH_COUNT: 10,
|
|
126
|
+
PROCESSING_ATTEMPTS: 1,
|
|
127
|
+
PROCESSING_RETRY_DELAY: 1e3
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
// src/core/message/RabbitMQMessage.ts
|
|
131
|
+
var RabbitMQMessage = class _RabbitMQMessage {
|
|
132
|
+
constructor(message, id = RunMQUtils.generateUUID(), correlationId = RunMQUtils.generateUUID(), channel, amqpMessage = null, headers = {}) {
|
|
133
|
+
this.message = message;
|
|
134
|
+
this.id = id;
|
|
135
|
+
this.correlationId = correlationId;
|
|
136
|
+
this.channel = channel;
|
|
137
|
+
this.amqpMessage = amqpMessage;
|
|
138
|
+
this.headers = headers;
|
|
139
|
+
}
|
|
140
|
+
static from(messageData, channel, props, amqpMessage = null) {
|
|
141
|
+
return new _RabbitMQMessage(
|
|
142
|
+
messageData,
|
|
143
|
+
props.id,
|
|
144
|
+
props.correlationId,
|
|
145
|
+
channel,
|
|
146
|
+
amqpMessage,
|
|
147
|
+
{}
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
// src/core/consumer/processors/RunMQSucceededMessageAcknowledgerProcessor.ts
|
|
153
|
+
var RunMQSucceededMessageAcknowledgerProcessor = class {
|
|
154
|
+
constructor(consumer) {
|
|
155
|
+
this.consumer = consumer;
|
|
156
|
+
}
|
|
157
|
+
async consume(message) {
|
|
158
|
+
const result = await this.consumer.consume(message);
|
|
159
|
+
if (result) {
|
|
160
|
+
message.channel.ack(message.amqpMessage);
|
|
161
|
+
}
|
|
162
|
+
return result;
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
// src/core/consumer/processors/RunMQFailedMessageRejecterProcessor.ts
|
|
167
|
+
var RunMQFailedMessageRejecterProcessor = class {
|
|
168
|
+
constructor(consumer) {
|
|
169
|
+
this.consumer = consumer;
|
|
170
|
+
}
|
|
171
|
+
async consume(message) {
|
|
172
|
+
try {
|
|
173
|
+
return await this.consumer.consume(message);
|
|
174
|
+
} catch (e) {
|
|
175
|
+
message.channel.nack(message.amqpMessage, false, false);
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
// src/core/consumer/ConsumerCreatorUtils.ts
|
|
182
|
+
var ConsumerCreatorUtils = class {
|
|
183
|
+
static getDLQTopicName(topic) {
|
|
184
|
+
return Constants.DLQ_QUEUE_PREFIX + topic;
|
|
185
|
+
}
|
|
186
|
+
static getRetryDelayTopicName(topic) {
|
|
187
|
+
return Constants.RETRY_DELAY_QUEUE_PREFIX + topic;
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
// src/core/consumer/processors/RunMQRetriesCheckerProcessor.ts
|
|
192
|
+
var _a;
|
|
193
|
+
var RunMQRetriesCheckerProcessor = class {
|
|
194
|
+
constructor(consumer, config, DLQPublisher, logger) {
|
|
195
|
+
this.consumer = consumer;
|
|
196
|
+
this.config = config;
|
|
197
|
+
this.DLQPublisher = DLQPublisher;
|
|
198
|
+
this.logger = logger;
|
|
199
|
+
this.maxAttempts = (_a = this.config.attempts) != null ? _a : DEFAULTS.PROCESSING_ATTEMPTS;
|
|
200
|
+
}
|
|
201
|
+
async consume(message) {
|
|
202
|
+
try {
|
|
203
|
+
return await this.consumer.consume(message);
|
|
204
|
+
} catch (e) {
|
|
205
|
+
if (this.hasReachedMaxRetries(message)) {
|
|
206
|
+
this.logMaxRetriesReached(message);
|
|
207
|
+
this.moveToFinalDeadLetter(message);
|
|
208
|
+
this.acknowledgeMessage(message);
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
throw e;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
hasReachedMaxRetries(message) {
|
|
215
|
+
const rejectedCount = this.getRejectionCount(message);
|
|
216
|
+
return rejectedCount >= this.maxAttempts;
|
|
217
|
+
}
|
|
218
|
+
logMaxRetriesReached(message) {
|
|
219
|
+
this.logger.error(
|
|
220
|
+
`Message reached maximum attempts. Moving to dead-letter queue.`,
|
|
221
|
+
{
|
|
222
|
+
message: message.message,
|
|
223
|
+
attempts: this.getRejectionCount(message),
|
|
224
|
+
max: this.maxAttempts
|
|
225
|
+
}
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
moveToFinalDeadLetter(message) {
|
|
229
|
+
this.DLQPublisher.publish(ConsumerCreatorUtils.getDLQTopicName(this.config.name), message);
|
|
230
|
+
}
|
|
231
|
+
acknowledgeMessage(message) {
|
|
232
|
+
try {
|
|
233
|
+
message.channel.ack(message.amqpMessage, false);
|
|
234
|
+
} catch (e) {
|
|
235
|
+
const error = new Error("A message acknowledge failed after publishing to final dead letter");
|
|
236
|
+
this.logger.error(error.message, { cause: e instanceof Error ? e.message : String(e) });
|
|
237
|
+
throw error;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
getRejectionCount(message) {
|
|
241
|
+
var _a2;
|
|
242
|
+
const xDeath = (_a2 = message.headers) == null ? void 0 : _a2["x-death"];
|
|
243
|
+
if (!Array.isArray(xDeath)) return 1;
|
|
244
|
+
const deathRecord = xDeath.filter((entry) => entry && entry.reason == "rejected")[0];
|
|
245
|
+
return deathRecord ? deathRecord.count + 1 : 1;
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
// src/core/consumer/processors/RunMQFailureLoggerProcessor.ts
|
|
250
|
+
var RunMQFailureLoggerProcessor = class {
|
|
251
|
+
constructor(consumer, logger) {
|
|
252
|
+
this.consumer = consumer;
|
|
253
|
+
this.logger = logger;
|
|
254
|
+
}
|
|
255
|
+
async consume(message) {
|
|
256
|
+
try {
|
|
257
|
+
return await this.consumer.consume(message);
|
|
258
|
+
} catch (e) {
|
|
259
|
+
this.logger.error(
|
|
260
|
+
"Message processing failed",
|
|
261
|
+
{
|
|
262
|
+
message: message.message
|
|
263
|
+
},
|
|
264
|
+
e instanceof Error ? e.stack : void 0
|
|
265
|
+
);
|
|
266
|
+
throw e;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
// src/core/consumer/processors/RunMQBaseProcessor.ts
|
|
272
|
+
var RunMQBaseProcessor = class {
|
|
273
|
+
constructor(handler, processorConfig, serializer) {
|
|
274
|
+
this.handler = handler;
|
|
275
|
+
this.processorConfig = processorConfig;
|
|
276
|
+
this.serializer = serializer;
|
|
277
|
+
}
|
|
278
|
+
async consume(message) {
|
|
279
|
+
const rabbitMQMessage = this.serializer.deserialize(message.message, this.processorConfig);
|
|
280
|
+
await this.handler(rabbitMQMessage);
|
|
281
|
+
return true;
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
// src/core/consumer/processors/RunMQExceptionLoggerProcessor.ts
|
|
286
|
+
var RunMQExceptionLoggerProcessor = class {
|
|
287
|
+
constructor(consumer, logger) {
|
|
288
|
+
this.consumer = consumer;
|
|
289
|
+
this.logger = logger;
|
|
290
|
+
}
|
|
291
|
+
async consume(message) {
|
|
292
|
+
try {
|
|
293
|
+
return await this.consumer.consume(message);
|
|
294
|
+
} catch (e) {
|
|
295
|
+
if (e instanceof Error) {
|
|
296
|
+
this.logger.error(e.message, e.stack);
|
|
297
|
+
throw e;
|
|
298
|
+
} else {
|
|
299
|
+
const errorString = JSON.stringify(e);
|
|
300
|
+
this.logger.error(errorString);
|
|
301
|
+
throw new Error(errorString);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
// src/core/message/RunMQMessage.ts
|
|
308
|
+
var RunMQMessage = class {
|
|
309
|
+
static isValid(obj) {
|
|
310
|
+
if (typeof obj === "object" && obj !== null) {
|
|
311
|
+
return "message" in obj && "meta" in obj && typeof obj.message === "object" && obj.message !== null && Array.isArray(obj.message) === false && typeof obj.meta === "object" && obj.meta !== null && "id" in obj.meta && "correlationId" in obj.meta && "publishedAt" in obj.meta && typeof obj.meta.id === "string" && typeof obj.meta.correlationId === "string" && typeof obj.meta.publishedAt === "number";
|
|
312
|
+
}
|
|
313
|
+
return false;
|
|
314
|
+
}
|
|
315
|
+
constructor(message, meta) {
|
|
316
|
+
this.message = message;
|
|
317
|
+
this.meta = meta;
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
var RunMQMessageMeta = class {
|
|
321
|
+
constructor(id, publishedAt, correlationId) {
|
|
322
|
+
this.id = id;
|
|
323
|
+
this.correlationId = correlationId;
|
|
324
|
+
this.publishedAt = publishedAt;
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
// src/core/serializers/deserializer/validation/AjvSchemaValidator.ts
|
|
329
|
+
import Ajv from "ajv";
|
|
330
|
+
var AjvSchemaValidator = class {
|
|
331
|
+
constructor() {
|
|
332
|
+
this.lastValidator = null;
|
|
333
|
+
this.ajv = new Ajv({
|
|
334
|
+
allErrors: true,
|
|
335
|
+
verbose: true,
|
|
336
|
+
strict: true
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
validate(schema, data) {
|
|
340
|
+
this.lastValidator = this.ajv.compile(schema);
|
|
341
|
+
return this.lastValidator(data);
|
|
342
|
+
}
|
|
343
|
+
getError() {
|
|
344
|
+
if (!this.lastValidator || !this.lastValidator.errors) {
|
|
345
|
+
return null;
|
|
346
|
+
}
|
|
347
|
+
return JSON.stringify(this.lastValidator.errors);
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
// src/core/serializers/deserializer/validation/ValidatorFactory.ts
|
|
352
|
+
var validatorCache = /* @__PURE__ */ new Map();
|
|
353
|
+
function getValidator(schemaType) {
|
|
354
|
+
const cached = validatorCache.get(schemaType);
|
|
355
|
+
if (cached) {
|
|
356
|
+
return cached;
|
|
357
|
+
}
|
|
358
|
+
let validator;
|
|
359
|
+
switch (schemaType) {
|
|
360
|
+
case "ajv":
|
|
361
|
+
validator = new AjvSchemaValidator();
|
|
362
|
+
break;
|
|
363
|
+
default:
|
|
364
|
+
throw new RunMQException(Exceptions.UNSUPPORTED_SCHEMA, { schemaType });
|
|
365
|
+
}
|
|
366
|
+
validatorCache.set(schemaType, validator);
|
|
367
|
+
return validator;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// src/core/serializers/deserializer/DefaultDeserializer.ts
|
|
371
|
+
var DeserializationError = class extends Error {
|
|
372
|
+
constructor(message, cause) {
|
|
373
|
+
super(message);
|
|
374
|
+
this.cause = cause;
|
|
375
|
+
this.name = "DeserializationError";
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
var RunMQSchemaValidationError = class extends Error {
|
|
379
|
+
constructor(message, error) {
|
|
380
|
+
super(message);
|
|
381
|
+
this.error = error;
|
|
382
|
+
this.name = "ValidationError";
|
|
383
|
+
}
|
|
384
|
+
};
|
|
385
|
+
var DefaultDeserializer = class {
|
|
386
|
+
deserialize(data, processorConfig) {
|
|
387
|
+
if (!data) {
|
|
388
|
+
throw new DeserializationError("Input must be a non-empty string");
|
|
389
|
+
}
|
|
390
|
+
let parsed;
|
|
391
|
+
try {
|
|
392
|
+
parsed = JSON.parse(data);
|
|
393
|
+
} catch (error) {
|
|
394
|
+
throw new DeserializationError(
|
|
395
|
+
`Failed to parse JSON: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
396
|
+
error
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
if (!RunMQMessage.isValid(parsed)) {
|
|
400
|
+
throw new RunMQSchemaValidationError(
|
|
401
|
+
"Invalid message format: not valid RunMQMessage structure"
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
const typedParsed = parsed;
|
|
405
|
+
if (processorConfig.messageSchema) {
|
|
406
|
+
const { type, schema } = processorConfig.messageSchema;
|
|
407
|
+
const validator = getValidator(type);
|
|
408
|
+
if (!validator.validate(schema, typedParsed.message)) {
|
|
409
|
+
throw new RunMQSchemaValidationError(
|
|
410
|
+
"Message validation failed against schema",
|
|
411
|
+
validator.getError() || void 0
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
const message = typedParsed.message;
|
|
416
|
+
return new RunMQMessage(
|
|
417
|
+
message,
|
|
418
|
+
new RunMQMessageMeta(
|
|
419
|
+
typedParsed.meta.id,
|
|
420
|
+
typedParsed.meta.publishedAt,
|
|
421
|
+
typedParsed.meta.correlationId
|
|
422
|
+
)
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
// src/core/publisher/producers/RunMQFailureLoggerProducer.ts
|
|
428
|
+
var RunMQFailureLoggerProducer = class {
|
|
429
|
+
constructor(producer, logger) {
|
|
430
|
+
this.producer = producer;
|
|
431
|
+
this.logger = logger;
|
|
432
|
+
}
|
|
433
|
+
publish(topic, message) {
|
|
434
|
+
try {
|
|
435
|
+
this.producer.publish(topic, message);
|
|
436
|
+
} catch (e) {
|
|
437
|
+
this.logger.error("Message publishing failed", {
|
|
438
|
+
message,
|
|
439
|
+
error: e instanceof Error ? e.message : JSON.stringify(e),
|
|
440
|
+
stack: e instanceof Error ? e.stack : void 0
|
|
441
|
+
});
|
|
442
|
+
throw e;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
// src/core/publisher/producers/RunMQBaseProducer.ts
|
|
448
|
+
var RunMQBaseProducer = class {
|
|
449
|
+
constructor(serializer, exchange = Constants.ROUTER_EXCHANGE_NAME) {
|
|
450
|
+
this.serializer = serializer;
|
|
451
|
+
this.exchange = exchange;
|
|
452
|
+
}
|
|
453
|
+
publish(topic, message) {
|
|
454
|
+
const runMQMessage = new RunMQMessage(
|
|
455
|
+
message.message,
|
|
456
|
+
new RunMQMessageMeta(
|
|
457
|
+
message.id,
|
|
458
|
+
Date.now(),
|
|
459
|
+
message.correlationId
|
|
460
|
+
)
|
|
461
|
+
);
|
|
462
|
+
const serialized = this.serializer.serialize(runMQMessage);
|
|
463
|
+
message.channel.publish(this.exchange, topic, Buffer.from(serialized), {
|
|
464
|
+
correlationId: message.correlationId,
|
|
465
|
+
messageId: message.id,
|
|
466
|
+
headers: message.headers
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
};
|
|
470
|
+
|
|
471
|
+
// src/core/serializers/DefaultSerializer.ts
|
|
472
|
+
var SerializationError = class extends Error {
|
|
473
|
+
constructor(message, cause) {
|
|
474
|
+
super(message);
|
|
475
|
+
this.cause = cause;
|
|
476
|
+
this.name = "SerializationError";
|
|
477
|
+
}
|
|
478
|
+
};
|
|
479
|
+
var DefaultSerializer = class {
|
|
480
|
+
serialize(data) {
|
|
481
|
+
try {
|
|
482
|
+
return JSON.stringify(data);
|
|
483
|
+
} catch (error) {
|
|
484
|
+
throw new SerializationError(
|
|
485
|
+
`Failed to serialize message: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
486
|
+
error
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
// src/core/publisher/RunMQPublisherCreator.ts
|
|
493
|
+
var RunMQPublisherCreator = class {
|
|
494
|
+
constructor(logger) {
|
|
495
|
+
this.logger = logger;
|
|
496
|
+
}
|
|
497
|
+
createPublisher(exchange = Constants.ROUTER_EXCHANGE_NAME) {
|
|
498
|
+
return new RunMQFailureLoggerProducer(
|
|
499
|
+
new RunMQBaseProducer(new DefaultSerializer(), exchange),
|
|
500
|
+
this.logger
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
};
|
|
504
|
+
|
|
505
|
+
// src/core/consumer/RunMQConsumerCreator.ts
|
|
506
|
+
var RunMQConsumerCreator = class {
|
|
507
|
+
constructor(defaultChannel, client, logger) {
|
|
508
|
+
this.defaultChannel = defaultChannel;
|
|
509
|
+
this.client = client;
|
|
510
|
+
this.logger = logger;
|
|
511
|
+
}
|
|
512
|
+
async createConsumer(consumerConfiguration) {
|
|
513
|
+
await this.assertQueues(consumerConfiguration);
|
|
514
|
+
await this.bindQueues(consumerConfiguration);
|
|
515
|
+
for (let i = 0; i < consumerConfiguration.processorConfig.consumersCount; i++) {
|
|
516
|
+
await this.runProcessor(consumerConfiguration);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
async runProcessor(consumerConfiguration) {
|
|
520
|
+
const consumerChannel = await this.getProcessorChannel();
|
|
521
|
+
const DLQPublisher = new RunMQPublisherCreator(this.logger).createPublisher(Constants.DEAD_LETTER_ROUTER_EXCHANGE_NAME);
|
|
522
|
+
await consumerChannel.prefetch(DEFAULTS.PREFETCH_COUNT);
|
|
523
|
+
await consumerChannel.consume(consumerConfiguration.processorConfig.name, async (msg) => {
|
|
524
|
+
if (msg) {
|
|
525
|
+
const rabbitmqMessage = new RabbitMQMessage(
|
|
526
|
+
msg.content.toString(),
|
|
527
|
+
msg.properties.messageId,
|
|
528
|
+
msg.properties.correlationId,
|
|
529
|
+
consumerChannel,
|
|
530
|
+
msg,
|
|
531
|
+
msg.properties.headers
|
|
532
|
+
);
|
|
533
|
+
return new RunMQExceptionLoggerProcessor(
|
|
534
|
+
new RunMQSucceededMessageAcknowledgerProcessor(
|
|
535
|
+
new RunMQFailedMessageRejecterProcessor(
|
|
536
|
+
new RunMQRetriesCheckerProcessor(
|
|
537
|
+
new RunMQFailureLoggerProcessor(
|
|
538
|
+
new RunMQBaseProcessor(
|
|
539
|
+
consumerConfiguration.processor,
|
|
540
|
+
consumerConfiguration.processorConfig,
|
|
541
|
+
new DefaultDeserializer()
|
|
542
|
+
),
|
|
543
|
+
this.logger
|
|
544
|
+
),
|
|
545
|
+
consumerConfiguration.processorConfig,
|
|
546
|
+
DLQPublisher,
|
|
547
|
+
this.logger
|
|
548
|
+
)
|
|
549
|
+
)
|
|
550
|
+
),
|
|
551
|
+
this.logger
|
|
552
|
+
).consume(rabbitmqMessage);
|
|
553
|
+
}
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
async assertQueues(consumerConfiguration) {
|
|
557
|
+
var _a2;
|
|
558
|
+
await this.defaultChannel.assertQueue(consumerConfiguration.processorConfig.name, {
|
|
559
|
+
durable: true,
|
|
560
|
+
deadLetterExchange: Constants.DEAD_LETTER_ROUTER_EXCHANGE_NAME,
|
|
561
|
+
deadLetterRoutingKey: consumerConfiguration.processorConfig.name
|
|
562
|
+
});
|
|
563
|
+
await this.defaultChannel.assertQueue(ConsumerCreatorUtils.getRetryDelayTopicName(consumerConfiguration.processorConfig.name), {
|
|
564
|
+
durable: true,
|
|
565
|
+
deadLetterExchange: Constants.ROUTER_EXCHANGE_NAME,
|
|
566
|
+
messageTtl: (_a2 = consumerConfiguration.processorConfig.attemptsDelay) != null ? _a2 : DEFAULTS.PROCESSING_RETRY_DELAY
|
|
567
|
+
});
|
|
568
|
+
await this.defaultChannel.assertQueue(ConsumerCreatorUtils.getDLQTopicName(consumerConfiguration.processorConfig.name), {
|
|
569
|
+
durable: true,
|
|
570
|
+
deadLetterExchange: Constants.ROUTER_EXCHANGE_NAME,
|
|
571
|
+
deadLetterRoutingKey: consumerConfiguration.processorConfig.name
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
async bindQueues(consumerConfiguration) {
|
|
575
|
+
await this.defaultChannel.bindQueue(
|
|
576
|
+
consumerConfiguration.processorConfig.name,
|
|
577
|
+
Constants.ROUTER_EXCHANGE_NAME,
|
|
578
|
+
consumerConfiguration.topic
|
|
579
|
+
);
|
|
580
|
+
await this.defaultChannel.bindQueue(
|
|
581
|
+
consumerConfiguration.processorConfig.name,
|
|
582
|
+
Constants.ROUTER_EXCHANGE_NAME,
|
|
583
|
+
consumerConfiguration.processorConfig.name
|
|
584
|
+
);
|
|
585
|
+
await this.defaultChannel.bindQueue(
|
|
586
|
+
ConsumerCreatorUtils.getRetryDelayTopicName(consumerConfiguration.processorConfig.name),
|
|
587
|
+
Constants.DEAD_LETTER_ROUTER_EXCHANGE_NAME,
|
|
588
|
+
consumerConfiguration.processorConfig.name
|
|
589
|
+
);
|
|
590
|
+
await this.defaultChannel.bindQueue(
|
|
591
|
+
ConsumerCreatorUtils.getDLQTopicName(consumerConfiguration.processorConfig.name),
|
|
592
|
+
Constants.DEAD_LETTER_ROUTER_EXCHANGE_NAME,
|
|
593
|
+
ConsumerCreatorUtils.getDLQTopicName(consumerConfiguration.processorConfig.name)
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
async getProcessorChannel() {
|
|
597
|
+
return await this.client.getChannel();
|
|
598
|
+
}
|
|
599
|
+
};
|
|
600
|
+
|
|
601
|
+
// src/core/consumer/ConsumerConfiguration.ts
|
|
602
|
+
var ConsumerConfiguration = class {
|
|
603
|
+
constructor(topic, processorConfig, processor) {
|
|
604
|
+
this.topic = topic;
|
|
605
|
+
this.processorConfig = processorConfig;
|
|
606
|
+
this.processor = processor;
|
|
607
|
+
}
|
|
608
|
+
};
|
|
609
|
+
|
|
610
|
+
// src/core/logging/RunMQConsoleLogger.ts
|
|
611
|
+
var RunMQConsoleLogger = class {
|
|
612
|
+
constructor() {
|
|
613
|
+
this.prefix = "[RunMQ] - ";
|
|
614
|
+
}
|
|
615
|
+
log(message) {
|
|
616
|
+
console.log(this.formatMessage(message));
|
|
617
|
+
}
|
|
618
|
+
error(message, ...optionalParams) {
|
|
619
|
+
console.error(this.formatMessage(message), ...optionalParams);
|
|
620
|
+
}
|
|
621
|
+
warn(message, ...optionalParams) {
|
|
622
|
+
console.warn(this.formatMessage(message), ...optionalParams);
|
|
623
|
+
}
|
|
624
|
+
info(message, ...optionalParams) {
|
|
625
|
+
console.info(this.formatMessage(message), ...optionalParams);
|
|
626
|
+
}
|
|
627
|
+
debug(message, ...optionalParams) {
|
|
628
|
+
console.debug(this.formatMessage(message), ...optionalParams);
|
|
629
|
+
}
|
|
630
|
+
verbose(message, ...optionalParams) {
|
|
631
|
+
console.debug(this.formatMessage(message), ...optionalParams);
|
|
632
|
+
}
|
|
633
|
+
formatMessage(message) {
|
|
634
|
+
return `${this.prefix} ${message}`;
|
|
635
|
+
}
|
|
636
|
+
};
|
|
637
|
+
|
|
638
|
+
// src/core/message/RabbitMQMessageProperties.ts
|
|
639
|
+
var RabbitMQMessageProperties = class {
|
|
640
|
+
constructor(id, correlationId) {
|
|
641
|
+
this.id = id;
|
|
642
|
+
this.correlationId = correlationId;
|
|
643
|
+
}
|
|
644
|
+
};
|
|
645
|
+
|
|
646
|
+
// src/core/RunMQ.ts
|
|
647
|
+
var RunMQ = class _RunMQ {
|
|
648
|
+
constructor(config, logger) {
|
|
649
|
+
this.retryAttempts = 0;
|
|
650
|
+
var _a2, _b;
|
|
651
|
+
this.logger = logger;
|
|
652
|
+
this.config = __spreadProps(__spreadValues({}, config), {
|
|
653
|
+
reconnectDelay: (_a2 = config.reconnectDelay) != null ? _a2 : DEFAULTS.RECONNECT_DELAY,
|
|
654
|
+
maxReconnectAttempts: (_b = config.maxReconnectAttempts) != null ? _b : DEFAULTS.MAX_RECONNECT_ATTEMPTS
|
|
655
|
+
});
|
|
656
|
+
this.amqplibClient = new AmqplibClient(this.config);
|
|
657
|
+
}
|
|
658
|
+
/**
|
|
659
|
+
* Starts the RunMQ instance by establishing a connection to RabbitMQ and initializing necessary components.
|
|
660
|
+
* @param config The configuration for the RunMQ connection @see RunMQConnectionConfig
|
|
661
|
+
* @param logger (Optional) A custom logger implementing the RunMQLogger interface; if not provided, a default console logger will be used
|
|
662
|
+
* @returns A promise that resolves to the initialized RunMQ instance
|
|
663
|
+
*/
|
|
664
|
+
static async start(config, logger = new RunMQConsoleLogger()) {
|
|
665
|
+
const instance = new _RunMQ(config, logger);
|
|
666
|
+
await instance.connectWithRetry();
|
|
667
|
+
await instance.initialize();
|
|
668
|
+
return instance;
|
|
669
|
+
}
|
|
670
|
+
/**
|
|
671
|
+
* Processes messages from the specified topic using the provided processor function and configuration.
|
|
672
|
+
* @param topic The name of the topic to process messages from, it should match the name used during publishing
|
|
673
|
+
* @param config The configuration for the message processor @see RunMQProcessorConfiguration
|
|
674
|
+
* @param processor The function that will process the incoming messages
|
|
675
|
+
*/
|
|
676
|
+
async process(topic, config, processor) {
|
|
677
|
+
const consumer = new RunMQConsumerCreator(this.defaultChannel, this.amqplibClient, this.logger);
|
|
678
|
+
await consumer.createConsumer(new ConsumerConfiguration(topic, config, processor));
|
|
679
|
+
}
|
|
680
|
+
/**
|
|
681
|
+
* Publishes a message to the specified topic with an optional correlation ID
|
|
682
|
+
* @param topic The name of the topic to publish the message to
|
|
683
|
+
* @param message The message payload to be published
|
|
684
|
+
* @param correlationId (Optional) A unique identifier for correlating messages; if not provided, a new UUID will be generated
|
|
685
|
+
*/
|
|
686
|
+
publish(topic, message, correlationId = RunMQUtils.generateUUID()) {
|
|
687
|
+
if (!this.publisher) {
|
|
688
|
+
throw new RunMQException(Exceptions.NOT_INITIALIZED, {});
|
|
689
|
+
}
|
|
690
|
+
RunMQUtils.assertRecord(message);
|
|
691
|
+
this.publisher.publish(
|
|
692
|
+
topic,
|
|
693
|
+
RabbitMQMessage.from(
|
|
694
|
+
message,
|
|
695
|
+
this.defaultChannel,
|
|
696
|
+
new RabbitMQMessageProperties(RunMQUtils.generateUUID(), correlationId)
|
|
697
|
+
)
|
|
698
|
+
);
|
|
699
|
+
this.logger.info(`Published message`, {
|
|
700
|
+
topic,
|
|
701
|
+
correlationId,
|
|
702
|
+
message
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
/**
|
|
706
|
+
* Disconnects from RabbitMQ, handling any errors that may occur during the disconnection process.
|
|
707
|
+
*/
|
|
708
|
+
async disconnect() {
|
|
709
|
+
try {
|
|
710
|
+
await this.amqplibClient.disconnect();
|
|
711
|
+
} catch (error) {
|
|
712
|
+
throw new RunMQException(
|
|
713
|
+
Exceptions.CONNECTION_NOT_ESTABLISHED,
|
|
714
|
+
{
|
|
715
|
+
error: error instanceof Error ? error.message : String(error)
|
|
716
|
+
}
|
|
717
|
+
);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
/**
|
|
721
|
+
* Checks if the connection is currently active.
|
|
722
|
+
*/
|
|
723
|
+
isActive() {
|
|
724
|
+
return this.amqplibClient.isActive();
|
|
725
|
+
}
|
|
726
|
+
async connectWithRetry() {
|
|
727
|
+
const maxAttempts = this.config.maxReconnectAttempts;
|
|
728
|
+
const delay = this.config.reconnectDelay;
|
|
729
|
+
while (this.retryAttempts < maxAttempts) {
|
|
730
|
+
try {
|
|
731
|
+
await this.amqplibClient.connect();
|
|
732
|
+
this.logger.log("Successfully connected to RabbitMQ");
|
|
733
|
+
this.retryAttempts = 0;
|
|
734
|
+
return;
|
|
735
|
+
} catch (error) {
|
|
736
|
+
this.retryAttempts++;
|
|
737
|
+
this.logger.error(`Connection attempt ${this.retryAttempts}/${maxAttempts} failed:`, error);
|
|
738
|
+
if (this.retryAttempts >= maxAttempts) {
|
|
739
|
+
throw new RunMQException(
|
|
740
|
+
Exceptions.EXCEEDING_CONNECTION_ATTEMPTS,
|
|
741
|
+
{
|
|
742
|
+
attempts: maxAttempts,
|
|
743
|
+
error: error instanceof Error ? error.message : String(error)
|
|
744
|
+
}
|
|
745
|
+
);
|
|
746
|
+
}
|
|
747
|
+
this.logger.error(`Retrying in ${delay}ms...`);
|
|
748
|
+
await RunMQUtils.delay(delay);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
async initialize() {
|
|
753
|
+
this.defaultChannel = await this.amqplibClient.getChannel();
|
|
754
|
+
await this.defaultChannel.assertExchange(Constants.ROUTER_EXCHANGE_NAME, "direct", { durable: true });
|
|
755
|
+
await this.defaultChannel.assertExchange(Constants.DEAD_LETTER_ROUTER_EXCHANGE_NAME, "direct", { durable: true });
|
|
756
|
+
this.publisher = new RunMQPublisherCreator(this.logger).createPublisher();
|
|
757
|
+
}
|
|
758
|
+
};
|
|
759
|
+
export {
|
|
760
|
+
RunMQ
|
|
761
|
+
};
|
|
762
|
+
//# sourceMappingURL=index.js.map
|