UNPKG

peanar

Version:

A job queue for Node.js based on RabbitMQ

212 lines (211 loc) 7.61 kB
"use strict"; 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 __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const events_1 = require("events"); const uuid = __importStar(require("uuid")); const debug_1 = __importDefault(require("debug")); const debug = (0, debug_1.default)('peanar:transact'); const app_1 = require("./app"); const exceptions_1 = require("./exceptions"); function timeout(ms) { return new Promise((res, rej) => { setTimeout(rej, ms); }); } class PeanarTransactor extends events_1.EventEmitter { def; app; queue_name; tid; _counter = 0; _acquireChannelPromise; committed = false; rolledback = false; began = false; constructor(def, app) { super(); this.def = def; this.app = app; this.queue_name = `${this.def.queue}:${this.def.name}:${uuid.v4()}`; this.tid = this.queue_name; } get queueName() { return this.queue_name; } acquireChannel() { if (this.app.state !== app_1.EAppState.RUNNING) { throw new exceptions_1.PeanarInternalError('Peanar is not running!'); } const _doAcquire = async () => { const conn = await this.app.broker.connect(); const ch = await conn.createChannel(); await ch.prefetch(0, false); ch.once('close', () => { this._acquireChannelPromise = undefined; if (this.app.state === app_1.EAppState.RUNNING) { this._acquireChannelPromise = _doAcquire(); } }); return ch; }; if (this._acquireChannelPromise) return this._acquireChannelPromise; return (this._acquireChannelPromise = _doAcquire()); } async begin(tid) { if (tid) { this.tid = tid; } await this.app.broker.queues([{ name: this.queue_name, durable: true, exclusive: false, auto_delete: false, arguments: { deadLetterExchange: this.def.exchange, deadLetterRoutingKey: this.def.routingKey, } }]); } async call(...args) { debug(`PeanarTransactor: call(${this.def.queue}:${this.def.name}})`); const req = { id: uuid.v4(), name: this.def.name, args, attempt: 1 }; if (this.app.state !== app_1.EAppState.RUNNING) { throw new exceptions_1.PeanarInternalError('PeanarTransactor::call() called while app is not in running state.'); } const properties = {}; if (typeof this.def.expires === 'number') { properties.expiration = this.def.expires.toString(); } await this.app.broker.publish({ routing_key: this.queue_name, exchange: '', properties, body: { id: req.id, name: req.name, args: req.args } }); this._counter += 1; return req.id; } async waitUntil(ms = 0) { if (this.committed || this.rolledback) return; const arr = [ (0, events_1.once)(this, 'conclude'), ]; if (ms > 0) { arr.push(timeout(ms)); } await Promise.race(arr); } async commit() { if (this.committed) { throw new exceptions_1.PeanarInternalError('Transaction already commited.'); } if (this.rolledback) { throw new exceptions_1.PeanarInternalError('Transaction has been rolled back.'); } let resolveFn; let consumeCounter = 0; const consumerPromise = new Promise(res => { resolveFn = () => res(undefined); }); const consumerCallback = () => { consumeCounter += 1; if (consumeCounter === this._counter) { if (typeof resolveFn === 'function') { resolveFn(); } else { setImmediate(resolveFn); } } }; const ch = await this.acquireChannel(); try { const consumer = await ch.consume(this.queue_name, consumerCallback); await consumerPromise; await ch.cancel(consumer.consumerTag); ch.nackAll(false); await ch.deleteQueue(this.queue_name, { ifEmpty: true }); debug(`${this.queue_name}: committed`); this.committed = true; this.emit('commit'); this.emit('conclude', 'commit'); } catch (ex) { if (ex.message === 'Channel closed' || ex.name === 'IllegalOperationError') { this.app.log(`Channel closed while committing the transaction ${this.queue_name}. Retrying...`); return this.commit(); } throw ex; } } async rollback() { if (this.committed) { throw new exceptions_1.PeanarInternalError('Transaction already commited.'); } if (this.rolledback) { throw new exceptions_1.PeanarInternalError('Transaction has been rolled back.'); } if (this.app.state !== app_1.EAppState.RUNNING || !this.app.broker.pool) { throw new exceptions_1.PeanarInternalError('Peanar is not running!'); } try { await this.app.broker.pool.acquireAndRun(ch => ch.deleteQueue(this.queue_name, { ifEmpty: true })); debug(`${this.queue_name}: rolled back`); this.rolledback = true; this.emit('rollback'); this.emit('conclude', 'rollback'); } catch (ex) { if (ex.message === 'Channel closed' || ex.name === 'IllegalOperationError') { this.app.log(`Channel closed while rolling back the transaction ${this.queue_name}. Retrying...`); return this.rollback(); } throw ex; } } } exports.default = PeanarTransactor;