import type { Logger } from "pino"; import type { EntityManager as FullEntityManager } from "typeorm"; type TransactionEvents = { /** Emitted when the transaction has committed for the purposes of the application. */ commit: undefined; /** Emitted when the transaction has rolled back for the purposes of the application. */ rollback: { reason: string; } | undefined; }; /** Thrown when the timeout associated with the root transaction elapses before the tx is committed or rolled back */ export declare class NestableTransactionTimeout extends Error { constructor(message?: string); } /** Options used when constructing a `Transaction` */ export interface TransactionOptions { /** * The global timeout for a transaction. * * By "global", we mean the timeout starts from the initial transaction, not each nesting level. */ timeoutDurationMS?: number; } /** * Narrowed interface for an EntityManager object from TypeORM that lets us pass custom versions of the EntityManager around */ export type AnyEntityManager = Pick; export type AfterCommitCallback = () => void | Promise; /** * Represents a running transaction, which can either be a root transaction managed with BEGIN/ROLLBACK/COMMIT, or a nested transaction managed with SAVEPOINT/RELEASE SAVEPOINT/ROLLBACK TO SAVEPOINT. * Has a reference to the connection which the transaction is open on via `db`, and maybe has a parent transaction which governs if `BEGIN` / `SAVEPOINT` is used. * * *Note*: There's a significant mismatch between the way Node works and the way that Postgres actually works with savepoints. One might assume that you could open an outer transaction, and then open three inner transactions, and then rollback any of the inner transactions and go along your merry way. This would be seeing the group of open transactions as a "tree" of potential histories, where any branch can be flipped back at any time. This is *not* how savepoints work: there is only one, linear history in the database for the whole group of nested transactions. If you open a root transaction and then three nested ones, and then roll back the 1st nested transaction, the other two nested transactions opened after it will be rolled back as well. There's one transaction timeline, and rollbacks go back to a point on that timeline. * For this reason, we don't have model each layer of the transaction as an object, and instead model it as a stateful object with a counter representing the current number of savepoints. */ export declare class Transaction { db: EntityManager; /** A unique key to identify this transaction */ key: string; /** The current nesting level of this transaction */ depth: number; /** Logger tagged with this transaction's information */ logger: Logger; /** Emitter for commit/rollback events */ private events; private afterCommitCallbacks; /** Timer used to track how long a transaction is held open */ private startTime?; /** * Controls the behaviour of `begin`, and `commit`/`rollback` once reaching the root depth (`transaction.depth == 0`). * * When `true`, savepoints are used when we `begin` a transaction. `commit` will `RELEASE SAVEPOINT` and `rollback` will `ROLLBACK TO SAVEPOINT`. * Otherwise, regular `COMMIT` and `ROLLBACK` will be used. */ private rootIsSavepoint; /** This queue acts like a mutex, by only allowing one operation added to it to run at a time */ private readonly queue; /** Handle for the global transaction timeout */ private timeoutHandle; constructor(db: EntityManager, options: TransactionOptions); /** Listen to events on this transaction */ on(event: E, listener: (data: TransactionEvents[E]) => void | Promise): void; /** Await an event on this transaction */ once(event: E): Promise; /** * Returns whether or not this transaction is active. * * A transaction is considered active if the underlying query runner it's using has an active transaction and its depth is above 0. */ get isActive(): boolean; /** * Begin the transaction. * * If the transaction has already begun, or the query runner associated with this `EntityManager` is already in a transaction, a new savepoint is created. * Otherwise, a new query runner is created and a database transaction started. */ begin(): Promise; /** * Commit the transaction. * * If we began with a query runner with an open DB transaction, or we're at a depth greater than 1, we only release the associated savepoint for the current depth. * Otherwise, we commit the DB transaction. */ commit(): Promise; /** * Roll back the transaction. * * If we began with a query runner with an open DB transaction, or we're at a depth greater than 1, we only rollback to the associated savepoint for the current depth. * Otherwise, we roll back the DB transaction. */ rollback(reason?: TransactionEvents["rollback"]): Promise; /** * Fully roll back the transaction. * * If we began with a query runner with an open DB transaction, we only rollback to the savepoint for depth 1. * Otherwise, we roll back the DB transaction. */ rollbackRoot(reason?: TransactionEvents["rollback"]): Promise; runAfterCommit(callback: AfterCommitCallback): void; private timeout; /** Fire the final commit/rollback event. */ private fireFinalEvent; private rollbackInternal; private finally; } /** * Postgres specific nested transaction support on top of TypeORM * * :eyeroll: * * We use this because TypeORM doesn't have support for SAVEPOINTs or other nested transaction mechanisms, and doesn't seem to want to add it because because all the DBs it supports don't support them themselves. So, we add the functionality in userland. * NestableTransaction also supports emitting events so commits or rollbacks can trigger behaviour. The `commit` or `rollback` event for a nested transaction fires when the *root* transaction commits or rolls back, as in when durability is actually achieved, and other processes in the system can now read what has been written. This is useful for something like enqueuing background jobs that are about to read data the transaction has just written so that those background jobs definitely can find the data. */ export declare function NestableTransaction(root: EntityManager, transact: (manager: EntityManager, transaction: Transaction) => Promise): Promise; export declare function NestableTransaction(root: EntityManager, timeout: number, transact: (manager: EntityManager, transaction: Transaction) => Promise): Promise; /** * Nestable transaction kickoff that can be used imperatively * If there's no outstanding transaction for the given `EntityManager`, starts a new one. If there is, starts a nested one. **/ export declare const startNestableTransaction: (db: EntityManager, timeout?: number) => Promise>; /** * Nestable transaction rollback that can be used imperatively * Returns a new `EntityManager` that the calling code can continue to use, which sometimes will still be in an outer transaction, and sometimes won't. It's important that the calling code use the returned value in order to preserve nested transactional semantics. **/ export declare const rollbackNestableTransaction: (transaction: Transaction, reason?: any) => Promise; /** * Nestable transaction commit that can be used imperatively * Returns a new `EntityManager` that the calling code can continue to use, which sometimes will still be in an outer transaction, and sometimes won't. It's important that the calling code use the returned value in order to preserve nested transactional semantics. **/ export declare const commitNestableTransaction: (transaction: Transaction) => Promise; export declare const isWithinTransaction: (db: AnyEntityManager) => boolean | undefined; export declare const runAfterCommitOrImmediately: (db: AnyEntityManager, callback: AfterCommitCallback) => Promise; export {};