UNPKG

@arturwojnar/hermes-postgresql

Version:

Production-Ready TypeScript Outbox Pattern for PostgreSQL

99 lines 4.2 kB
import { CancellationPromise, Duration } from '@arturwojnar/hermes'; import { createAsyncOpsQueue } from '../../common/createAsyncOpsQueue.js'; import { createIntervalResendingStrategy } from './intervalResendingStrategy.js'; const createNonBlockingPublishingQueue = (publish, options) => { const onFailedPublish = options?.onFailedPublish || (() => Promise.resolve()); const ids = new Set(); const acknowledgmentQueue = createAsyncOpsQueue(); const messages = new Array(); let publishingPromise = CancellationPromise.resolved(); const getNextMessageThatShouldBeDelivered = () => { return messages.find((message) => !message.delivered); }; const getNextTransactionLsnThatShouldBeDelivered = () => { return getNextMessageThatShouldBeDelivered()?.transaction?.lsn; }; const getState = (lsn) => { return messages.find(({ transaction }) => transaction.lsn === lsn); }; const queue = (messageToPublish) => { if (ids.has(messageToPublish.transaction.lsn)) { return messageToPublish; } ids.add(messageToPublish.transaction.lsn); messages.push({ ...messageToPublish, delivered: false, failed: true }); return messageToPublish; }; const run = async (messageToPublish) => { if (messages.length > 0 && !publishingPromise.isPending) { publishingPromise = new CancellationPromise(); } messageToPublish = messageToPublish || messages[0]; await _publishMessage(messageToPublish); }; const _removeMessage = (message) => { if (messages.length === 0) { return; } const index = messages.findIndex(({ transaction }) => transaction.lsn === message.transaction.lsn); if (index !== -1) { messages.splice(index, 1); ids.delete(message.transaction.lsn); } }; const _getFirstMessagedThatIsDeliveredButNotAcked = () => { return messages.find((message) => message.delivered); }; const _publishMessage = async (message) => { try { await publish(message); if (message.transaction.lsn === getNextTransactionLsnThatShouldBeDelivered()) { await acknowledgmentQueue.waitFor(acknowledgmentQueue.queue(() => message.acknowledge())); _removeMessage(message); let messageToAck; while ((messageToAck = _getFirstMessagedThatIsDeliveredButNotAcked())) { const message = messageToAck; await acknowledgmentQueue.waitFor(acknowledgmentQueue.queue(() => message.acknowledge())); _removeMessage(message); } if (messages.length === 0) { publishingPromise.resolve(); } } else { const index = messages.findIndex(({ transaction }) => transaction.lsn === message.transaction.lsn); if (index >= 0 && !messages[index].delivered) { console.log(`Processed ${messages[index].transaction.lsn}`); messages[index].delivered = true; } } return 'published'; } catch (error) { const state = getState(message.transaction.lsn); if (state) { state.failed = true; } await onFailedPublish(message.transaction); } }; let stopResending; if (options?.waitAfterFailedPublish?.ms !== 0) { stopResending = createIntervalResendingStrategy()({ getMessages: () => messages, publishMessage: _publishMessage, isPublishing: () => publishingPromise.isPending, interval: options?.waitAfterFailedPublish || Duration.ofSeconds(30), }); } return { name: () => 'NonBlockingPublishingQueue', queue, run, size: () => messages.length, waitUntilIsEmpty: () => publishingPromise, dispose: () => stopResending?.(), }; }; export { createNonBlockingPublishingQueue }; //# sourceMappingURL=createNonBlockingPublishingQueue.js.map