UNPKG

@arturwojnar/hermes-postgresql

Version:

Production-Ready TypeScript Outbox Pattern for PostgreSQL

120 lines 3.67 kB
import { Duration } from '@arturwojnar/hermes'; class AsyncOutboxConsumer { _params; _checkInterval; _getSql; _started = false; _isProcessing = false; _intervalId = null; constructor(_params) { this._params = _params; this._checkInterval = _params.checkInterval || Duration.ofSeconds(15); this._getSql = _params.getSql; } async send(message, options) { if (!this._getSql()) { throw new Error('Database connection not established. Call start() first.'); } const sql = options?.tx || this._getSql(); if (Array.isArray(message)) { if ('savepoint' in sql) { for (const m of message) { await this._publishOne(sql, m); } } else { await sql.begin(async (sql) => { for (const m of message) { await this._publishOne(sql, m); } }); } } else { await this._publishOne(sql, message); } } start() { if (this._started) { throw new Error(`AsyncOutboxConsumer is already started`); } this._started = true; this._startPolling(); return (() => Promise.resolve(stop())); } async stop() { if (this._intervalId) { clearInterval(this._intervalId); this._intervalId = null; } this._started = false; } async _publishOne(sql, message) { await sql ` INSERT INTO "asyncOutbox" ( "consumerName", "messageId", "messageType", "data" ) VALUES ( ${this._params.consumerName}, ${message.messageId}, ${message.messageType}, ${this._getSql().json(message.message)} ) `; } _startPolling() { this._intervalId = setInterval(async () => { try { await this._processUndeliveredMessages(); } catch (error) { } }, this._checkInterval.ms); } async _processUndeliveredMessages() { if (this._isProcessing) { return; } this._isProcessing = true; try { const pendingMessages = await this._getSql() ` SELECT * FROM "asyncOutbox" WHERE delivered = false ORDER BY "addedAt" ASC LIMIT 10 `; for (const message of pendingMessages) { try { await this._params.publish({ position: message.position, messageId: message.messageId, messageType: message.messageType, message: message.data, redeliveryCount: message.failsCount || 0, }); await this._getSql() ` UPDATE "asyncOutbox" SET "delivered" = true, "sentAt" = NOW() WHERE "position" = ${message.position} `; } catch (error) { await this._getSql() ` UPDATE "asyncOutbox" SET "failsCount" = COALESCE("failsCount", 0) + 1 WHERE "position" = ${message.position} `; } } } finally { this._isProcessing = false; } } } const createAsyncOutboxConsumer = (params) => new AsyncOutboxConsumer(params); export { AsyncOutboxConsumer, createAsyncOutboxConsumer, }; //# sourceMappingURL=AsyncOutboxConsumer.js.map