@arturwojnar/hermes-postgresql
Version:
Production-Ready TypeScript Outbox Pattern for PostgreSQL
120 lines • 3.67 kB
JavaScript
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