directus
1import { type Knex } from 'knex';2import { getDatabaseClient } from '../database/index.js';3import { useLogger } from '../logger.js';4
5/**
6* Execute the given handler within the current transaction or a newly created one
7* if the current knex state isn't a transaction yet.
8*
9* Can be used to ensure the handler is run within a transaction,
10* while preventing nested transactions.
11*/
12export const transaction = async <T = unknown>(knex: Knex, handler: (knex: Knex) => Promise<T>): Promise<T> => {13if (knex.isTransaction) {14return handler(knex);15} else {16try {17return await knex.transaction((trx) => handler(trx));18} catch (error: any) {19const client = getDatabaseClient(knex);20
21/**22* This error code indicates that the transaction failed due to another
23* concurrent or recent transaction attempting to write to the same data.
24* This can usually be solved by restarting the transaction on client-side
25* after a short delay, so that it is executed against the latest state.
26*
27* @link https://www.cockroachlabs.com/docs/stable/transaction-retry-error-reference
28*/
29const COCKROACH_RETRY_ERROR_CODE = '40001';30
31if (client !== 'cockroachdb' || error?.code !== COCKROACH_RETRY_ERROR_CODE) throw error;32
33const MAX_ATTEMPTS = 3;34const BASE_DELAY = 100;35
36const logger = useLogger();37
38for (let attempt = 0; attempt < MAX_ATTEMPTS; ++attempt) {39const delay = 2 ** attempt * BASE_DELAY;40
41await new Promise((resolve) => setTimeout(resolve, delay));42
43logger.trace(`Restarting failed transaction (attempt ${attempt + 1}/${MAX_ATTEMPTS})`);44
45try {46return await knex.transaction((trx) => handler(trx));47} catch (error: any) {48if (error?.code !== COCKROACH_RETRY_ERROR_CODE) throw error;49}50}51
52/** Initial execution + additional attempts */53const attempts = 1 + MAX_ATTEMPTS;54throw new Error(`Transaction failed after ${attempts} attempts`, { cause: error });55}56}57};58