UNPKG

@citrineos/util

Version:

The OCPP util module which supplies helpful utilities like cache and queue connectors, etc.

219 lines 9.92 kB
"use strict"; // Copyright (c) 2023 S44, LLC // Copyright Contributors to the CitrineOS Project // // SPDX-License-Identifier: Apache 2.0 var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.RabbitMqReceiver = void 0; const amqplib = __importStar(require("amqplib")); const __1 = require("../.."); const base_1 = require("@citrineos/base"); const class_transformer_1 = require("class-transformer"); /** * Implementation of a {@link IMessageHandler} using RabbitMQ as the underlying transport. */ class RabbitMqReceiver extends base_1.AbstractMessageHandler { constructor(config, logger, module, cache) { super(config, logger, module); this._cache = cache || new __1.MemoryCache(); this._connect() .then((channel) => { this._channel = channel; }) .catch((error) => { this._logger.error('Failed to connect to RabbitMQ', error); }); } /** * Methods */ /** * Binds queue to an exchange given identifier and optional actions and filter. * Note: Due to the nature of AMQP 0-9-1 model, if you need to filter for the identifier, you **MUST** provide it in the filter object. * * @param {string} identifier - The identifier of the channel to subscribe to. * @param {CallAction[]} actions - Optional. An array of actions to filter the messages. * @param {{ [k: string]: string; }} filter - Optional. An object representing the filter to apply on the messages. * @return {Promise<boolean>} A promise that resolves to true if the subscription is successful, false otherwise. */ subscribe(identifier, actions, filter) { return __awaiter(this, void 0, void 0, function* () { var _a; // If actions are a defined but empty list, it is likely a module // with no available actions and should not have a queue. // // If actions are undefined, it is likely a charger, // which is "allowed" not to have actions. if (actions && actions.length === 0) { this._logger.debug(`Skipping queue binding for module ${identifier} as there are no available actions.`); return true; } const exchange = (_a = this._config.util.messageBroker.amqp) === null || _a === void 0 ? void 0 : _a.exchange; const queueName = `${RabbitMqReceiver.QUEUE_PREFIX}${identifier}_${Date.now()}`; // Ensure that filter includes the x-match header set to all filter = filter ? Object.assign({ 'x-match': 'all' }, filter) : { 'x-match': 'all' }; const channel = this._channel || (yield this._connect()); this._channel = channel; // Assert exchange and queue yield channel.assertExchange(exchange, 'headers', { durable: false }); yield channel.assertQueue(queueName, { durable: false, autoDelete: true, exclusive: false, }); // Bind queue based on provided actions and filters if (actions && actions.length > 0) { for (const action of actions) { this._logger.debug(`Bind ${queueName} on ${exchange} for ${action} with filter ${JSON.stringify(filter)}.`); yield channel.bindQueue(queueName, exchange, '', Object.assign({ action }, filter)); } } else { this._logger.debug(`Bind ${queueName} on ${exchange} with filter ${JSON.stringify(filter)}.`); yield channel.bindQueue(queueName, exchange, '', filter); } // Start consuming messages yield channel.consume(queueName, (msg) => this._onMessage(msg, channel)); // Define cache key const cacheKey = `${RabbitMqReceiver.CACHE_PREFIX}${identifier}`; // Retrieve cached queue names const cachedQueues = yield this._cache .get(cacheKey, base_1.CacheNamespace.Other, () => Array) .then((value) => { if (value) { value.push(queueName); return value; } return new Array(queueName); }); // Add queue name to cache yield this._cache.set(cacheKey, JSON.stringify(cachedQueues), base_1.CacheNamespace.Other); return true; }); } unsubscribe(identifier) { return this._cache .get(`${RabbitMqReceiver.CACHE_PREFIX}${identifier}`, base_1.CacheNamespace.Other, () => Array) .then((queues) => __awaiter(this, void 0, void 0, function* () { var _a, _b; if (queues) { const channel = this._channel || (yield this._connect()); this._channel = channel; for (const queue of queues) { yield channel.unbindQueue(queue, ((_a = this._config.util.messageBroker.amqp) === null || _a === void 0 ? void 0 : _a.exchange) || '', ''); const messageCount = yield ((_b = this._channel) === null || _b === void 0 ? void 0 : _b.deleteQueue(queue)); this._logger.info(`Queue ${identifier} deleted with ${messageCount === null || messageCount === void 0 ? void 0 : messageCount.messageCount} messages remaining.`); } return true; } else { this._logger.warn(`Failed to delete queue for ${identifier}, queue name not found in cache.`); return false; } })); } shutdown() { return Promise.resolve(); } /** * Protected Methods */ /** * Connect to RabbitMQ */ _connect() { var _a; return amqplib .connect(((_a = this._config.util.messageBroker.amqp) === null || _a === void 0 ? void 0 : _a.url) || '') .then((connection) => { this._connection = connection; return connection.createChannel(); }) .then((channel) => { // Add listener for channel errors channel.on('error', (err) => { this._logger.error('AMQP channel error', err); // TODO: add recovery logic }); return channel; }); } /** * Underlying RabbitMQ message handler. * * @param message The AMQPMessage to process */ _onMessage(message, channel) { return __awaiter(this, void 0, void 0, function* () { if (message) { try { this._logger.debug('_onMessage:Received message:', message.properties, message.content.toString()); const parsed = (0, class_transformer_1.plainToInstance)((base_1.Message), JSON.parse(message.content.toString())); yield this.handle(parsed, message.properties); } catch (error) { if (error instanceof base_1.RetryMessageError) { this._logger.warn('Retrying message: ', error.message); // Retryable error, usually ongoing call with station when trying to send new call channel.nack(message); return; } else { this._logger.error('Error while processing message:', error, message); } } channel.ack(message); } }); } } exports.RabbitMqReceiver = RabbitMqReceiver; /** * Constants */ RabbitMqReceiver.QUEUE_PREFIX = 'rabbit_queue_'; RabbitMqReceiver.CACHE_PREFIX = 'rabbit_subscription_'; //# sourceMappingURL=receiver.js.map