UNPKG

@kitmi/data

Version:

Jacaranda Framework Data Access Model

280 lines (279 loc) 9.72 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "default", { enumerable: true, get: function() { return _default; } }); const _utils = require("@kitmi/utils"); const _jacaranda = require("@kitmi/jacaranda"); const _sys = require("@kitmi/sys"); const _types = require("@kitmi/types"); const _Connector = /*#__PURE__*/ _interop_require_default(require("../../Connector")); function _interop_require_default(obj) { return obj && obj.__esModule ? obj : { default: obj }; } const rabbitmq = _jacaranda.runtime.get(_jacaranda.NS_MODULE, 'rabbitmq-client') ?? (0, _sys.tryRequire)('rabbitmq-client'); const { Connection } = rabbitmq; /** * A callback function to be called to handle a dequeued message. * @callback workerFunction * @param {Message} msg - Message object */ /** * Rabbitmq data storage connector. * @class * @extends Connector */ class RabbitmqConnector extends _Connector.default { getChannelName(opts) { return opts.exchange ? opts.queue ? `[X]${opts.exchange}|${opts.queue}` : `[X]${opts.exchange}` : `[Q]${opts.queue}`; } async logEnqueue(queue, obj) { if (this.options.logStatement) { const logMsg = `${this.driver}: new message enqueued to [${queue}].`; this.app.log('info', logMsg, { msg: obj }); } } async logDequeue(queue, obj) { if (this.options.logStatement) { const logMsg = `${this.driver}: new message dequeued from [${queue}].`; this.app.log('info', logMsg, { msg: obj }); } } /** * Close all connection initiated by this connector. */ async end_() { if (this.channels) { await (0, _utils.batchAsync_)(this.channels, async (ch)=>{ try { await ch.close(); } catch (err) {} }); delete this.channels; } await this.events.emit_('closing'); try { this.conn?.close(); } catch (err) {} this.events.allOff(); } /** * Create a database connection based on the default connection string of the connector and given options. */ connect() { if (!this.conn) { this.conn = new Connection(this.connectionString); this.conn.on('error', (err)=>{ this.app.log('error', `${this.driver}: connection error: ${err.message}}`, _utils._.omit(err, [ 'message', 'stack' ])); }); this.conn.on('connection.blocked', (reason)=>{ this.app.log('warn', `${this.driver}: connection blocked: ${reason}}`); }); this.conn.on('connection.unblocked', ()=>{ this.app.log('info', `${this.driver}: connection unblocked.`); }); if (this.options.logConnection) { this.conn.on('connection', ()=>{ this.app.log('info', `${this.driver}: connection successfully (re)established.`); }); } const { publishers, connectorName } = this.options; _utils._.each(publishers, ({ confirm = true, maxAttempts = 2, routingKey = '', durable = true, ...opts }, index)=>{ if (!opts.exchange && !opts.queue) { throw new _types.InvalidArgument(`${this.driver}[${connectorName}]: missing exchange or queue for publisher "#${index}".`); } opts.durable = durable; const chKey = this.getChannelName(opts); const chOpts = { confirm, maxAttempts }; const arrayOpts = [ opts ]; if (opts.exchange) { if (!opts.type) { opts.type = 'fanout'; } chOpts.exchanges = arrayOpts; } else { chOpts.queues = arrayOpts; } this.channels[chKey] = this.conn.createPublisher(chOpts); }); } return this.conn; } ensurePubChannel(opts) { const chKey = this.getChannelName(opts); const ch = this.channels[chKey]; if (ch == null) { throw new _types.InvalidArgument(`${this.driver}: missing pre-configured channel for publisher "${chKey}".`); } return [ chKey, ch ]; } async createConsumer_(opts, handler) { return new Promise((resolve, reject)=>{ const chKey = this.getChannelName(opts); let _deleteOnStop = false; let _timeout = 5000; if (opts.exchange) { let { exchange, type = 'fanout', durable = true, deleteOnStop, timeout, ..._opts } = opts; if (deleteOnStop) { _deleteOnStop = deleteOnStop; } if (timeout) { _timeout = timeout; } opts = { ..._opts, exchanges: [ { exchange, type, durable } ] }; } const timerHandle = setTimeout(()=>{ reject(new Error(`${this.driver}: consumer [${chKey}] timeout.`)); }, _timeout); const sub = this.conn.createConsumer(opts, (msg)=>{ this.logDequeue(chKey, msg); return handler(msg); }); sub.on('error', (err)=>{ this.app.log('error', `${this.driver}: consumer [${chKey}] error: ${err.message}}`, _utils._.omit(err, [ 'message', 'stack' ])); }); sub.on('ready', ()=>{ clearTimeout(timerHandle); if (this.options.logConnection) { this.app.log('info', `${this.driver}: consumer [${chKey}] ready.`); } resolve(chKey); }); if (_deleteOnStop) { this.events.on('closing', async ()=>{ await this.conn.queueDelete({ queue: sub.queue }); }); } this.app.on('stopping', async ()=>{ await sub.close(); }); }); } async ping_() { return true; } /** * Send a job to worker queue. * @see https://github.com/cody-greene/node-rabbitmq-client * @param {*} queue * @param {*} obj */ async postJob_(queue, obj) { if (!queue) { throw new _types.InvalidArgument('Missing queue name.'); } const [key, pub] = this.ensurePubChannel({ queue }); await pub.send(queue, obj); this.logEnqueue(key, obj); } /** * Publish a message to an exchange. * @param {String} exchange * @param {Object} obj * @param {Object} [opts] */ async publish_(exchange, obj, opts) { if (!exchange) { throw new _types.InvalidArgument('Missing exchange name.'); } const [key, pub] = this.ensurePubChannel({ exchange }); await pub.send({ exchange, routingKey: '', ...opts }, obj); this.logEnqueue(key, obj); } /** * Waiting for message from a queue by a worker. * @see https://github.com/cody-greene/node-rabbitmq-client * @param {*} queue * @param {workerFunction} consumerMethod * @param {boolean} [nonCritical=false] - Flag to indicate if the message is non-critical */ async waitForJob_(queue, consumerMethod, nonCritical) { if (!queue) { throw new _types.InvalidArgument('Missing queue name.'); } return this.createConsumer_({ queue, queueOptions: { durable: true }, noAck: nonCritical, concurrency: 1, qos: { prefetchCount: 2 } }, consumerMethod); } /** * Subscribe to a message from an exchange. * @param {String} exchange * @param {workerFunction} consumerMethod */ async subscribe_(exchange, consumerMethod, { routingKey = '', ...options } = {}) { if (!exchange) { throw new _types.InvalidArgument('Missing exchange name.'); } return this.createConsumer_({ exchange, queueBindings: [ { exchange, routingKey } ], exclusive: true, noAck: true, ...options }, consumerMethod); } /** * @param {App} app * @param {string} connectionString * @param {object} options * @property {boolean} [options.logStatement] - Flag to log queued message * @property {Array} [options.exchanges] - List of exchanges to be created */ constructor(app, connectionString, options){ super(app, 'rabbitmq', connectionString, options); this.channels = {}; this.events = new _jacaranda.AsyncEmitter(); this.connect(); } } RabbitmqConnector.driverLib = rabbitmq; const _default = RabbitmqConnector; //# sourceMappingURL=Connector.js.map