@frankvdb/node-red-contrib-amqp
Version:
RabbitMQ nodes for node-red
393 lines (392 loc) • 15.6 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const uuid_1 = require("uuid");
const cloneDeep = require("lodash.clonedeep");
const amqplib_1 = require("amqplib");
const types_1 = require("./types");
const constants_1 = require("./constants");
class Amqp {
constructor(RED, node, config) {
this.RED = RED;
this.node = node;
this.config = {
name: config.name,
broker: config.broker,
prefetch: config.prefetch,
reconnectOnError: config.reconnectOnError,
noAck: config.noAck,
exchange: {
name: config.exchangeName,
type: config.exchangeType,
routingKey: config.exchangeRoutingKey,
durable: config.exchangeDurable,
},
queue: {
name: config.queueName,
exclusive: config.queueExclusive,
durable: config.queueDurable,
autoDelete: config.queueAutoDelete,
queueType: config.queueType,
queueArguments: this.parseJson(config.queueArguments)
},
amqpProperties: this.parseJson(config.amqpProperties),
headers: this.parseJson(config.headers),
outputs: config.outputs,
rpcTimeout: config.rpcTimeoutMilliseconds,
};
}
async connect() {
const { broker } = this.config;
// wtf happened to the types?
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
this.broker = this.RED.nodes.getNode(broker);
if (!this.broker) {
const err = new Error(`AMQP broker node not found: ${broker}`);
this.node.error(err.message);
throw err;
}
const brokerUrl = this.getBrokerUrl(this.broker);
const { host, port, vhost } = this.broker;
const brokerInfo = `${host}:${port}${vhost ? `/${vhost}` : ''}`;
this.node.log(`Connecting to AMQP broker ${brokerInfo}`);
try {
this.connection = await (0, amqplib_1.connect)(brokerUrl, { heartbeat: 2 });
this.node.log(`Connected to AMQP broker ${brokerInfo}`);
}
catch (err) {
this.node.warn(`Failed to connect to AMQP broker ${brokerInfo}: ${err}`);
throw err;
}
/* istanbul ignore next */
this.connection.on('error', (e) => {
// Set node to disconnected status
this.node.status(constants_1.NODE_STATUS.Disconnected);
this.node.warn(`AMQP connection error ${e}`);
});
/* istanbul ignore next */
this.connection.on('close', () => {
this.node.status(constants_1.NODE_STATUS.Disconnected);
this.node.log(`AMQP Connection closed`);
});
return this.connection;
}
async initialize() {
await this.createChannel();
await this.assertExchange();
return this.channel;
}
async consume() {
try {
const { noAck } = this.config;
await this.assertQueue();
await this.bindQueue();
await this.channel.consume(this.q.queue, amqpMessage => {
const msg = this.assembleMessage(amqpMessage);
this.node.send(msg);
/* istanbul ignore else */
if (!noAck && !this.isManualAck()) {
this.ack(msg);
}
}, { noAck });
}
catch (e) {
this.node.error(`Could not consume message: ${e}`);
}
}
setRoutingKey(newRoutingKey) {
this.config.exchange.routingKey = newRoutingKey;
}
ack(msg) {
var _a;
const allUpTo = !!((_a = msg.manualAck) === null || _a === void 0 ? void 0 : _a.allUpTo);
try {
this.channel.ack(msg, allUpTo);
}
catch (e) {
this.node.error(`Could not ack message: ${e}`);
}
}
ackAll() {
try {
this.channel.ackAll();
}
catch (e) {
this.node.error(`Could not ackAll messages: ${e}`);
}
}
nack(msg) {
var _a, _b, _c;
const allUpTo = !!((_a = msg.manualAck) === null || _a === void 0 ? void 0 : _a.allUpTo);
const requeue = (_c = (_b = msg.manualAck) === null || _b === void 0 ? void 0 : _b.requeue) !== null && _c !== void 0 ? _c : true;
try {
this.channel.nack(msg, allUpTo, requeue);
}
catch (e) {
this.node.error(`Could not nack message: ${e}`);
}
}
nackAll(msg) {
var _a, _b;
const requeue = (_b = (_a = msg.manualAck) === null || _a === void 0 ? void 0 : _a.requeue) !== null && _b !== void 0 ? _b : true;
try {
this.channel.nackAll(requeue);
}
catch (e) {
this.node.error(`Could not nackAll messages: ${e}`);
}
}
reject(msg) {
var _a, _b;
const requeue = (_b = (_a = msg.manualAck) === null || _a === void 0 ? void 0 : _a.requeue) !== null && _b !== void 0 ? _b : true;
try {
this.channel.reject(msg, requeue);
}
catch (e) {
this.node.error(`Could not reject message: ${e}`);
}
}
async publish(msg, properties) {
this.parseRoutingKeys().forEach(async (routingKey) => {
this.handlePublish(this.config, msg, properties, routingKey);
});
}
async handlePublish(config, msg, properties, routingKey) {
var _a, _b;
const { exchange: { name }, outputs: rpcRequested, } = config;
try {
let correlationId = '';
let replyTo = '';
if (rpcRequested) {
// Send request for remote procedure call
correlationId =
(properties === null || properties === void 0 ? void 0 : properties.correlationId) ||
((_a = this.config.amqpProperties) === null || _a === void 0 ? void 0 : _a.correlationId) ||
(0, uuid_1.v4)();
replyTo =
(properties === null || properties === void 0 ? void 0 : properties.replyTo) || ((_b = this.config.amqpProperties) === null || _b === void 0 ? void 0 : _b.replyTo) || (0, uuid_1.v4)();
await this.handleRemoteProcedureCall(correlationId, replyTo);
}
const options = Object.assign(Object.assign({ correlationId,
replyTo }, this.config.amqpProperties), properties);
// when the name field is empty, publish just like the sendToQueue method;
// see https://amqp-node.github.io/amqplib/channel_api.html#channel_publish
this.channel.publish(name, routingKey, Buffer.from(msg), options);
}
catch (e) {
this.node.error(`Could not publish message: ${e}`);
}
}
getRpcConfig(replyTo) {
const rpcConfig = cloneDeep(this.config);
rpcConfig.exchange.name = '';
rpcConfig.queue.name = replyTo;
rpcConfig.queue.autoDelete = true;
rpcConfig.queue.exclusive = true;
rpcConfig.queue.durable = false;
rpcConfig.noAck = true;
return rpcConfig;
}
async handleRemoteProcedureCall(correlationId, replyTo) {
const rpcConfig = this.getRpcConfig(replyTo);
try {
// If we try to delete a queue that's already deleted
// bad things will happen.
let rpcQueueHasBeenDeleted = false;
let additionalErrorMessaging = '';
/************************************
* assert queue and set up consumer
************************************/
const queueName = await this.assertQueue(rpcConfig);
await this.channel.consume(queueName, async (amqpMessage) => {
if (amqpMessage) {
const msg = this.assembleMessage(amqpMessage);
if (msg.properties.correlationId === correlationId) {
this.node.send(msg);
/* istanbul ignore else */
if (!rpcQueueHasBeenDeleted) {
await this.channel.deleteQueue(queueName);
rpcQueueHasBeenDeleted = true;
}
}
else {
additionalErrorMessaging += ` Correlation ids do not match. Expecting: ${correlationId}, received: ${msg.properties.correlationId}`;
}
}
}, { noAck: rpcConfig.noAck });
/****************************************
* Check if RPC has timed out and handle
****************************************/
setTimeout(async () => {
try {
if (!rpcQueueHasBeenDeleted) {
this.node.send({
payload: {
message: `Timeout while waiting for RPC response.${additionalErrorMessaging}`,
config: rpcConfig,
},
});
await this.channel.deleteQueue(queueName);
}
}
catch (e) {
// TODO: Keep an eye on this
// This might close the whole channel
this.node.error(`Error trying to cancel RPC consumer: ${e}`);
}
}, rpcConfig.rpcTimeout || 3000);
}
catch (e) {
this.node.error(`Could not consume RPC message: ${e}`);
}
}
async close() {
var _a;
const { name: exchangeName } = this.config.exchange;
const queueName = (_a = this.q) === null || _a === void 0 ? void 0 : _a.queue;
try {
/* istanbul ignore else */
if (exchangeName && queueName) {
const routingKeys = this.parseRoutingKeys();
try {
for (let x = 0; x < routingKeys.length; x++) {
await this.channel.unbindQueue(queueName, exchangeName, routingKeys[x]);
}
}
catch (e) {
/* istanbul ignore next */
console.error('Error unbinding queue: ', e.message);
}
}
}
catch (e) {
/* istanbul ignore next */
this.node.error(`Error unbinding queue: ${e}`);
}
if (this.channel) {
try {
await this.channel.close();
}
catch (e) {
this.node.error(`Error closing AMQP channel: ${e}`);
}
}
if (this.connection) {
try {
await this.connection.close();
}
catch (e) {
this.node.error(`Error closing AMQP connection: ${e}`);
}
}
}
async createChannel() {
const { prefetch } = this.config;
this.channel = await this.connection.createChannel();
this.channel.prefetch(Number(prefetch));
/* istanbul ignore next */
this.channel.on('error', (e) => {
// Set node to disconnected status
this.node.status(constants_1.NODE_STATUS.Disconnected);
this.node.error(`AMQP Connection Error ${e}`, { payload: { error: e, source: 'Amqp' } });
});
/* istanbul ignore next */
this.channel.on('close', () => {
this.node.status(constants_1.NODE_STATUS.Disconnected);
this.node.log('AMQP Channel closed');
});
/* istanbul ignore next */
this.channel.on('return', () => {
this.node.warn('AMQP Message returned');
});
return this.channel;
}
async assertExchange() {
const { name, type, durable } = this.config.exchange;
/* istanbul ignore else */
if (name) {
await this.channel.assertExchange(name, type, {
durable,
});
}
}
async assertQueue(configParams) {
const { queue } = configParams || this.config;
const { name, exclusive, durable, autoDelete, queueType, queueArguments } = queue;
this.q = await this.channel.assertQueue(name, {
exclusive,
durable,
autoDelete,
arguments: Object.assign({ "x-queue-type": queueType }, (queueArguments || {})),
});
return name;
}
async bindQueue(configParams) {
const { name, type, routingKey } = (configParams === null || configParams === void 0 ? void 0 : configParams.exchange) || this.config.exchange;
const { headers } = (configParams === null || configParams === void 0 ? void 0 : configParams.amqpProperties) || this.config;
try {
if (this.canHaveRoutingKey(type) && name) {
const promises = this.parseRoutingKeys(routingKey).map(key => this.channel.bindQueue(this.q.queue, name, key));
await Promise.all(promises);
}
if (type === types_1.ExchangeType.Fanout) {
await this.channel.bindQueue(this.q.queue, name, '');
}
if (type === types_1.ExchangeType.Headers) {
await this.channel.bindQueue(this.q.queue, name, '', headers);
}
}
catch (e) {
this.node.error(`Could not bind queue: ${e}`);
}
}
canHaveRoutingKey(type) {
return type === types_1.ExchangeType.Direct || type === types_1.ExchangeType.Topic;
}
getBrokerUrl(broker) {
let url = '';
if (broker) {
const { host, port, vhost, tls, credsFromSettings, credentials } = broker;
const { username, password } = credsFromSettings
? this.getCredsFromSettings()
: credentials;
const protocol = tls ? /* istanbul ignore next */ 'amqps' : 'amqp';
url = `${protocol}://${encodeURIComponent(username)}:${encodeURIComponent(password)}@${host}:${port}/${vhost}`;
}
return url;
}
getCredsFromSettings() {
return {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
username: this.RED.settings.MW_CONTRIB_AMQP_USERNAME,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
password: this.RED.settings.MW_CONTRIB_AMQP_PASSWORD,
};
}
parseRoutingKeys(routingKeyArg) {
var _a;
const routingKey = routingKeyArg || this.config.exchange.routingKey || ((_a = this.q) === null || _a === void 0 ? void 0 : _a.queue) || '';
const keys = routingKey === null || routingKey === void 0 ? void 0 : routingKey.split(',').map(key => key.trim());
return keys;
}
assembleMessage(amqpMessage) {
const payload = this.parseJson(amqpMessage.content.toString());
return Object.assign(Object.assign({}, amqpMessage), { payload });
}
isManualAck() {
return this.node.type === types_1.NodeType.AmqpInManualAck;
}
parseJson(jsonInput) {
let output;
try {
output = JSON.parse(jsonInput);
}
catch (_a) {
output = jsonInput;
}
return output;
}
}
exports.default = Amqp;