@frankvdb/node-red-contrib-amqp
Version:
RabbitMQ nodes for node-red
264 lines (263 loc) • 12.7 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 AmqpIn(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;
let onConsumerCancelled;
const me = this;
const reconnectBackoff = new reconnect_backoff_1.default();
const nodeEmitter = me;
// 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 inputListener = async (msg, _, done) => {
if (msg.payload && msg.payload.reconnectCall && typeof reconnect === 'function') {
try {
await reconnect();
done && done();
}
catch (e) {
done && done(toError(e));
}
}
else {
done && done();
}
};
// receive input reconnectCall
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();
});
const removeEventListeners = () => {
var _a, _b, _c, _d, _e;
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);
}
if (typeof onConsumerCancelled === 'function') {
(_e = nodeEmitter.off) === null || _e === void 0 ? void 0 : _e.call(nodeEmitter, 'amqp:consumer-cancelled', onConsumerCancelled);
}
};
async function initializeNode(nodeIns) {
var _a;
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();
// istanbul ignore else
if (connection) {
channel = await amqp.initialize();
await amqp.consume();
if (isShuttingDown) {
await amqp.close().catch(() => undefined);
return;
}
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 },
});
};
onConsumerCancelled = async () => {
nodeIns.warn('AMQP consumer cancelled event received');
try {
await reconnect();
}
catch (reconnectError) {
nodeIns.error(`Reconnect failed after consumer cancellation: ${reconnectError}`, {
payload: { error: reconnectError, location: types_1.ErrorLocationEnum.ChannelErrorEvent },
});
}
};
connection.on('close', onConnClose);
connection.on('error', onConnError);
channel.on('close', onChannelClose);
channel.on('error', onChannelError);
(_a = nodeEmitter.on) === null || _a === void 0 ? void 0 : _a.call(nodeEmitter, 'amqp:consumer-cancelled', onConsumerCancelled);
amqp.markConnected();
reconnectBackoff.reset();
}
}
catch (e) {
await amqp.close().catch(() => undefined);
if (isShuttingDown) {
return;
}
const err = isErrorLike(e) ? e : {};
if (isInvalidLoginError(err)) {
nodeIns.status(constants_1.NODE_STATUS.Invalid);
nodeIns.error(`AmqpIn() 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(`AmqpIn() ${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);
}
}
}
}
// call
initializeNode(this);
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
RED.nodes.registerType(types_1.NodeType.AmqpIn, AmqpIn);
};