@citrineos/util
Version:
The OCPP util module which supplies helpful utilities like cache and queue connectors, etc.
219 lines • 9.92 kB
JavaScript
;
// 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