UNPKG

@frankvdb/node-red-contrib-amqp

Version:
324 lines (323 loc) 14.4 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 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); };