@vendure/core
Version:
A modern, headless ecommerce framework
111 lines • 4.92 kB
JavaScript
;
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];
const inheritedQueryRunner = (entityManager === null || entityManager === void 0 ? void 0 : entityManager.queryRunner) && !entityManager.queryRunner.isReleased
? entityManager.queryRunner
: undefined;
const queryRunner = inheritedQueryRunner !== null && inheritedQueryRunner !== void 0 ? inheritedQueryRunner : 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 {
// Only release the QueryRunner if we created it ourselves. If it was inherited
// from an outer transaction (e.g. a @Transaction('manual') caller), releasing it
// would invalidate the caller's context. The active-transaction check additionally
// covers the nested-savepoint case where the parent transaction is still open.
if (!inheritedQueryRunner &&
!queryRunner.isTransactionActive &&
queryRunner.isReleased === false) {
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