UNPKG

@suissa/universal-queues

Version:

Factory universal para mensageria (RabbitMQ, Kafka, SQS) para sistemas distribuídos.

261 lines 11.8 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 __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; 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; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.RabbitMQClient = void 0; // src/rabbitmq.ts const amqp = __importStar(require("amqplib")); const retry_1 = require("./decorators/retry"); const fs_1 = require("fs"); const crypto_1 = require("crypto"); // ---------- utils mínimos ---------- class TimeoutError extends Error { constructor(msg = 'Operation timed out') { super(msg); this.name = 'TimeoutError'; } } function withTimeout(p, ms, label) { return new Promise((resolve, reject) => { const t = setTimeout(() => reject(new TimeoutError(`${label} timeout after ${ms}ms`)), ms); p.then(v => { clearTimeout(t); resolve(v); }, e => { clearTimeout(t); reject(e); }); }); } function enrichHeaders(exchange, routingKey, message, headers = {}) { const eventId = headers['x-event-id'] ?? (0, crypto_1.randomUUID)(); const hash = (0, crypto_1.createHash)('sha256').update(JSON.stringify(message)).digest('hex'); const origin = `${exchange}.${routingKey}`; return { headers: { ...headers, 'x-event-id': eventId, 'x-event-hash': hash, 'x-origin': origin }, eventId, hash, origin }; } const envURIs = process.env.RABBITMQ_URIS?.split(',').map(s => s.trim()).filter(Boolean); const envTimeout = process.env.UNIVERSAL_QUEUES_OPERATION_TIMEOUT_MS ? Number(process.env.UNIVERSAL_QUEUES_OPERATION_TIMEOUT_MS) : undefined; const envConfirm = process.env.RABBITMQ_CONFIRM ? /^(1|true|yes)$/i.test(process.env.RABBITMQ_CONFIRM) : undefined; const envResolveOnFallback = process.env.UNIVERSAL_QUEUES_RESOLVE_ON_FALLBACK ? /^(1|true|yes)$/i.test(process.env.UNIVERSAL_QUEUES_RESOLVE_ON_FALLBACK) : undefined; class RabbitMQClient { connection; channel; prefetch; operationTimeoutMs; uris; confirm; forensicPath; resolveOnFallback; constructor(opts = {}) { this.prefetch = opts.prefetch ?? 0; this.operationTimeoutMs = opts.operationTimeoutMs ?? envTimeout ?? 5000; this.uris = opts.uris ?? envURIs ?? []; this.confirm = opts.confirm ?? envConfirm ?? true; this.forensicPath = opts.forensicPath ?? process.env.UNIVERSAL_QUEUES_FORENSIC_PATH; this.resolveOnFallback = opts.resolveOnFallback ?? envResolveOnFallback ?? false; } // ---------- CONNECT (timeout + failover) ---------- async connect(uri) { const candidates = this.uris.length ? this.uris : [uri]; let lastErr; for (const candidate of candidates) { try { // Alguns setups de TS “deduzem” ChannelModel aqui. Forço a assinatura correta com um cast. const connectFn = amqp.connect; this.connection = await withTimeout(connectFn(candidate), this.operationTimeoutMs, `connect(${candidate})`); // Guard de existência para createConfirmChannel (algumas definições antigas não expõem) const hasConfirm = this.confirm && typeof this.connection.createConfirmChannel === 'function'; if (hasConfirm) { this.channel = await withTimeout(this.connection.createConfirmChannel(), this.operationTimeoutMs, 'createConfirmChannel'); } else { this.channel = await withTimeout(this.connection.createChannel(), this.operationTimeoutMs, 'createChannel'); } if (this.prefetch > 0) { await withTimeout(this.channel.prefetch(this.prefetch), this.operationTimeoutMs, 'prefetch'); } const close = async () => { try { await this.close(); } catch { } process.exit(0); }; process.once('SIGINT', close); process.once('SIGTERM', close); return; // conectado } catch (e) { lastErr = e; } } throw lastErr; } // ---------- PUBLISH (timeout + confirm + ZoombieQ) ---------- async publishEvent(exchange, routingKey, message, headers = {}) { const env = enrichHeaders(exchange, routingKey, message, headers); try { await withTimeout(this.channel.assertExchange(exchange, 'topic', { durable: true }), this.operationTimeoutMs, 'assertExchange'); const ok = this.channel.publish(exchange, routingKey, Buffer.from(JSON.stringify(message)), { headers: env.headers, persistent: true }); const waitForConfirms = this.channel.waitForConfirms; if (typeof waitForConfirms === 'function') { await withTimeout(waitForConfirms.call(this.channel), this.operationTimeoutMs, 'waitForConfirms'); } else if (!ok) { throw new TimeoutError('publish backpressure (no confirm)'); } } catch (error) { await this.runFallback('ZoombieQ', { exchange, routingKey, message, headers: env.headers, error }); if (this.resolveOnFallback) return; // contrato IMessaging: Promise<void> throw error; } } // ---------- SUBSCRIBE ---------- async subscribeToEvent(exchange, queue, routingKey, handler) { await this.channel.assertExchange(exchange, 'topic', { durable: true }); await this.channel.assertQueue(queue, { durable: true, deadLetterExchange: `${exchange}.dlq` }); await this.channel.bindQueue(queue, exchange, routingKey); this.channel.consume(queue, (msg) => { if (!msg) return; try { const content = JSON.parse(msg.content.toString()); handler(content); this.ackMessage(msg); } catch { this.nackMessage(msg); } }); } // ---------- FANOUT ---------- async publishToFanout(exchange, message) { await this.channel.assertExchange(exchange, 'fanout', { durable: true }); this.channel.publish(exchange, '', Buffer.from(JSON.stringify(message)), { persistent: true }); const waitForConfirms = this.channel.waitForConfirms; if (typeof waitForConfirms === 'function') { await withTimeout(waitForConfirms.call(this.channel), this.operationTimeoutMs, 'waitForConfirms(fanout)'); } } async subscribeToFanout(exchange, handler) { await this.channel.assertExchange(exchange, 'fanout', { durable: true }); const q = await this.channel.assertQueue('', { exclusive: true }); await this.channel.bindQueue(q.queue, exchange, ''); this.channel.consume(q.queue, (msg) => { if (!msg) return; handler(JSON.parse(msg.content.toString())); this.ackMessage(msg); }); } // ---------- OUTBOX + DLQ ---------- async publishToOutbox(event) { // TODO: DB transacional + UNIQUE(eventId,hash) p/ dedup console.error('[OUTBOX]', JSON.stringify(event)); } async handleDeadLetter(dlqExchange, dlqQueue, handler) { await this.channel.assertExchange(dlqExchange, 'fanout', { durable: true }); await this.channel.assertQueue(dlqQueue, { durable: true }); await this.channel.bindQueue(dlqQueue, dlqExchange, ''); this.channel.consume(dlqQueue, (msg) => { if (!msg) return; handler(JSON.parse(msg.content.toString())); this.ackMessage(msg); }); } // ---------- ACK/NACK ---------- ackMessage(msg) { this.channel.ack(msg); } nackMessage(msg, requeue = false) { this.channel.nack(msg, false, requeue); } // ---------- CLOSE ---------- async close() { try { if (this.channel) await this.channel.close(); } catch { } try { if (this.connection) await this.connection.close(); } catch { } } // ---------- Fallback genérico (ZoombieQ = DLQ + Outbox + Log) ---------- async runFallback(mode, ctx) { const record = { eventId: ctx.headers['x-event-id'], hash: ctx.headers['x-event-hash'], origin: ctx.headers['x-origin'], reason: ctx.error instanceof Error ? `${ctx.error.name}:${ctx.error.message}` : String(ctx.error), ts: new Date().toISOString(), payload: ctx.message, headers: ctx.headers, }; const toDLQ = async () => { const dlq = `${ctx.exchange}.dlq`; try { await this.publishToFanout(dlq, record); } catch (e) { await this.appendForensic({ ...record, sink: 'DLQ_ERROR', err: String(e) }); } }; const toOutbox = async () => { try { await this.publishToOutbox(record); } catch (e) { await this.appendForensic({ ...record, sink: 'OUTBOX_ERROR', err: String(e) }); } }; const toForensic = async () => { await this.appendForensic({ ...record, sink: 'FORENSIC' }); }; if (mode === 'DLQ') await toDLQ(); else if (mode === 'Outbox') await toOutbox(); else await Promise.allSettled([toDLQ(), toOutbox(), toForensic()]); } async appendForensic(line) { if (!this.forensicPath) return; try { await fs_1.promises.appendFile(this.forensicPath, JSON.stringify(line) + '\n', 'utf8'); } catch { } } } exports.RabbitMQClient = RabbitMQClient; __decorate([ (0, retry_1.Retry)(2, 250) ], RabbitMQClient.prototype, "publishEvent", null); //# sourceMappingURL=rabbitmq.js.map