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