UNPKG

@frankvdb/node-red-contrib-amqp

Version:
264 lines (263 loc) 12.7 kB
"use strict"; 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); };