UNPKG

@arturwojnar/hermes-postgresql

Version:

Production-Ready TypeScript Outbox Pattern for PostgreSQL

158 lines 6.75 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 { createSerializedPublishingQueue } from './createSerializedPublishingQueue.js'; describe('publishingQueue', () => { const createMockTransaction = (lsn) => ({ transactionId: 1, lsn, timestamp: new Date(), results: [], }); const createMockMessage = (lsn) => ({ transaction: createMockTransaction(lsn), acknowledge: jest.fn().mockResolvedValue(), }); const createMockPublish = () => jest.fn().mockImplementation(async () => { }); it('should queue unique messages', () => { const mockPublish = createMockPublish(); const queue = createSerializedPublishingQueue(mockPublish); const message1 = createMockMessage('0/1'); const message2 = createMockMessage('0/2'); queue.queue(message1); queue.queue(message2); expect(queue.size()).toBe(2); }); it('should ignore duplicate messages based on LSN', () => { const mockPublish = createMockPublish(); const queue = createSerializedPublishingQueue(mockPublish); const message1 = createMockMessage('0/1'); const message2 = createMockMessage('0/1'); queue.queue(message1); queue.queue(message2); expect(queue.size()).toBe(1); }); it('should publish messages in FIFO order', async () => { const publishedMessages = []; const mockPublish = jest .fn() .mockImplementation(async (message) => { publishedMessages.push(message.transaction.lsn); }); const { queue, size, run } = createSerializedPublishingQueue(mockPublish); const message1 = createMockMessage('0/1'); const message2 = createMockMessage('0/2'); const message3 = createMockMessage('0/3'); const message4 = createMockMessage('0/4'); queue(message1); queue(message2); queue(message3); queue(message4); await run(); expect(publishedMessages).toEqual(['0/1', '0/2', '0/3', '0/4']); 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, run } = createSerializedPublishingQueue(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 run(); expect(mockFailedPublishCallback).toHaveBeenCalledWith(message1.transaction); expect(mockFailedPublishCallback).toHaveBeenCalledTimes(1); expect(publishedMessages).toEqual(['0/1', '0/2', '0/3']); expect(message1.acknowledge).toHaveBeenCalledTimes(1); expect(message2.acknowledge).toHaveBeenCalledTimes(1); expect(message3.acknowledge).toHaveBeenCalledTimes(1); }); 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, run } = createSerializedPublishingQueue(mockPublish, { waitAfterFailedPublish: Duration.ofMiliseconds(10), onFailedPublish: mockFailedPublishCallback, }); const message = createMockMessage('0/1'); queue(message); await run(); expect(mockPublish).toHaveBeenCalledTimes(3); expect(mockFailedPublishCallback).toHaveBeenCalledTimes(2); expect(publishedMessages).toEqual(['0/1']); expect(message.acknowledge).toHaveBeenCalledTimes(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, run } = createSerializedPublishingQueue(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); const anotherMessage = createMockMessage('0/50'); queue(anotherMessage); await run(); expect(mockPublish).toHaveBeenCalledTimes(7); expect(publishedMessages).toEqual(['0/1', '0/2', '0/3', '0/4', '0/5', '0/50', '0/100']); messages.forEach((message) => expect(message.acknowledge).toHaveBeenCalledTimes(1)); expect(anotherMessage.acknowledge).toHaveBeenCalledTimes(1); expect(laterMessage.acknowledge).toHaveBeenCalledTimes(1); }); it('should prevent concurrent publishing', async () => { const mockPublish = jest .fn() .mockImplementation(async () => await setTimeout(100)); const { queue, run } = createSerializedPublishingQueue(mockPublish); queue(createMockMessage('0/1')); queue(createMockMessage('0/2')); await run(); expect(mockPublish).toHaveBeenCalledTimes(2); }); }); //# sourceMappingURL=createSerializedPublishingQueue.unit.test.js.map