UNPKG

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).

196 lines 9.62 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.justDoIt = exports.processPool = exports.getClient = exports.executeTransaction = exports.IsolationLevel = exports.awaitWithTimeout = exports.sleep = void 0; const database_1 = require("./database"); const error_1 = require("./error"); /** * Sleep for a given amount of milliseconds * @param milliseconds The time in milliseconds to sleep * @returns The (void) promise to await */ const sleep = (milliseconds) => __awaiter(void 0, void 0, void 0, function* () { return new Promise((resolve) => setTimeout(resolve, milliseconds)); }); exports.sleep = sleep; /** * Run a promise but make sure to wait only a maximum amount of time for it to finish. * @param promise The promise to execute * @param timeoutInMs The amount of time in milliseconds to wait for the promise to finish * @param failureMessage The message for the error if the timeout was reached * @returns The promise return value or a timeout error is thrown */ const awaitWithTimeout = (promise, timeoutInMs, failureMessage) => { let timeoutHandle; const timeoutPromise = new Promise((_resolve, reject) => { timeoutHandle = setTimeout(() => reject(new error_1.TransactionalOutboxInboxError(failureMessage !== null && failureMessage !== void 0 ? failureMessage : 'Timeout', 'TIMEOUT')), timeoutInMs); }); return Promise.race([promise(), timeoutPromise]) .then((result) => { return result; }) .finally(() => clearTimeout(timeoutHandle)); }; exports.awaitWithTimeout = awaitWithTimeout; /** * PostgreSQL available isolation levels. The isolation level "Read uncommitted" * is the same as "Read committed" in PostgreSQL. And the readonly variants are * not usable as the message must be marked as processed or the * "finished_attempts" counter is updated for the message. */ var IsolationLevel; (function (IsolationLevel) { /** Highest protection - no serialization anomaly */ IsolationLevel["Serializable"] = "SERIALIZABLE"; /** Second highest protection - no non-repeatable read */ IsolationLevel["RepeatableRead"] = "REPEATABLE READ"; /** Lowest protection (same as read uncommitted in PG) - non-repeatable reads possible */ IsolationLevel["ReadCommitted"] = "READ COMMITTED"; })(IsolationLevel || (exports.IsolationLevel = IsolationLevel = {})); /** * Open a transaction and execute the callback as part of the transaction. * @param client The PostgreSQL database client * @param callback The callback to execute DB commands with. * @param isolationLevel The database transaction isolation level. Falls back to the default PostgreSQL transaction level if not provided. * @returns The result of the callback (if any). * @throws Any error from the database or the callback. */ const executeTransaction = (client, callback, isolationLevel) => __awaiter(void 0, void 0, void 0, function* () { const isolation = Object.values(IsolationLevel).includes(isolationLevel) ? isolationLevel : undefined; try { yield client.query(isolation ? `START TRANSACTION ISOLATION LEVEL ${isolation}` : 'BEGIN'); const result = yield callback(client); yield client.query('COMMIT'); (0, database_1.releaseIfPoolClient)(client); return result; } catch (err) { const error = (0, error_1.ensureExtendedError)(err, 'DB_ERROR'); try { yield client.query('ROLLBACK'); } catch (rollbackError) { error.innerError = (0, error_1.ensureExtendedError)(rollbackError, 'DB_ERROR'); } (0, database_1.releaseIfPoolClient)(client, error); throw error; } }); exports.executeTransaction = executeTransaction; /** * Creates a PoolClient from the given pool and attaches the logger for error logging. * If the logger is provided and has the log level 'trace' the client will track * all the queries that were done during its lifetime. The queries are appended * to errors that are thrown from that client/connection in the custom property * `queryStack` on the pg library error. * @param pool A PostgreSQL database pool from which clients can be created. * @param logger A logger that will be registered for the on-error event of the client. * @returns The PoolClient object */ const getClient = (pool, logger) => __awaiter(void 0, void 0, void 0, function* () { var _a; const client = yield pool.connect(); if (logger) { const appendQueryStack = (error) => { if ('queryStack' in client && error instanceof Error) { error.queryStack = client.queryStack; } }; // The pool can return a new or an old client - thus we remove existing event listeners client.removeAllListeners('error'); client.on('error', (err) => { const error = (0, error_1.ensureExtendedError)(err, 'DB_ERROR'); appendQueryStack(error); logger.error(error, 'PostgreSQL client error'); }); // Track the queries in case the log level is 'trace' if (((_a = logger.level) === null || _a === void 0 ? void 0 : _a.toLocaleLowerCase()) === 'trace') { const c = client; if (c.queryStack === undefined) { // Wrap the client query to track all queries (only once as clients are reused) const query = client.query; const queryWrapper = (queryOrConfig, values, callback) => __awaiter(void 0, void 0, void 0, function* () { var _a; if (typeof queryOrConfig === 'object' && 'text' in queryOrConfig) { c.queryStack.push({ query: queryOrConfig.text, values: (_a = queryOrConfig.values) !== null && _a !== void 0 ? _a : values, }); } else { c.queryStack.push({ query: queryOrConfig, values }); } // This matches the pg implementation - TypeScript types are too strict return query.call(client, queryOrConfig, values, callback); }); client.query = queryWrapper; // Subscribing to "message" and append the stack if the parameter is an error object c.connection.removeAllListeners('message'); c.connection.on('message', (error) => { appendQueryStack(error); }); } c.queryStack = []; } } return client; }); exports.getClient = getClient; /** * Process incoming promises in a pool with a maximum size. When that size is not * reached it tries to fill the processing pool up to the `getBatchSize` * number. It runs in an endless loop until the signal `stopped` variable is set * to true. * @param processingPool The promise items in the pool which are being worked on * @param getNextBatch Get the next items to fill up the pool. Gets the batch size input parameter from the `getBatchSize` parameter * @param getBatchSize Async function that gets the number of batch items that are currently processed * @param signal A signal object to stop the processing. Setting the stopped property to true will stop the loop * @param maxDelayInMs The maximum number of milliseconds to wait before checking if new items are there */ const processPool = (processingPool, getNextBatch, getBatchSize, signal, maxDelayInMs) => __awaiter(void 0, void 0, void 0, function* () { while (!signal.stopped) { // get the dynamic pool size - and one await is needed to allow event loop to continue const poolSize = yield getBatchSize(processingPool.size); const diff = poolSize - processingPool.size; if (diff > 0) { const items = yield getNextBatch(diff); items.forEach((item) => { processingPool.add(item); item.then(() => { processingPool.delete(item); }); }); const awaitItems = Array.from(processingPool); if (awaitItems.length < poolSize) { // If there are not enough items then try again at least after this duration awaitItems.push((0, exports.sleep)(maxDelayInMs)); } yield Promise.race(awaitItems); } } }); exports.processPool = processPool; /** * Execute a function in a safe way e.g. pool.end where multiple callers could * have called it where the second one will throw an error. * @param it the function to execute */ const justDoIt = (it) => __awaiter(void 0, void 0, void 0, function* () { try { yield it(); } catch (_a) { // noop } }); exports.justDoIt = justDoIt; //# sourceMappingURL=utils.js.map