UNPKG

@arturwojnar/hermes-postgresql

Version:

Production-Ready TypeScript Outbox Pattern for PostgreSQL

68 lines 3.31 kB
import { Duration, noop, swallow } from '@arturwojnar/hermes'; import { pipe } from 'fp-ts/lib/function.js'; import { setTimeout } from 'node:timers/promises'; import { convertBigIntToLsn, incrementWAL } from '../common/lsn.js'; import { onData as _onData } from './onData.js'; import { sendStandbyStatusUpdate } from './sendStandbyStatusUpdate.js'; import { addInsert, createTransaction, emptyTransaction } from './transaction/transaction.js'; import { MessageType, TopLevelType, } from './types.js'; const PSQL_ADMIN_SHUTDOWN = '57P01'; const startLogicalReplication = async (params) => { const { state, sql } = params; const onInsert = params.onInsert || noop; const location = typeof state.lastProcessedLsn === 'undefined' ? '0/00000000' : state.lastProcessedLsn; let currentTransaction = emptyTransaction(location); const stream = await sql .unsafe(`START_REPLICATION SLOT ${state.slotName} LOGICAL ${convertBigIntToLsn(incrementWAL(location))} (proto_version '1', publication_names '${params.state.publication}')`) .writable(); const curriedOnData = _onData(params.columnConfig); const onData = (message) => curriedOnData(message); const acknowledgeLastLsn = sendStandbyStatusUpdate(stream, () => state.lastProcessedLsn); const close = async () => { const timeout = Duration.ofSeconds(1).ms; await Promise.all([Promise.race([swallow(() => sql.end({ timeout })), setTimeout(timeout)])]); }; const storeResult = (result) => { if (result.messageType === MessageType.Begin) { currentTransaction = createTransaction(result.transactionId, result.lsn, result.timestamp); } else if (result.messageType === MessageType.Insert) { addInsert(currentTransaction, result.result); } else if (result.messageType === MessageType.Commit) { } return result; }; const handleResult = (result) => { if (result.topLevelType === TopLevelType.PrimaryKeepaliveMessage && result.shouldPong) { acknowledgeLastLsn(); } else if (result.topLevelType === TopLevelType.XLogData && result.messageType === MessageType.Commit) { const acknowledge = () => { sendStandbyStatusUpdate(stream, () => params.state.lastProcessedLsn); state.lastProcessedLsn = currentTransaction.lsn; currentTransaction = emptyTransaction(state.lastProcessedLsn); }; onInsert(currentTransaction, acknowledge); } }; stream.on('data', (message) => { pipe(message, onData, storeResult, handleResult); }); stream.on('error', async (error) => { const pError = error; if ((pError.code === PSQL_ADMIN_SHUTDOWN || pError.code === 'CONNECTION_CLOSED') && pError.query.includes('START_REPLICATION SLOT')) { console.info('Replication connection closed due to database shutdown'); await swallow(() => sql.end({ timeout: Duration.ofSeconds(1).ms })); } else { console.error(error, 'startLogicalReplication'); } }); stream.on('close', () => { close().catch(noop); }); }; export { startLogicalReplication }; //# sourceMappingURL=logicalReplicationStream.js.map