UNPKG

@vendure/core

Version:

A modern, headless ecommerce framework

107 lines 4.48 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.TransactionWrapper = void 0; const rxjs_1 = require("rxjs"); const operators_1 = require("rxjs/operators"); const TransactionAlreadyStartedError_1 = require("typeorm/error/TransactionAlreadyStartedError"); const constants_1 = require("../common/constants"); /** * @description * This helper class is used to wrap operations in a TypeORM transaction in order to ensure * atomic operations on the database. */ class TransactionWrapper { /** * @description * Executes the `work` function within the context of a transaction. If the `work` function * resolves / completes, then all the DB operations it contains will be committed. If it * throws an error or rejects, then all DB operations will be rolled back. * * @note * This function does not mutate your context. Instead, this function makes a copy and passes * context to work function. */ async executeInTransaction(originalCtx, work, mode, isolationLevel, connection) { // Copy to make sure original context will remain valid after transaction completes const ctx = originalCtx.copy(); const entityManager = ctx[constants_1.TRANSACTION_MANAGER_KEY]; let queryRunner = entityManager === null || entityManager === void 0 ? void 0 : entityManager.queryRunner; if (!queryRunner || queryRunner.isReleased) { queryRunner = connection.createQueryRunner(); } if (mode === 'auto') { await this.startTransaction(queryRunner, isolationLevel); } ctx[constants_1.TRANSACTION_MANAGER_KEY] = queryRunner.manager; try { const maxRetries = 5; const result = await (0, rxjs_1.lastValueFrom)((0, rxjs_1.from)(work(ctx)).pipe((0, operators_1.retryWhen)(errors => errors.pipe((0, operators_1.tap)(err => { if (!this.isRetriableError(err)) { throw err; } }), (0, operators_1.take)(maxRetries))))); if (queryRunner.isTransactionActive) { await queryRunner.commitTransaction(); } return result; } catch (error) { if (queryRunner.isTransactionActive) { await queryRunner.rollbackTransaction(); } throw error; } finally { if (!queryRunner.isTransactionActive && queryRunner.isReleased === false) { // There is a check for an active transaction // because this could be a nested transaction (savepoint). await queryRunner.release(); } } } /** * Attempts to start a DB transaction, with retry logic in the case that a transaction * is already started for the connection (which is mainly a problem with SQLite/Sql.js) */ async startTransaction(queryRunner, isolationLevel) { const maxRetries = 25; let attempts = 0; let lastError; // Returns false if a transaction is already in progress async function attemptStartTransaction() { try { await queryRunner.startTransaction(isolationLevel); return true; } catch (err) { lastError = err; if (err instanceof TransactionAlreadyStartedError_1.TransactionAlreadyStartedError) { return false; } throw err; } } while (attempts < maxRetries) { const result = await attemptStartTransaction(); if (result) { return; } attempts++; // insert an increasing delay before retrying await new Promise(resolve => setTimeout(resolve, attempts * 20)); } throw lastError; } /** * If the resolver function throws an error, there are certain cases in which * we want to retry the whole thing again - notably in the case of a deadlock * situation, which can usually be retried with success. */ isRetriableError(err) { const mysqlDeadlock = err.code === 'ER_LOCK_DEADLOCK'; const postgresDeadlock = err.code === 'deadlock_detected'; return mysqlDeadlock || postgresDeadlock; } } exports.TransactionWrapper = TransactionWrapper; //# sourceMappingURL=transaction-wrapper.js.map