runmq 1.3.0 → 1.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/README.md CHANGED
@@ -23,8 +23,9 @@ Whether you’re running <b>background jobs</b>, designing an <b>event-driven ar
23
23
  - **Isolated Queues per Processor**: Each processor gets its own dedicated queue and DLQ, ensuring full isolation and predictable behavior across services.
24
24
  - **Schema Validation**: Optional JSON Schema validation powered by AJV for safer message handling and data integrity.
25
25
  - **Concurrent Consumers**: Scale either horizontally (multiple instances) or vertically (multiple consumers per queue, leveraging RabbitMQ prefetch) to maximize throughput and efficiency.
26
- - **RabbitMQ Durability & Acknowledgements**: Leverages RabbitMQs persistent storage and acknowledgment model to guarantee at-least-once delivery, even across restarts and failures.
26
+ - **RabbitMQ Durability & Acknowledgements**: Leverages RabbitMQ's persistent storage and acknowledgment model to guarantee at-least-once delivery, even across restarts and failures.
27
27
  - **Custom Logging**: Plug in your own logger or use the default console logger for full control over message visibility.
28
+ - **Management Dashboard**: A web-based dashboard for real-time monitoring and management of queues, DLQs, and message processing. [Check it out!](https://github.com/runmq/pulse)
28
29
 
29
30
  ## Installation
30
31
 
@@ -212,7 +213,24 @@ RunMQ can leverage RabbitMQ policies to manage the delay between attempts, it's
212
213
  #### Benefits
213
214
  - Flexible and easy management of retry delays
214
215
  - Reduces operational overhead
215
- - Fully compatible with RunMQs retry and DLQ mechanisms
216
+ - Fully compatible with RunMQ's retry and DLQ mechanisms
217
+
218
+ ### Queue Metadata Storage
219
+
220
+ RunMQ automatically stores queue metadata (such as max retries and creation timestamp) using RabbitMQ's parameters API. This enables external tools and dashboards to discover RunMQ-managed queues and understand their configuration without direct access to the application code.
221
+
222
+ When a processor is configured, RunMQ creates a metadata parameter that stores:
223
+ - **Version**: Schema version for future-proof migrations.
224
+ - **Max Retries**: The configured retry limit for the queue.
225
+ - **Created At**: ISO 8601 timestamp when the queue was first configured.
226
+ - **Updated At**: ISO 8601 timestamp when the configuration was last changed (if applicable).
227
+
228
+ #### Benefits
229
+ - **Dashboard Integration**: External monitoring tools and dashboards can query RabbitMQ's management API to retrieve queue metadata and display topology information (e.g., "10 retries with 5s delay, then to DLQ").
230
+ - **Self-Documenting Queues**: Queue configurations are discoverable directly from RabbitMQ, without needing access to application source code.
231
+ - **Automatic Updates**: When processor configuration changes, metadata is automatically updated while preserving the original creation timestamp.
232
+
233
+ > **Note**: This feature requires RabbitMQ Management Plugin to be enabled for external tools to query the metadata parameters and for the parameters to be set.
216
234
 
217
235
  ### Custom Logger
218
236
 
package/dist/index.cjs CHANGED
@@ -244,10 +244,39 @@ var RabbitMQClientChannel = class {
244
244
  }
245
245
  };
246
246
 
247
+ // src/core/logging/RunMQConsoleLogger.ts
248
+ var RunMQConsoleLogger = class {
249
+ constructor() {
250
+ this.prefix = "[RunMQ] - ";
251
+ }
252
+ log(message) {
253
+ console.log(this.formatMessage(message));
254
+ }
255
+ error(message, ...optionalParams) {
256
+ console.error(this.formatMessage(message), ...optionalParams);
257
+ }
258
+ warn(message, ...optionalParams) {
259
+ console.warn(this.formatMessage(message), ...optionalParams);
260
+ }
261
+ info(message, ...optionalParams) {
262
+ console.info(this.formatMessage(message), ...optionalParams);
263
+ }
264
+ debug(message, ...optionalParams) {
265
+ console.debug(this.formatMessage(message), ...optionalParams);
266
+ }
267
+ verbose(message, ...optionalParams) {
268
+ console.debug(this.formatMessage(message), ...optionalParams);
269
+ }
270
+ formatMessage(message) {
271
+ return `${this.prefix} ${message}`;
272
+ }
273
+ };
274
+
247
275
  // src/core/clients/RabbitMQClientAdapter.ts
