UNPKG

@arturwojnar/hermes-postgresql

Version:

Production-Ready TypeScript Outbox Pattern for PostgreSQL

151 lines 6.28 kB
import { Duration } from '@arturwojnar/hermes'; import { describe, expect, it, jest } from '@jest/globals'; import { setTimeout as setTimeoutCallback } from 'timers'; import { setTimeout } from 'timers/promises'; import { createPublishingQueue } from './publishingQueue.js'; describe('publishingQueue', () => { const createMockTransaction = (lsn) => ({ transactionId: 1, lsn, timestamp: new Date(), results: [], }); const createMockMessage = (lsn) => ({ transaction: createMockTransaction(lsn), acknowledge: jest.fn(), }); const createMockPublish = () => jest.fn().mockImplementation(async () => { }); it('should queue unique messages', () => { const mockPublish = createMockPublish(); const queue = createPublishingQueue(mockPublish); const message1 = createMockMessage('0/1'); const message2 = createMockMessage('0/2'); queue.queue(message1); queue.queue(message2); expect(queue.size()).toBe(2); expect(mockPublish).not.toHaveBeenCalled(); }); it('should ignore duplicate messages based on LSN', () => { const mockPublish = createMockPublish(); const queue = createPublishingQueue(mockPublish); const message1 = createMockMessage('0/1'); const message2 = createMockMessage('0/1'); queue.queue(message1); queue.queue(message2); expect(queue.size()).toBe(1); expect(mockPublish).not.toHaveBeenCalled(); }); it('should publish messages in FIFO order', async () => { const publishedMessages = []; const mockPublish = jest .fn() .mockImplementation(async (message) => { publishedMessages.push(message.transaction.lsn); }); const { queue, publishMessages, size } = createPublishingQueue(mockPublish); const message1 = createMockMessage('0/1'); const message2 = createMockMessage('0/2'); const message3 = createMockMessage('0/3'); queue(message1); queue(message2); queue(message3); await publishMessages(); expect(publishedMessages).toEqual(['0/1', '0/2', '0/3']); expect(size()).toBe(0); }); it('should continue publishing after failed messages', async () => { const mockError = new Error('Publish failed'); const publishedMessages = []; const mockPublish = jest .fn() .mockImplementationOnce(async () => { throw mockError; }) .mockImplementation(async (message) => { publishedMessages.push(message.transaction.lsn); }); const mockFailedPublishCallback = jest .fn(async (tx) => { }) .mockImplementation(async (tx) => { return Promise.resolve(); }); const { queue, publishMessages } = createPublishingQueue(mockPublish, { waitAfterFailedPublish: Duration.ofMiliseconds(1), onFailedPublish: mockFailedPublishCallback, }); const message1 = createMockMessage('0/1'); const message2 = createMockMessage('0/2'); const message3 = createMockMessage('0/3'); queue(message1); queue(message2); queue(message3); await publishMessages(); expect(mockFailedPublishCallback).toHaveBeenCalledWith(message1.transaction); expect(mockFailedPublishCallback).toHaveBeenCalledTimes(1); expect(publishedMessages).toEqual(['0/1', '0/2', '0/3']); }); it('should keep retrying failed message until success', async () => { const mockError = new Error('Publish failed'); const publishedMessages = []; const mockPublish = jest .fn() .mockRejectedValueOnce(mockError) .mockRejectedValueOnce(mockError) .mockImplementation(async (message) => { publishedMessages.push(message.transaction.lsn); }); const mockFailedPublishCallback = jest .fn(async (tx) => { }) .mockImplementation(async (tx) => { return Promise.resolve(); }); const { queue, publishMessages } = createPublishingQueue(mockPublish, { waitAfterFailedPublish: Duration.ofMiliseconds(10), onFailedPublish: mockFailedPublishCallback, }); const message = createMockMessage('0/1'); queue(message); await publishMessages(); expect(mockPublish).toHaveBeenCalledTimes(3); expect(mockFailedPublishCallback).toHaveBeenCalledTimes(2); expect(publishedMessages).toEqual(['0/1']); }); it('should publish all queued messages until queue is empty', async () => { const publishedMessages = []; const mockPublish = jest .fn() .mockImplementation(async (message) => { await setTimeout(100); publishedMessages.push(message.transaction.lsn); }); const { queue, publishMessages } = createPublishingQueue(mockPublish); const messages = [ createMockMessage('0/1'), createMockMessage('0/2'), createMockMessage('0/3'), createMockMessage('0/4'), createMockMessage('0/5'), ]; const laterMessage = createMockMessage('0/100'); messages.forEach((msg) => queue(msg)); setTimeoutCallback(() => { queue(laterMessage); }, 330); await publishMessages(); expect(mockPublish).toHaveBeenCalledTimes(6); expect(publishedMessages).toEqual(['0/1', '0/2', '0/3', '0/4', '0/5', '0/100']); }); it('should prevent concurrent publishing', async () => { const mockPublish = jest .fn() .mockImplementation(async () => await setTimeout(100)); const { queue, publishMessages } = createPublishingQueue(mockPublish); queue(createMockMessage('0/1')); queue(createMockMessage('0/2')); const publish1 = publishMessages(); const publish2 = publishMessages(); await Promise.all([publish1, publish2]); expect(mockPublish).toHaveBeenCalledTimes(2); }); }); //# sourceMappingURL=publishingQueue.test.js.map