@frankvdb/node-red-contrib-amqp
Version:
RabbitMQ nodes for node-red
324 lines (323 loc) • 14.4 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const constants_1 = require("../constants");
const types_1 = require("../types");
const Amqp_1 = __importDefault(require("../Amqp"));
const reconnect_backoff_1 = __importDefault(require("../reconnect-backoff"));
module.exports = function (RED) {
const isErrorLike = (value) => typeof value === 'object' && value !== null;
const isInvalidLoginError = (err) => err.code === types_1.ErrorType.InvalidLogin || /ACCESS_REFUSED/i.test(err.message || '');
const toError = (value) => value instanceof Error ? value : new Error(String(value));
function AmqpOut(config) {
let reconnectTimeout;
let reconnect = null;
let reconnectScheduled = false;
let isShuttingDown = false;
let connection = null;
let channel = null;
let onConnClose;
let onConnError;
let onChannelClose;
let onChannelError;
const me = this;
const reconnectBackoff = new reconnect_backoff_1.default();
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
RED.nodes.createNode(this, config);
this.status(constants_1.NODE_STATUS.Disconnected);
const configAmqp = config;
const amqp = new Amqp_1.default(RED, this, configAmqp);
const reconnectOnError = configAmqp.reconnectOnError;
const removeEventListeners = () => {
var _a, _b, _c, _d;
if (typeof onConnClose === 'function') {
(_a = connection === null || connection === void 0 ? void 0 : connection.off) === null || _a === void 0 ? void 0 : _a.call(connection, 'close', onConnClose);
}
if (typeof onConnError === 'function') {
(_b = connection === null || connection === void 0 ? void 0 : connection.off) === null || _b === void 0 ? void 0 : _b.call(connection, 'error', onConnError);
}
if (typeof onChannelClose === 'function') {
(_c = channel === null || channel === void 0 ? void 0 : channel.off) === null || _c === void 0 ? void 0 : _c.call(channel, 'close', onChannelClose);
}
if (typeof onChannelError === 'function') {
(_d = channel === null || channel === void 0 ? void 0 : channel.off) === null || _d === void 0 ? void 0 : _d.call(channel, 'error', onChannelError);
}
};
const setupEventListeners = (nodeIns) => {
onConnClose = async () => {
nodeIns.warn('AMQP connection closed event received');
try {
await reconnect();
}
catch (reconnectError) {
nodeIns.error(`Reconnect failed after connection close: ${reconnectError}`, {
payload: { error: reconnectError, location: types_1.ErrorLocationEnum.ConnectionErrorEvent },
});
}
};
onConnError = async (e) => {
if (reconnectOnError) {
try {
await reconnect();
}
catch (reconnectError) {
nodeIns.error(`Reconnect failed after connection error: ${reconnectError}`, {
payload: { error: reconnectError, location: types_1.ErrorLocationEnum.ConnectionErrorEvent },
});
}
}
nodeIns.error(`Connection error ${e}`, {
payload: { error: e, location: types_1.ErrorLocationEnum.ConnectionErrorEvent },
});
};
onChannelClose = async () => {
nodeIns.warn('AMQP channel closed event received');
try {
await reconnect();
}
catch (reconnectError) {
nodeIns.error(`Reconnect failed after channel close: ${reconnectError}`, {
payload: { error: reconnectError, location: types_1.ErrorLocationEnum.ChannelErrorEvent },
});
}
};
onChannelError = async (e) => {
if (reconnectOnError) {
try {
await reconnect();
}
catch (reconnectError) {
nodeIns.error(`Reconnect failed after channel error: ${reconnectError}`, {
payload: { error: reconnectError, location: types_1.ErrorLocationEnum.ChannelErrorEvent },
});
}
}
nodeIns.error(`Channel error ${e}`, {
payload: { error: e, location: types_1.ErrorLocationEnum.ChannelErrorEvent },
});
};
connection.on('close', onConnClose);
connection.on('error', onConnError);
channel.on('close', onChannelClose);
channel.on('error', onChannelError);
};
const handleError = async (e, nodeIns) => {
const err = isErrorLike(e) ? e : {};
if (isInvalidLoginError(err)) {
nodeIns.status(constants_1.NODE_STATUS.Invalid);
nodeIns.error(`AmqpOut() Could not connect to broker ${e}`, {
payload: { error: e, location: types_1.ErrorLocationEnum.ConnectError },
});
if (reconnectOnError) {
let reconnectFailed = false;
await reconnect().catch(reconnectError => {
reconnectFailed = true;
nodeIns.status(constants_1.NODE_STATUS.Error);
nodeIns.error(`Reconnect failed during initialization: ${reconnectError}`, {
payload: { error: reconnectError, location: types_1.ErrorLocationEnum.ConnectError },
});
});
if (!reconnectFailed) {
nodeIns.status(constants_1.NODE_STATUS.Invalid);
}
}
}
else {
nodeIns.error(`AmqpOut() ${e}`, {
payload: { error: e, location: types_1.ErrorLocationEnum.ConnectError },
});
if (reconnectOnError) {
await reconnect().catch(reconnectError => {
nodeIns.status(constants_1.NODE_STATUS.Error);
nodeIns.error(`Reconnect failed during initialization: ${reconnectError}`, {
payload: { error: reconnectError, location: types_1.ErrorLocationEnum.ConnectError },
});
});
}
else {
nodeIns.status(constants_1.NODE_STATUS.Error);
}
}
};
// handle input event
const inputListener = async (msg, _, done) => {
const { payload, routingKey, vhost, properties: msgProperties } = msg;
const { exchangeRoutingKey, exchangeRoutingKeyType, amqpProperties, } = config;
// message properties override config properties
let properties;
try {
properties = Object.assign(Object.assign({}, JSON.parse(amqpProperties)), msgProperties);
}
catch (e) {
properties = msgProperties;
}
switch (exchangeRoutingKeyType) {
case 'msg':
case 'flow':
case 'global':
try {
amqp.setRoutingKey(RED.util.evaluateNodeProperty(exchangeRoutingKey, exchangeRoutingKeyType, this, msg));
}
catch (err) {
this.error(`Failed to evaluate routing key: ${err}`);
done && done(toError(err));
return;
}
break;
case 'jsonata': {
try {
const expr = RED.util.prepareJSONataExpression(exchangeRoutingKey, this);
const result = await new Promise((resolve, reject) => {
RED.util.evaluateJSONataExpression(expr, msg, (err, value) => {
if (err) {
reject(err);
}
else {
resolve(value);
}
});
});
if (typeof result !== 'string') {
this.warn(`Routing key JSONata expression returned ${typeof result}; coercing to string`);
}
amqp.setRoutingKey(String(result));
}
catch (err) {
this.error(`Failed to evaluate JSONata expression: ${err}`);
done && done(toError(err));
return;
}
break;
}
case 'str':
default:
if (routingKey) {
// if incoming payload contains a routingKey value
// override our string value with it.
// Superfluous (and possibly confusing) at this point
// but keeping it to retain backwards compatibility
amqp.setRoutingKey(routingKey);
}
break;
}
if (vhost) {
try {
clearTimeout(reconnectTimeout);
reconnectScheduled = false;
removeEventListeners();
await amqp.setVhost(vhost);
connection = amqp.getConnection();
channel = amqp.getChannel();
setupEventListeners(me);
amqp.markConnected();
}
catch (e) {
await handleError(e, me);
done && done(toError(e));
return;
}
}
try {
await amqp.publish(payload, properties);
}
catch (e) {
done && done(toError(e));
return;
}
done && done();
};
this.on('input', inputListener);
// When the node is re-deployed
this.on('close', async (removedOrDone, doneMaybe) => {
const removed = typeof removedOrDone === 'boolean' ? removedOrDone : false;
const done = typeof removedOrDone === 'function' ? removedOrDone : doneMaybe;
isShuttingDown = true;
clearTimeout(reconnectTimeout);
removeEventListeners();
let closeError;
try {
await amqp.close();
}
catch (e) {
closeError = e;
}
finally {
if (removed) {
amqp.removeBrokerNodeState();
}
}
if (closeError) {
done && done(toError(closeError));
return;
}
done && done();
});
async function initializeNode(nodeIns) {
reconnect = async () => {
if (isShuttingDown || reconnectScheduled) {
if (isShuttingDown) {
nodeIns.log('Reconnect skipped: node is shutting down');
}
return;
}
reconnectScheduled = true;
clearTimeout(reconnectTimeout);
try {
nodeIns.log('Reconnect requested: closing AMQP resources');
removeEventListeners();
await amqp.close();
if (isShuttingDown) {
reconnectScheduled = false;
nodeIns.log('Reconnect aborted: node started shutting down while closing AMQP resources');
return;
}
channel = null;
connection = null;
const reconnectDelayMs = reconnectBackoff.nextDelayMs();
nodeIns.log(`Reconnect scheduled in ${reconnectDelayMs}ms`);
reconnectTimeout = setTimeout(() => {
reconnectScheduled = false;
if (isShuttingDown) {
nodeIns.log('Reconnect timer fired but node is shutting down');
return;
}
nodeIns.log('Reconnect timer fired: re-initializing AMQP node');
void initializeNode(nodeIns);
}, reconnectDelayMs);
}
catch (error) {
reconnectScheduled = false;
throw error;
}
};
try {
connection = await amqp.connect();
if (connection) {
channel = await amqp.initialize();
if (isShuttingDown) {
await amqp.close().catch(() => undefined);
return;
}
setupEventListeners(nodeIns);
amqp.markConnected();
reconnectBackoff.reset();
}
}
catch (e) {
await amqp.close().catch(() => undefined);
if (isShuttingDown) {
return;
}
await handleError(e, nodeIns);
}
}
// call
initializeNode(this);
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
RED.nodes.registerType(types_1.NodeType.AmqpOut, AmqpOut);
};