@suissa/universal-queues
Version:
Factory universal para mensageria (RabbitMQ, Kafka, SQS) para sistemas distribuídos.
261 lines • 11.8 kB
JavaScript
;
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