UNPKG

@arturwojnar/hermes-postgresql

Version:

Production-Ready TypeScript Outbox Pattern for PostgreSQL

86 lines 2.98 kB
import { assertNever, CancellationPromise, Duration, noop } from '@arturwojnar/hermes'; import { setTimeout } from 'node:timers/promises'; const createSerializedPublishingQueue = (publish, options) => { const waitAfterFailedPublish = options?.waitAfterFailedPublish || Duration.ofSeconds(1); const onFailedPublish = options?.onFailedPublish || (() => Promise.resolve()); const ids = new Set(); const messages = new Array(); let isPublishing = false; let publishingPromise = CancellationPromise.resolved(); const queue = (messageToPublish) => { if (ids.has(messageToPublish.transaction.lsn)) { return messageToPublish; } ids.add(messageToPublish.transaction.lsn); messages.push(messageToPublish); return messageToPublish; }; const dequeueOldest = () => { if (messages.length === 0) { return; } const oldest = messages.shift(); ids.delete(oldest.transaction.lsn); }; const run = async () => { if (isPublishing) { return; } isPublishing = true; publishingPromise = new CancellationPromise(); try { do { const result = await _publishOldestMessage(); switch (result) { case 'published': continue; case 'failed': await onFailedPublish(messages[0].transaction); if (waitAfterFailedPublish) { await setTimeout(waitAfterFailedPublish.ms); } break; case 'exhausted': isPublishing = false; publishingPromise.resolve(); break; default: assertNever(result); } } while (isPublishing); } finally { isPublishing = false; publishingPromise.resolve(); } }; const _publishOldestMessage = async () => { if (messages.length === 0) { return 'exhausted'; } const oldest = messages[0]; try { await publish(oldest); dequeueOldest(); await oldest.acknowledge(); return 'published'; } catch (error) { if (messages.length && messages[0].transaction.lsn !== oldest.transaction.lsn) { ids.add(oldest.transaction.lsn); messages.unshift(oldest); } return 'failed'; } }; return { name: () => 'SerializedPublishingQueue', queue, run, size: () => messages.length, waitUntilIsEmpty: () => publishingPromise, dispose: noop, }; }; export { createSerializedPublishingQueue }; //# sourceMappingURL=createSerializedPublishingQueue.js.map