pg-transactional-outbox
Version:
A PostgreSQL based transactional outbox and inbox pattern implementation to support exactly once message processing (with at least once message delivery).
75 lines (70 loc) • 2.7 kB
text/typescript
import { DatabaseClient } from '../common/database';
import { OutboxOrInbox } from '../common/listener-config';
import { TransactionalLogger } from '../common/logger';
import { IsolationLevel, executeTransaction } from '../common/utils';
import { StoredTransactionalMessage } from '../message/transactional-message';
import { FullPollingListenerSettings } from './config';
const lastLogTime = {
inbox: 0,
outbox: 0,
};
/**
* Gets the next inbox messages from the database and sets the locked_until
* @param maxMessages The maximum number of messages to fetch.
* @param client The database client to use for the query.
* @param settings The settings object for the inbox table and function name.
* @param logger The logger to use for logging.
* @param outboxOrInbox The outbox or inbox name
* @returns A promise that resolves to the query result object.
*/
export const getNextMessagesBatch = async (
maxMessages: number,
client: DatabaseClient,
settings: FullPollingListenerSettings,
logger: TransactionalLogger,
outboxOrInbox: OutboxOrInbox,
): Promise<StoredTransactionalMessage[]> => {
const schema = settings.nextMessagesFunctionSchema;
const func = settings.nextMessagesFunctionName;
const lock = settings.nextMessagesLockInMs;
const messagesResult = await executeTransaction(
client,
async (client) =>
await client.query(
/* sql */ `SELECT * FROM ${schema}.${func}(${maxMessages}, ${lock});`,
),
IsolationLevel.RepeatableRead,
);
if (messagesResult.rowCount ?? 0 > 0) {
logger.debug(
{ messageIds: messagesResult.rows.map((m) => m.id) },
`Found ${messagesResult.rowCount} ${outboxOrInbox} message(s) to process.`,
);
lastLogTime[outboxOrInbox] = Date.now();
} else {
if (lastLogTime[outboxOrInbox] <= Date.now() - 60_000) {
logger.trace(
`Found no unprocessed ${outboxOrInbox} messages in the last minute.`,
);
lastLogTime[outboxOrInbox] = Date.now();
}
}
return messagesResult.rows.map(mapInbox);
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const mapInbox = (i: any): StoredTransactionalMessage => ({
id: i.id,
aggregateType: i.aggregate_type,
aggregateId: i.aggregate_id,
messageType: i.message_type,
payload: i.payload,
metadata: i.metadata as Record<string, unknown> | undefined,
createdAt: i.created_at.toISOString(),
concurrency: i.concurrency,
finishedAttempts: i.finished_attempts,
lockedUntil: i.locked_until?.toISOString() ?? null,
startedAttempts: i.started_attempts,
processedAt: i.processed_at?.toISOString() ?? null,
abandonedAt: i.abandoned_at?.toISOString() ?? null,
segment: i.segment,
});