* Starts a transaction and runs a provided function, ensuring the commitTransaction is always attempted when all operations run in the function have completed. * * **IMPORTANT:** This method requires the function passed in to return a Promise. That promise must be made by `await`-ing all opera
(
fn: WithTransactionCallback<T>,
options?: TransactionOptions & {
/**
* Configures a timeoutMS expiry for the entire withTransactionCallback.
*
* @remarks
* - The remaining timeout will not be applied to callback operations that do not use the ClientSession.
* - Overriding timeoutMS for operations executed using the explicit session inside the provided callback will result in a client-side error.
*/
timeoutMS?: number;
}
)
| 712 | * @returns A raw command response or undefined |
| 713 | */ |
| 714 | async withTransaction<T = any>( |
| 715 | fn: WithTransactionCallback<T>, |
| 716 | options?: TransactionOptions & { |
| 717 | /** |
| 718 | * Configures a timeoutMS expiry for the entire withTransactionCallback. |
| 719 | * |
| 720 | * @remarks |
| 721 | * - The remaining timeout will not be applied to callback operations that do not use the ClientSession. |
| 722 | * - Overriding timeoutMS for operations executed using the explicit session inside the provided callback will result in a client-side error. |
| 723 | */ |
| 724 | timeoutMS?: number; |
| 725 | } |
| 726 | ): Promise<T> { |
| 727 | const MAX_TIMEOUT = 120_000; |
| 728 | |
| 729 | const timeoutMS = options?.timeoutMS ?? this.timeoutMS ?? null; |
| 730 | this.timeoutContext = |
| 731 | timeoutMS != null |
| 732 | ? TimeoutContext.create({ |
| 733 | timeoutMS, |
| 734 | serverSelectionTimeoutMS: this.clientOptions.serverSelectionTimeoutMS, |
| 735 | socketTimeoutMS: this.clientOptions.socketTimeoutMS |
| 736 | }) |
| 737 | : null; |
| 738 | |
| 739 | // 1. Define the following: |
| 740 | // 1.1 Record the current monotonic time, which will be used to enforce the 120-second / CSOT timeout before later retry attempts. |
| 741 | // 1.2 Set `transactionAttempt` to `0`. |
| 742 | // 1.3 Set `TIMEOUT_MS` to be `timeoutMS` if given, otherwise MAX_TIMEOUT (120-seconds). |
| 743 | |
| 744 | // Timeout Error propagation |
| 745 | // When the previously encountered error needs to be propagated because there is no more time for another attempt, |
| 746 | // and it is not already a timeout error, then: |
| 747 | // - A timeout error MUST be propagated instead. It MUST expose the previously encountered error as specified in |
| 748 | // the "Errors" section of the CSOT specification. |
| 749 | // - If exposing the previously encountered error from a timeout error is impossible in a driver, then the driver |
| 750 | // is exempt from the requirement and MUST propagate the previously encountered error as is. The timeout error |
| 751 | // MUST copy all error labels from the previously encountered error. |
| 752 | |
| 753 | // The spec describes timeout checks as "elapsed time < TIMEOUT_MS" (where elapsed = now - start). |
| 754 | // We precompute `deadline = now + remainingTimeMS` so each check becomes simply `now < deadline`. |
| 755 | const csotEnabled = !!this.timeoutContext?.csotEnabled(); |
| 756 | const remainingTimeMS = this.timeoutContext?.csotEnabled() |
| 757 | ? this.timeoutContext.remainingTimeMS |
| 758 | : MAX_TIMEOUT; |
| 759 | const deadline = processTimeMS() + remainingTimeMS; |
| 760 | |
| 761 | let committed = false; |
| 762 | let result: T; |
| 763 | |
| 764 | let lastError: Error | null = null; |
| 765 | |
| 766 | try { |
| 767 | retryTransaction: for ( |
| 768 | let transactionAttempt = 0, isRetry = false; |
| 769 | !committed; |
| 770 | ++transactionAttempt, isRetry = transactionAttempt > 0 |
| 771 | ) { |