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