248
276
  var RabbitMQClientAdapter = class {
249
- constructor(config) {
277
+ constructor(config, logger = new RunMQConsoleLogger()) {
250
278
  this.config = config;
279
+ this.logger = logger;
251
280
  this.isConnected = false;
252
281
  this.acquiredChannels = [];
253
282
  }
@@ -271,17 +300,17 @@ var RabbitMQClientAdapter = class {
271
300
  connectionTimeout: 5e3
272
301
  });
273
302
  this.connection.on("error", (err) => {
274
- console.error("RabbitMQ connection error:", err);
303
+ this.logger.error("RabbitMQ connection error:", { error: err });
275
304
  this.isConnected = false;
276
305
  });
277
306
  this.connection.on("connection", () => {
278
307
  this.isConnected = true;
279
308
  });
280
309
  this.connection.on("connection.blocked", (reason) => {
281
- console.warn("RabbitMQ connection blocked:", reason);
310
+ this.logger.warn("RabbitMQ connection blocked:", { reason });
282
311
  });
283
312
  this.connection.on("connection.unblocked", () => {
284
- console.info("RabbitMQ connection unblocked");
313
+ this.logger.info("RabbitMQ connection unblocked");
285
314
  });
286
315
  await this.connection.onConnect(5e3, true);
287
316
  this.isConnected = true;
@@ -368,7 +397,8 @@ var Constants = {
368
397
  DEAD_LETTER_ROUTER_EXCHANGE_NAME: RUNMQ_PREFIX + "dead_letter_router",
369
398
  RETRY_DELAY_QUEUE_PREFIX: RUNMQ_PREFIX + "retry_delay_",
370
399
  DLQ_QUEUE_PREFIX: RUNMQ_PREFIX + "dlq_",
371
- MESSAGE_TTL_OPERATOR_POLICY_PREFIX: RUNMQ_PREFIX + "message_ttl_operator_policy"
400
+ MESSAGE_TTL_OPERATOR_POLICY_PREFIX: RUNMQ_PREFIX + "message_ttl_operator_policy",
401
+ METADATA_STORE_PREFIX: RUNMQ_PREFIX + "metadata_"
372
402
  };
373
403
  var DEFAULTS = {
374
404
  RECONNECT_DELAY: 5e3,
@@ -446,6 +476,27 @@ var RunMQFailedMessageRejecterProcessor = class {
446
476
  }
447
477
  };
448
478
 
479
+ // src/core/message/RunMQMessage.ts
480
+ var RunMQMessage = class {
481
+ static isValid(obj) {
482
+ if (typeof obj === "object" && obj !== null) {
483
+ 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";
484
+ }
485
+ return false;
486
+ }
487
+ constructor(message, meta) {
488
+ this.message = message;
489
+ this.meta = meta;
490
+ }
491
+ };
492
+ var RunMQMessageMeta = class {
493
+ constructor(id, publishedAt, correlationId) {
494
+ this.id = id;
495
+ this.correlationId = correlationId;
496
+ this.publishedAt = publishedAt;
497
+ }
498
+ };
499
+
449
500
  // src/core/consumer/ConsumerCreatorUtils.ts
450
501
  var ConsumerCreatorUtils = class {
451
502
  static getDLQTopicName(topic) {
@@ -497,7 +548,28 @@ var RunMQRetriesCheckerProcessor = class {
497
548
  );
498
549
  }
499
550
  moveToFinalDeadLetter(message) {
500
- this.DLQPublisher.publish(ConsumerCreatorUtils.getDLQTopicName(this.config.name), message);
551
+ const originalPayload = this.extractOriginalPayload(message);
552
+ const dlqMessage = new RabbitMQMessage(
553
+ originalPayload,
554
+ message.id,
555
+ message.correlationId,
556
+ message.channel,
557
+ message.amqpMessage,
558
+ message.headers
559
+ );
560
+ this.DLQPublisher.publish(ConsumerCreatorUtils.getDLQTopicName(this.config.name), dlqMessage);
561
+ }
562
+ extractOriginalPayload(message) {
563
+ if (typeof message.message === "string") {
564
+ try {
565
+ const parsed = JSON.parse(message.message);
566
+ if (RunMQMessage.isValid(parsed)) {
567
+ return parsed.message;
568
+ }
569
+ } catch (e) {
570
+ }
571
+ }
572
+ return message.message;
501
573
  }
502
574
  acknowledgeMessage(message) {
503
575
  try {
@@ -575,27 +647,6 @@ var RunMQExceptionLoggerProcessor = class {
575
647
  }
576
648
  };
577
649
 
578
- // src/core/message/RunMQMessage.ts
579
- var RunMQMessage = class {
580
- static isValid(obj) {
581
- if (typeof obj === "object" && obj !== null) {
582
- 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";
583
- }
584
- return false;
585
- }
586
- constructor(message, meta) {
587
- this.message = message;
588
- this.meta = meta;
589
- }
590
- };
591
- var RunMQMessageMeta = class {
592
- constructor(id, publishedAt, correlationId) {
593
- this.id = id;
594
- this.correlationId = correlationId;
595
- this.publishedAt = publishedAt;
596
- }
597
- };
598
-
599
650
  // src/core/serializers/deserializer/validation/AjvSchemaValidator.ts
600
651
  var import_ajv = __toESM(require("ajv"), 1);
601
652
  var AjvSchemaValidator = class {
@@ -870,6 +921,91 @@ var RabbitMQManagementClient = class {
870
921
  return false;
871
922
  }
872
923
  }
924
+ /**
925
+ * Creates or updates a RabbitMQ parameter.
926
+ * Parameters are custom key-value stores that can hold any JSON data.
927
+ *
928
+ * @param name - The parameter name
929
+ * @param value - The parameter value (any JSON-serializable object)
930
+ */
931
+ async setParameter(name, value) {
932
+ try {
933
+ const url = `${this.config.url}/api/global-parameters/${encodeURIComponent(name)}`;
934
+ const response = await fetch(url, {
935
+ method: "PUT",
936
+ headers: {
937
+ "Content-Type": "application/json",
938
+ "Authorization": this.getAuthHeader()
939
+ },
940
+ body: JSON.stringify({ value })
941
+ });
942
+ if (!response.ok) {
943
+ const error = await response.text();
944
+ this.logger.error(`Failed to set parameter ${name}: ${response.status} - ${error}`);
945
+ return false;
946
+ }
947
+ this.logger.info(`Successfully set parameter: ${name}`);
948
+ return true;
949
+ } catch (error) {
950
+ this.logger.error(`Error setting parameter: ${error}`);
951
+ return false;
952
+ }
953
+ }
954
+ /**
955
+ * Gets a RabbitMQ parameter.
956
+ *
957
+ * @param name - The parameter name
958
+ */
959
+ async getParameter(name) {
960
+ try {
961
+ const url = `${this.config.url}/api/global-parameters/${encodeURIComponent(name)}`;
962
+ const response = await fetch(url, {
963
+ method: "GET",
964
+ headers: {
965
+ "Authorization": this.getAuthHeader()
966
+ }
967
+ });
968
+ if (!response.ok) {
969
+ if (response.status === 404) {
970
+ return null;
971
+ }
972
+ const error = await response.text();
973
+ this.logger.error(`Failed to get parameter ${name}: ${response.status} - ${error}`);
974
+ return null;
975
+ }
976
+ const data = await response.json();
977
+ return data.value;
978
+ } catch (error) {
979
+ this.logger.error(`Error getting parameter: ${error}`);
980
+ return null;
981
+ }
982
+ }
983
+ /**
984
+ * Deletes a RabbitMQ parameter.
985
+ *
986
+ * @param name - The parameter name
987
+ */
988
+ async deleteParameter(name) {
989
+ try {
990
+ const url = `${this.config.url}/api/global-parameters/${encodeURIComponent(name)}`;
991
+ const response = await fetch(url, {
992
+ method: "DELETE",
993
+ headers: {
994
+ "Authorization": this.getAuthHeader()
995
+ }
996
+ });
997
+ if (!response.ok && response.status !== 404) {
998
+ const error = await response.text();
999
+ this.logger.error(`Failed to delete parameter ${name}: ${response.status} - ${error}`);
1000
+ return false;
1001
+ }
1002
+ this.logger.info(`Successfully deleted parameter: ${name}`);
1003
+ return true;
1004
+ } catch (error) {
1005
+ this.logger.error(`Error deleting parameter: ${error}`);
1006
+ return false;
1007
+ }
1008
+ }
873
1009
  };
874
1010
 
875
1011
  // src/core/management/Policies/RabbitMQMessageTTLPolicy.ts
@@ -899,6 +1035,9 @@ var RunMQTTLPolicyManager = class {
899
1035
  }
900
1036
  }
901
1037
  async initialize() {
1038
+ if (this.isManagementPluginEnabled) {
1039
+ return;
1040
+ }
902
1041
  if (!this.managementClient) {
903
1042
  this.logger.warn("Management client not configured");
904
1043
  return;
@@ -931,21 +1070,166 @@ var RunMQTTLPolicyManager = class {
931
1070
  }
932
1071
  };
933
1072
 
1073
+ // src/core/management/Policies/RunMQQueueMetadata.ts
1074
+ var METADATA_SCHEMA_VERSION = 0;
1075
+
1076
+ // src/core/management/Policies/RabbitMQMetadata.ts
1077
+ var RabbitMQMetadata = class {
1078
+ /**
1079
+ * Creates metadata for a queue.
1080
+ * @param maxRetries - Maximum retry attempts configured for the queue
1081
+ * @param existingMetadata - Optional existing metadata to preserve createdAt
1082
+ * @returns RunMQQueueMetadata object
1083
+ */
1084
+ static createMetadataFor(maxRetries, existingMetadata) {
1085
+ var _a2;
1086
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1087
+ return __spreadValues({
1088
+ version: METADATA_SCHEMA_VERSION,
1089
+ maxRetries,
1090
+ createdAt: (_a2 = existingMetadata == null ? void 0 : existingMetadata.createdAt) != null ? _a2 : now
1091
+ }, existingMetadata ? { updatedAt: now } : {});
1092
+ }
1093
+ /**
1094
+ * Gets the parameter name for a given queue.
1095
+ */
1096
+ static getParameterName(queueName) {
1097
+ return Constants.METADATA_STORE_PREFIX + queueName;
1098
+ }
1099
+ };
1100
+
1101
+ // src/core/management/Policies/RunMQMetadataManager.ts
1102
+ var RunMQMetadataManager = class {
1103
+ constructor(logger, managementConfig) {
1104
+ this.logger = logger;
1105
+ this.managementConfig = managementConfig;
1106
+ this.managementClient = null;
1107
+ this.isManagementPluginEnabled = false;
1108
+ if (this.managementConfig) {
1109
+ this.managementClient = new RabbitMQManagementClient(this.managementConfig, this.logger);
1110
+ }
1111
+ }
1112
+ /**
1113
+ * Initialize the manager by checking if management plugin is available.
1114
+ */
1115
+ async initialize() {
1116
+ if (this.isManagementPluginEnabled) {
1117
+ return;
1118
+ }
1119
+ if (!this.managementClient) {
1120
+ this.logger.warn("Management client not configured - metadata storage disabled");
1121
+ return;
1122
+ }
1123
+ this.isManagementPluginEnabled = await this.managementClient.checkManagementPluginEnabled();
1124
+ if (!this.isManagementPluginEnabled) {
1125
+ this.logger.warn("RabbitMQ management plugin is not enabled - metadata storage disabled");
1126
+ } else {
1127
+ this.logger.info("RunMQ metadata storage initialized");
1128
+ }
1129
+ }
1130
+ /**
1131
+ * Store or update metadata for a queue.
1132
+ * If metadata already exists, preserves createdAt and sets updatedAt.
1133
+ *
1134
+ * @param queueName - The name of the queue
1135
+ * @param maxRetries - Maximum retry attempts
1136
+ * @returns true if metadata was stored successfully, false otherwise
1137
+ */
1138
+ async apply(queueName, maxRetries) {
1139
+ if (!this.isManagementPluginEnabled || !this.managementClient) {
1140
+ this.logger.warn(`Cannot store metadata for queue '${queueName}' - management plugin not available`);
1141
+ return false;
1142
+ }
1143
+ try {
1144
+ const existingMetadata = await this.getMetadata(queueName);
1145
+ const metadata = RabbitMQMetadata.createMetadataFor(
1146
+ maxRetries,
1147
+ existingMetadata != null ? existingMetadata : void 0
1148
+ );
1149
+ const paramName = RabbitMQMetadata.getParameterName(queueName);
1150
+ const success = await this.managementClient.setParameter(
1151
+ paramName,
1152
+ metadata
1153
+ );
1154
+ if (success) {
1155
+ const action = existingMetadata ? "Updated" : "Created";
1156
+ this.logger.info(`${action} metadata for queue: ${queueName}`);
1157
+ return true;
1158
+ }
1159
+ this.logger.error(`Failed to store metadata for queue: ${queueName}`);
1160
+ return false;
1161
+ } catch (error) {
1162
+ this.logger.error(`Error storing metadata for queue ${queueName}: ${error}`);
1163
+ return false;
1164
+ }
1165
+ }
1166
+ /**
1167
+ * Get metadata for a queue.
1168
+ *
1169
+ * @param queueName - The name of the queue
1170
+ * @returns The queue metadata or null if not found
1171
+ */
1172
+ async getMetadata(queueName) {
1173
+ if (!this.isManagementPluginEnabled || !this.managementClient) {
1174
+ return null;
1175
+ }
1176
+ try {
1177
+ const paramName = RabbitMQMetadata.getParameterName(queueName);
1178
+ return await this.managementClient.getParameter(
1179
+ paramName
1180
+ );
1181
+ } catch (error) {
1182
+ this.logger.warn(`Failed to get metadata for queue ${queueName}: ${error}`);
1183
+ return null;
1184
+ }
1185
+ }
1186
+ /**
1187
+ * Delete metadata for a queue.
1188
+ *
1189
+ * @param queueName - The name of the queue
1190
+ */
1191
+ async cleanup(queueName) {
1192
+ if (!this.isManagementPluginEnabled || !this.managementClient) {
1193
+ return;
1194
+ }
1195
+ const paramName = RabbitMQMetadata.getParameterName(queueName);
1196
+ await this.managementClient.deleteParameter(paramName);
1197
+ this.logger.info(`Deleted metadata for queue: ${queueName}`);
1198
+ }
1199
+ /**
1200
+ * Check if management plugin is enabled and metadata storage is available.
1201
+ */
1202
+ isEnabled() {
1203
+ return this.isManagementPluginEnabled;
1204
+ }
1205
+ };
1206
+
934
1207
  // src/core/consumer/RunMQConsumerCreator.ts
935
1208
  var RunMQConsumerCreator = class {
936
1209
  constructor(client, logger, managementConfig) {
937
1210
  this.client = client;
938
1211
  this.logger = logger;
939
1212
  this.ttlPolicyManager = new RunMQTTLPolicyManager(logger, managementConfig);
1213
+ this.metadataManager = new RunMQMetadataManager(logger, managementConfig);
940
1214
  }
941
1215
  async createConsumer(consumerConfiguration) {
942
1216
  await this.ttlPolicyManager.initialize();
1217
+ await this.metadataManager.initialize();
943
1218
  await this.assertQueues(consumerConfiguration);
944
1219
  await this.bindQueues(consumerConfiguration);
1220
+ await this.storeMetadata(consumerConfiguration);
945
1221
  for (let i = 0; i < consumerConfiguration.processorConfig.consumersCount; i++) {
946
1222
  await this.runProcessor(consumerConfiguration);
947
1223
  }
948
1224
  }
1225
+ async storeMetadata(consumerConfiguration) {
1226
+ var _a2;
1227
+ const maxRetries = (_a2 = consumerConfiguration.processorConfig.attempts) != null ? _a2 : DEFAULTS.PROCESSING_ATTEMPTS;
1228
+ await this.metadataManager.apply(
1229
+ consumerConfiguration.processorConfig.name,
1230
+ maxRetries
1231
+ );
1232
+ }
949
1233
  async runProcessor(consumerConfiguration) {
950
1234
  const consumerChannel = await this.getProcessorChannel();
951
1235
  const DLQPublisher = new RunMQPublisherCreator(this.logger).createPublisher(Constants.DEAD_LETTER_ROUTER_EXCHANGE_NAME);
@@ -1062,34 +1346,6 @@ var ConsumerConfiguration = class {
1062
1346
  }
1063
1347
  };
1064
1348
 
1065
- // src/core/logging/RunMQConsoleLogger.ts
1066
- var RunMQConsoleLogger = class {
1067
- constructor() {
1068
- this.prefix = "[RunMQ] - ";
1069
- }
1070
- log(message) {
1071
- console.log(this.formatMessage(message));
1072
- }
1073
- error(message, ...optionalParams) {
1074
- console.error(this.formatMessage(message), ...optionalParams);
1075
- }
1076
- warn(message, ...optionalParams) {
1077
- console.warn(this.formatMessage(message), ...optionalParams);
1078
- }
1079
- info(message, ...optionalParams) {
1080
- console.info(this.formatMessage(message), ...optionalParams);
1081
- }
1082
- debug(message, ...optionalParams) {
1083
- console.debug(this.formatMessage(message), ...optionalParams);
1084
- }
1085
- verbose(message, ...optionalParams) {
1086
- console.debug(this.formatMessage(message), ...optionalParams);
1087
- }
1088
- formatMessage(message) {
1089
- return `${this.prefix} ${message}`;
1090
- }
1091
- };
1092
-
1093
1349
  // src/core/message/RabbitMQMessageProperties.ts
1094
1350
  var RabbitMQMessageProperties = class {
1095
1351
  constructor(id, correlationId) {
@@ -1108,7 +1364,8 @@ var RunMQ = class _RunMQ {
1108
1364
  reconnectDelay: (_a2 = config.reconnectDelay) != null ? _a2 : DEFAULTS.RECONNECT_DELAY,
1109
1365
  maxReconnectAttempts: (_b = config.maxReconnectAttempts) != null ? _b : DEFAULTS.MAX_RECONNECT_ATTEMPTS
1110
1366
  });
1111
- this.client = new RabbitMQClientAdapter(this.config);
1367
+ this.client = new RabbitMQClientAdapter(this.config, this.logger);
1368
+ this.consumer = new RunMQConsumerCreator(this.client, this.logger, this.config.management);
1112
1369
  }
1113
1370
  /**
1114
1371
  * Starts the RunMQ instance by establishing a connection to RabbitMQ and initializing necessary components.
@@ -1129,8 +1386,7 @@ var RunMQ = class _RunMQ {
1129
1386
  * @param processor The function that will process the incoming messages
1130
1387
  */
1131
1388
  async process(topic, config, processor) {
1132
- const consumer = new RunMQConsumerCreator(this.client, this.logger, this.config.management);
1133
- await consumer.createConsumer(new ConsumerConfiguration(topic, config, processor));
1389
+ await this.consumer.createConsumer(new ConsumerConfiguration(topic, config, processor));
1134
1390
  }
1135
1391
  /**
1136
1392
  * Publishes a message to the specified topic with an optional correlation ID
@@ -1189,7 +1445,6 @@ var RunMQ = class _RunMQ {
1189
1445
  return;
1190
1446
  } catch (error) {
1191
1447
  this.retryAttempts++;
1192
- console.log(this.logger);
1193
1448
  this.logger.error(`Connection attempt ${this.retryAttempts}/${maxAttempts} failed:`, error);
1194
1449
  if (this.retryAttempts >= maxAttempts) {
1195
1450
  throw new RunMQException(