mongodb 6.5.0 → 6.6.0-dev.20240504.sha.2609953

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (207) hide show
  1. package/README.md +9 -9
  2. package/lib/admin.js +9 -9
  3. package/lib/admin.js.map +1 -1
  4. package/lib/bson.js +33 -22
  5. package/lib/bson.js.map +1 -1
  6. package/lib/bulk/common.js +13 -12
  7. package/lib/bulk/common.js.map +1 -1
  8. package/lib/change_stream.js +28 -18
  9. package/lib/change_stream.js.map +1 -1
  10. package/lib/client-side-encryption/auto_encrypter.js +2 -2
  11. package/lib/client-side-encryption/auto_encrypter.js.map +1 -1
  12. package/lib/client-side-encryption/client_encryption.js +6 -6
  13. package/lib/client-side-encryption/client_encryption.js.map +1 -1
  14. package/lib/client-side-encryption/providers/aws.js +13 -10
  15. package/lib/client-side-encryption/providers/aws.js.map +1 -1
  16. package/lib/client-side-encryption/providers/azure.js +6 -3
  17. package/lib/client-side-encryption/providers/azure.js.map +1 -1
  18. package/lib/cmap/auth/aws_temporary_credentials.js +140 -0
  19. package/lib/cmap/auth/aws_temporary_credentials.js.map +1 -0
  20. package/lib/cmap/auth/gssapi.js +7 -6
  21. package/lib/cmap/auth/gssapi.js.map +1 -1
  22. package/lib/cmap/auth/mongodb_aws.js +8 -101
  23. package/lib/cmap/auth/mongodb_aws.js.map +1 -1
  24. package/lib/cmap/auth/mongodb_oidc/aws_service_workflow.js +1 -1
  25. package/lib/cmap/auth/mongodb_oidc/aws_service_workflow.js.map +1 -1
  26. package/lib/cmap/auth/mongodb_oidc/callback_lock_cache.js +2 -1
  27. package/lib/cmap/auth/mongodb_oidc/callback_lock_cache.js.map +1 -1
  28. package/lib/cmap/auth/mongodb_oidc/service_workflow.js +1 -1
  29. package/lib/cmap/auth/mongodb_oidc/service_workflow.js.map +1 -1
  30. package/lib/cmap/auth/scram.js +2 -2
  31. package/lib/cmap/auth/scram.js.map +1 -1
  32. package/lib/cmap/commands.js +24 -111
  33. package/lib/cmap/commands.js.map +1 -1
  34. package/lib/cmap/connect.js +4 -4
  35. package/lib/cmap/connect.js.map +1 -1
  36. package/lib/cmap/connection.js +61 -36
  37. package/lib/cmap/connection.js.map +1 -1
  38. package/lib/cmap/connection_pool.js +22 -13
  39. package/lib/cmap/connection_pool.js.map +1 -1
  40. package/lib/cmap/handshake/client_metadata.js +2 -2
  41. package/lib/cmap/handshake/client_metadata.js.map +1 -1
  42. package/lib/cmap/wire_protocol/compression.js +8 -8
  43. package/lib/cmap/wire_protocol/compression.js.map +1 -1
  44. package/lib/cmap/wire_protocol/on_demand/document.js +218 -0
  45. package/lib/cmap/wire_protocol/on_demand/document.js.map +1 -0
  46. package/lib/cmap/wire_protocol/responses.js +184 -0
  47. package/lib/cmap/wire_protocol/responses.js.map +1 -0
  48. package/lib/collection.js +42 -38
  49. package/lib/collection.js.map +1 -1
  50. package/lib/connection_string.js +4 -6
  51. package/lib/connection_string.js.map +1 -1
  52. package/lib/cursor/abstract_cursor.js +76 -43
  53. package/lib/cursor/abstract_cursor.js.map +1 -1
  54. package/lib/cursor/aggregation_cursor.js +16 -33
  55. package/lib/cursor/aggregation_cursor.js.map +1 -1
  56. package/lib/cursor/find_cursor.js +36 -18
  57. package/lib/cursor/find_cursor.js.map +1 -1
  58. package/lib/cursor/run_command_cursor.js +3 -2
  59. package/lib/cursor/run_command_cursor.js.map +1 -1
  60. package/lib/db.js +15 -19
  61. package/lib/db.js.map +1 -1
  62. package/lib/deps.js +31 -26
  63. package/lib/deps.js.map +1 -1
  64. package/lib/encrypter.js +14 -5
  65. package/lib/encrypter.js.map +1 -1
  66. package/lib/error.js +4 -3
  67. package/lib/error.js.map +1 -1
  68. package/lib/gridfs/download.js +19 -14
  69. package/lib/gridfs/download.js.map +1 -1
  70. package/lib/gridfs/index.js.map +1 -1
  71. package/lib/gridfs/upload.js +6 -1
  72. package/lib/gridfs/upload.js.map +1 -1
  73. package/lib/index.js.map +1 -1
  74. package/lib/mongo_client.js +11 -7
  75. package/lib/mongo_client.js.map +1 -1
  76. package/lib/mongo_logger.js +3 -0
  77. package/lib/mongo_logger.js.map +1 -1
  78. package/lib/operations/aggregate.js +2 -1
  79. package/lib/operations/aggregate.js.map +1 -1
  80. package/lib/operations/command.js +1 -1
  81. package/lib/operations/command.js.map +1 -1
  82. package/lib/operations/create_collection.js +1 -1
  83. package/lib/operations/create_collection.js.map +1 -1
  84. package/lib/operations/delete.js +4 -3
  85. package/lib/operations/delete.js.map +1 -1
  86. package/lib/operations/drop.js +1 -1
  87. package/lib/operations/drop.js.map +1 -1
  88. package/lib/operations/execute_operation.js +23 -8
  89. package/lib/operations/execute_operation.js.map +1 -1
  90. package/lib/operations/find.js +4 -4
  91. package/lib/operations/find.js.map +1 -1
  92. package/lib/operations/get_more.js +2 -1
  93. package/lib/operations/get_more.js.map +1 -1
  94. package/lib/operations/indexes.js +29 -121
  95. package/lib/operations/indexes.js.map +1 -1
  96. package/lib/operations/insert.js +3 -3
  97. package/lib/operations/insert.js.map +1 -1
  98. package/lib/operations/kill_cursors.js +3 -1
  99. package/lib/operations/kill_cursors.js.map +1 -1
  100. package/lib/operations/list_collections.js +1 -1
  101. package/lib/operations/list_collections.js.map +1 -1
  102. package/lib/operations/list_databases.js +1 -1
  103. package/lib/operations/list_databases.js.map +1 -1
  104. package/lib/operations/operation.js.map +1 -1
  105. package/lib/operations/run_command.js +4 -2
  106. package/lib/operations/run_command.js.map +1 -1
  107. package/lib/operations/search_indexes/create.js.map +1 -1
  108. package/lib/operations/stats.js +1 -1
  109. package/lib/operations/stats.js.map +1 -1
  110. package/lib/operations/update.js +1 -1
  111. package/lib/operations/update.js.map +1 -1
  112. package/lib/sdam/common.js.map +1 -1
  113. package/lib/sdam/monitor.js +139 -42
  114. package/lib/sdam/monitor.js.map +1 -1
  115. package/lib/sdam/server.js +5 -15
  116. package/lib/sdam/server.js.map +1 -1
  117. package/lib/sdam/server_description.js +1 -0
  118. package/lib/sdam/server_description.js.map +1 -1
  119. package/lib/sdam/server_selection.js +1 -1
  120. package/lib/sdam/server_selection.js.map +1 -1
  121. package/lib/sdam/srv_polling.js +2 -1
  122. package/lib/sdam/srv_polling.js.map +1 -1
  123. package/lib/sdam/topology.js +67 -54
  124. package/lib/sdam/topology.js.map +1 -1
  125. package/lib/sdam/topology_description.js +10 -0
  126. package/lib/sdam/topology_description.js.map +1 -1
  127. package/lib/sessions.js +133 -93
  128. package/lib/sessions.js.map +1 -1
  129. package/lib/timeout.js +77 -0
  130. package/lib/timeout.js.map +1 -0
  131. package/lib/utils.js +61 -28
  132. package/lib/utils.js.map +1 -1
  133. package/mongodb.d.ts +150 -38
  134. package/package.json +17 -14
  135. package/src/admin.ts +9 -9
  136. package/src/bson.ts +14 -0
  137. package/src/bulk/common.ts +3 -2
  138. package/src/change_stream.ts +39 -30
  139. package/src/client-side-encryption/auto_encrypter.ts +2 -2
  140. package/src/client-side-encryption/client_encryption.ts +6 -6
  141. package/src/client-side-encryption/providers/aws.ts +17 -10
  142. package/src/client-side-encryption/providers/azure.ts +5 -3
  143. package/src/cmap/auth/aws_temporary_credentials.ts +169 -0
  144. package/src/cmap/auth/gssapi.ts +9 -11
  145. package/src/cmap/auth/mongodb_aws.ts +19 -126
  146. package/src/cmap/auth/mongodb_oidc/aws_service_workflow.ts +1 -1
  147. package/src/cmap/auth/mongodb_oidc/callback_lock_cache.ts +2 -1
  148. package/src/cmap/auth/mongodb_oidc/service_workflow.ts +1 -1
  149. package/src/cmap/auth/scram.ts +2 -2
  150. package/src/cmap/commands.ts +28 -132
  151. package/src/cmap/connect.ts +4 -4
  152. package/src/cmap/connection.ts +107 -43
  153. package/src/cmap/connection_pool.ts +32 -29
  154. package/src/cmap/handshake/client_metadata.ts +2 -5
  155. package/src/cmap/wire_protocol/compression.ts +11 -13
  156. package/src/cmap/wire_protocol/on_demand/document.ts +338 -0
  157. package/src/cmap/wire_protocol/responses.ts +237 -0
  158. package/src/collection.ts +87 -58
  159. package/src/connection_string.ts +9 -7
  160. package/src/cursor/abstract_cursor.ts +102 -38
  161. package/src/cursor/aggregation_cursor.ts +32 -34
  162. package/src/cursor/find_cursor.ts +33 -21
  163. package/src/cursor/list_search_indexes_cursor.ts +1 -1
  164. package/src/cursor/run_command_cursor.ts +3 -2
  165. package/src/db.ts +42 -21
  166. package/src/deps.ts +52 -40
  167. package/src/encrypter.ts +14 -5
  168. package/src/error.ts +9 -3
  169. package/src/gridfs/download.ts +19 -31
  170. package/src/gridfs/index.ts +2 -0
  171. package/src/gridfs/upload.ts +11 -8
  172. package/src/index.ts +13 -5
  173. package/src/mongo_client.ts +21 -15
  174. package/src/mongo_logger.ts +3 -0
  175. package/src/mongo_types.ts +1 -1
  176. package/src/operations/aggregate.ts +2 -1
  177. package/src/operations/command.ts +1 -1
  178. package/src/operations/create_collection.ts +7 -2
  179. package/src/operations/delete.ts +4 -3
  180. package/src/operations/drop.ts +1 -1
  181. package/src/operations/execute_operation.ts +29 -10
  182. package/src/operations/find.ts +13 -14
  183. package/src/operations/get_more.ts +9 -1
  184. package/src/operations/indexes.ts +103 -176
  185. package/src/operations/insert.ts +2 -2
  186. package/src/operations/kill_cursors.ts +3 -2
  187. package/src/operations/list_collections.ts +5 -1
  188. package/src/operations/list_databases.ts +1 -1
  189. package/src/operations/operation.ts +3 -0
  190. package/src/operations/run_command.ts +6 -4
  191. package/src/operations/search_indexes/create.ts +4 -1
  192. package/src/operations/stats.ts +1 -1
  193. package/src/operations/update.ts +7 -7
  194. package/src/sdam/common.ts +8 -2
  195. package/src/sdam/monitor.ts +178 -61
  196. package/src/sdam/server.ts +27 -20
  197. package/src/sdam/server_description.ts +8 -3
  198. package/src/sdam/server_selection.ts +2 -3
  199. package/src/sdam/srv_polling.ts +3 -2
  200. package/src/sdam/topology.ts +114 -117
  201. package/src/sdam/topology_description.ts +14 -4
  202. package/src/sessions.ts +168 -148
  203. package/src/timeout.ts +96 -0
  204. package/src/utils.ts +85 -32
  205. package/lib/operations/common_functions.js +0 -38
  206. package/lib/operations/common_functions.js.map +0 -1
  207. package/src/operations/common_functions.ts +0 -79
package/src/sessions.ts CHANGED
@@ -1,8 +1,7 @@
1
- import { promisify } from 'util';
2
-
3
1
  import { Binary, type Document, Long, type Timestamp } from './bson';
4
2
  import type { CommandOptions, Connection } from './cmap/connection';
5
3
  import { ConnectionPoolMetrics } from './cmap/metrics';
4
+ import { type MongoDBResponse } from './cmap/wire_protocol/responses';
6
5
  import { isSharded } from './cmap/wire_protocol/shared';
7
6
  import { PINNED, UNPINNED } from './constants';
8
7
  import type { AbstractCursor } from './cursor/abstract_cursor';
@@ -38,12 +37,12 @@ import {
38
37
  import {
39
38
  ByteUtils,
40
39
  calculateDurationInMs,
41
- type Callback,
42
40
  commandSupportsReadConcern,
43
41
  isPromiseLike,
44
42
  List,
45
43
  maxWireVersion,
46
44
  now,
45
+ squashError,
47
46
  uuidV4
48
47
  } from './utils';
49
48
  import { WriteConcern } from './write_concern';
@@ -58,6 +57,9 @@ export interface ClientSessionOptions {
58
57
  snapshot?: boolean;
59
58
  /** The default TransactionOptions to use for transactions started on this session. */
60
59
  defaultTransactionOptions?: TransactionOptions;
60
+ /** @internal
61
+ * The value of timeoutMS used for CSOT. Used to override client timeoutMS */
62
+ defaultTimeoutMS?: number;
61
63
 
62
64
  /** @internal */
63
65
  owner?: symbol | AbstractCursor;
@@ -128,6 +130,8 @@ export class ClientSession extends TypedEventEmitter<ClientSessionEvents> {
128
130
  [kPinnedConnection]?: Connection;
129
131
  /** @internal */
130
132
  [kTxnNumberIncrement]: number;
133
+ /** @internal */
134
+ timeoutMS?: number;
131
135
 
132
136
  /**
133
137
  * Create a client session.
@@ -170,6 +174,7 @@ export class ClientSession extends TypedEventEmitter<ClientSessionEvents> {
170
174
  this.sessionPool = sessionPool;
171
175
  this.hasEnded = false;
172
176
  this.clientOptions = clientOptions;
177
+ this.timeoutMS = options.defaultTimeoutMS ?? client.options?.timeoutMS;
173
178
 
174
179
  this.explicit = !!options.explicit;
175
180
  this[kServerSession] = this.explicit ? this.sessionPool.acquire() : null;
@@ -185,7 +190,7 @@ export class ClientSession extends TypedEventEmitter<ClientSessionEvents> {
185
190
 
186
191
  this.operationTime = undefined;
187
192
  this.owner = options.owner;
188
- this.defaultTransactionOptions = Object.assign({}, options.defaultTransactionOptions);
193
+ this.defaultTransactionOptions = { ...options.defaultTransactionOptions };
189
194
  this.transaction = new Transaction();
190
195
  }
191
196
 
@@ -274,8 +279,9 @@ export class ClientSession extends TypedEventEmitter<ClientSessionEvents> {
274
279
  this.hasEnded = true;
275
280
  this.emit('ended', this);
276
281
  }
277
- } catch {
282
+ } catch (error) {
278
283
  // spec indicates that we should ignore all errors for `endSessions`
284
+ squashError(error);
279
285
  } finally {
280
286
  maybeClearPinnedConnection(this, { force: true, ...options });
281
287
  }
@@ -415,14 +421,14 @@ export class ClientSession extends TypedEventEmitter<ClientSessionEvents> {
415
421
  * Commits the currently active transaction in this session.
416
422
  */
417
423
  async commitTransaction(): Promise<void> {
418
- return endTransactionAsync(this, 'commitTransaction');
424
+ return await endTransaction(this, 'commitTransaction');
419
425
  }
420
426
 
421
427
  /**
422
428
  * Aborts the currently active transaction in this session.
423
429
  */
424
430
  async abortTransaction(): Promise<void> {
425
- return endTransactionAsync(this, 'abortTransaction');
431
+ return await endTransaction(this, 'abortTransaction');
426
432
  }
427
433
 
428
434
  /**
@@ -435,18 +441,26 @@ export class ClientSession extends TypedEventEmitter<ClientSessionEvents> {
435
441
  /**
436
442
  * Starts a transaction and runs a provided function, ensuring the commitTransaction is always attempted when all operations run in the function have completed.
437
443
  *
438
- * **IMPORTANT:** This method requires the user to return a Promise, and `await` all operations.
444
+ * **IMPORTANT:** This method requires the function passed in to return a Promise. That promise must be made by `await`-ing all operations in such a way that rejections are propagated to the returned promise.
439
445
  *
440
446
  * @remarks
441
- * This function:
442
- * - If all operations successfully complete and the `commitTransaction` operation is successful, then this function will return the result of the provided function.
443
- * - If the transaction is unable to complete or an error is thrown from within the provided function, then this function will throw an error.
447
+ * - If all operations successfully complete and the `commitTransaction` operation is successful, then the provided function will return the result of the provided function.
448
+ * - If the transaction is unable to complete or an error is thrown from within the provided function, then the provided function will throw an error.
444
449
  * - If the transaction is manually aborted within the provided function it will not throw.
445
- * - May be called multiple times if the driver needs to attempt to retry the operations.
450
+ * - If the driver needs to attempt to retry the operations, the provided function may be called multiple times.
446
451
  *
447
452
  * Checkout a descriptive example here:
448
453
  * @see https://www.mongodb.com/blog/post/quick-start-nodejs--mongodb--how-to-implement-transactions
449
454
  *
455
+ * If a command inside withTransaction fails:
456
+ * - It may cause the transaction on the server to be aborted.
457
+ * - This situation is normally handled transparently by the driver.
458
+ * - However, if the application catches such an error and does not rethrow it, the driver will not be able to determine whether the transaction was aborted or not.
459
+ * - The driver will then retry the transaction indefinitely.
460
+ *
461
+ * To avoid this situation, the application must not silently handle errors within the provided function.
462
+ * If the application needs to handle errors within, it must await all operations such that if an operation is rejected it becomes the rejection of the callback function passed into withTransaction.
463
+ *
450
464
  * @param fn - callback to run within a transaction
451
465
  * @param options - optional settings for the transaction
452
466
  * @returns A raw command response or undefined
@@ -456,7 +470,7 @@ export class ClientSession extends TypedEventEmitter<ClientSessionEvents> {
456
470
  options?: TransactionOptions
457
471
  ): Promise<T> {
458
472
  const startTime = now();
459
- return attemptTransaction(this, startTime, fn, options);
473
+ return await attemptTransaction(this, startTime, fn, options);
460
474
  }
461
475
  }
462
476
 
@@ -511,6 +525,7 @@ export function maybeClearPinnedConnection(
511
525
 
512
526
  if (options?.error == null || options?.force) {
513
527
  loadBalancer.pool.checkIn(conn);
528
+ session[kPinnedConnection] = undefined;
514
529
  conn.emit(
515
530
  UNPINNED,
516
531
  session.transaction.state !== TxnState.NO_TRANSACTION
@@ -522,8 +537,6 @@ export function maybeClearPinnedConnection(
522
537
  loadBalancer.pool.clear({ serviceId: conn.serviceId });
523
538
  }
524
539
  }
525
-
526
- session[kPinnedConnection] = undefined;
527
540
  }
528
541
  }
529
542
 
@@ -538,33 +551,33 @@ function isMaxTimeMSExpiredError(err: MongoError) {
538
551
  );
539
552
  }
540
553
 
541
- function attemptTransactionCommit<T>(
554
+ async function attemptTransactionCommit<T>(
542
555
  session: ClientSession,
543
556
  startTime: number,
544
557
  fn: WithTransactionCallback<T>,
545
- result: any,
558
+ result: T,
546
559
  options: TransactionOptions
547
560
  ): Promise<T> {
548
- return session.commitTransaction().then(
549
- () => result,
550
- (err: MongoError) => {
551
- if (
552
- err instanceof MongoError &&
553
- hasNotTimedOut(startTime, MAX_WITH_TRANSACTION_TIMEOUT) &&
554
- !isMaxTimeMSExpiredError(err)
555
- ) {
556
- if (err.hasErrorLabel(MongoErrorLabel.UnknownTransactionCommitResult)) {
557
- return attemptTransactionCommit(session, startTime, fn, result, options);
558
- }
559
-
560
- if (err.hasErrorLabel(MongoErrorLabel.TransientTransactionError)) {
561
- return attemptTransaction(session, startTime, fn, options);
562
- }
561
+ try {
562
+ await session.commitTransaction();
563
+ return result;
564
+ } catch (commitErr) {
565
+ if (
566
+ commitErr instanceof MongoError &&
567
+ hasNotTimedOut(startTime, MAX_WITH_TRANSACTION_TIMEOUT) &&
568
+ !isMaxTimeMSExpiredError(commitErr)
569
+ ) {
570
+ if (commitErr.hasErrorLabel(MongoErrorLabel.UnknownTransactionCommitResult)) {
571
+ return await attemptTransactionCommit(session, startTime, fn, result, options);
563
572
  }
564
573
 
565
- throw err;
574
+ if (commitErr.hasErrorLabel(MongoErrorLabel.TransientTransactionError)) {
575
+ return await attemptTransaction(session, startTime, fn, options);
576
+ }
566
577
  }
567
- );
578
+
579
+ throw commitErr;
580
+ }
568
581
  }
569
582
 
570
583
  const USER_EXPLICIT_TXN_END_STATES = new Set<TxnState>([
@@ -577,12 +590,12 @@ function userExplicitlyEndedTransaction(session: ClientSession) {
577
590
  return USER_EXPLICIT_TXN_END_STATES.has(session.transaction.state);
578
591
  }
579
592
 
580
- function attemptTransaction<T>(
593
+ async function attemptTransaction<T>(
581
594
  session: ClientSession,
582
595
  startTime: number,
583
596
  fn: WithTransactionCallback<T>,
584
597
  options: TransactionOptions = {}
585
- ): Promise<any> {
598
+ ): Promise<T> {
586
599
  session.startTransaction(options);
587
600
 
588
601
  let promise;
@@ -593,65 +606,52 @@ function attemptTransaction<T>(
593
606
  }
594
607
 
595
608
  if (!isPromiseLike(promise)) {
596
- session.abortTransaction().catch(() => null);
597
- return Promise.reject(
598
- new MongoInvalidArgumentError('Function provided to `withTransaction` must return a Promise')
609
+ try {
610
+ await session.abortTransaction();
611
+ } catch (error) {
612
+ squashError(error);
613
+ }
614
+ throw new MongoInvalidArgumentError(
615
+ 'Function provided to `withTransaction` must return a Promise'
599
616
  );
600
617
  }
601
618
 
602
- return promise.then(
603
- result => {
604
- if (userExplicitlyEndedTransaction(session)) {
605
- return result;
606
- }
607
-
608
- return attemptTransactionCommit(session, startTime, fn, result, options);
609
- },
610
- err => {
611
- function maybeRetryOrThrow(err: MongoError): Promise<any> {
612
- if (
613
- err instanceof MongoError &&
614
- err.hasErrorLabel(MongoErrorLabel.TransientTransactionError) &&
615
- hasNotTimedOut(startTime, MAX_WITH_TRANSACTION_TIMEOUT)
616
- ) {
617
- return attemptTransaction(session, startTime, fn, options);
618
- }
619
-
620
- if (isMaxTimeMSExpiredError(err)) {
621
- err.addErrorLabel(MongoErrorLabel.UnknownTransactionCommitResult);
622
- }
623
-
624
- throw err;
625
- }
619
+ try {
620
+ const result = await promise;
621
+ if (userExplicitlyEndedTransaction(session)) {
622
+ return result;
623
+ }
624
+ return await attemptTransactionCommit(session, startTime, fn, result, options);
625
+ } catch (err) {
626
+ if (session.inTransaction()) {
627
+ await session.abortTransaction();
628
+ }
626
629
 
627
- if (session.inTransaction()) {
628
- return session.abortTransaction().then(() => maybeRetryOrThrow(err));
629
- }
630
+ if (
631
+ err instanceof MongoError &&
632
+ err.hasErrorLabel(MongoErrorLabel.TransientTransactionError) &&
633
+ hasNotTimedOut(startTime, MAX_WITH_TRANSACTION_TIMEOUT)
634
+ ) {
635
+ return await attemptTransaction(session, startTime, fn, options);
636
+ }
630
637
 
631
- return maybeRetryOrThrow(err);
638
+ if (isMaxTimeMSExpiredError(err)) {
639
+ err.addErrorLabel(MongoErrorLabel.UnknownTransactionCommitResult);
632
640
  }
633
- );
634
- }
635
641
 
636
- const endTransactionAsync = promisify(
637
- endTransaction as (
638
- session: ClientSession,
639
- commandName: 'abortTransaction' | 'commitTransaction',
640
- callback: (error: Error) => void
641
- ) => void
642
- );
642
+ throw err;
643
+ }
644
+ }
643
645
 
644
- function endTransaction(
646
+ async function endTransaction(
645
647
  session: ClientSession,
646
- commandName: 'abortTransaction' | 'commitTransaction',
647
- callback: Callback<void>
648
- ) {
648
+ commandName: 'abortTransaction' | 'commitTransaction'
649
+ ): Promise<void> {
649
650
  // handle any initial problematic cases
650
651
  const txnState = session.transaction.state;
651
652
 
652
653
  if (txnState === TxnState.NO_TRANSACTION) {
653
- callback(new MongoTransactionError('No transaction started'));
654
- return;
654
+ throw new MongoTransactionError('No transaction started');
655
655
  }
656
656
 
657
657
  if (commandName === 'commitTransaction') {
@@ -661,37 +661,32 @@ function endTransaction(
661
661
  ) {
662
662
  // the transaction was never started, we can safely exit here
663
663
  session.transaction.transition(TxnState.TRANSACTION_COMMITTED_EMPTY);
664
- callback();
665
664
  return;
666
665
  }
667
666
 
668
667
  if (txnState === TxnState.TRANSACTION_ABORTED) {
669
- callback(
670
- new MongoTransactionError('Cannot call commitTransaction after calling abortTransaction')
668
+ throw new MongoTransactionError(
669
+ 'Cannot call commitTransaction after calling abortTransaction'
671
670
  );
672
- return;
673
671
  }
674
672
  } else {
675
673
  if (txnState === TxnState.STARTING_TRANSACTION) {
676
674
  // the transaction was never started, we can safely exit here
677
675
  session.transaction.transition(TxnState.TRANSACTION_ABORTED);
678
- callback();
679
676
  return;
680
677
  }
681
678
 
682
679
  if (txnState === TxnState.TRANSACTION_ABORTED) {
683
- callback(new MongoTransactionError('Cannot call abortTransaction twice'));
684
- return;
680
+ throw new MongoTransactionError('Cannot call abortTransaction twice');
685
681
  }
686
682
 
687
683
  if (
688
684
  txnState === TxnState.TRANSACTION_COMMITTED ||
689
685
  txnState === TxnState.TRANSACTION_COMMITTED_EMPTY
690
686
  ) {
691
- callback(
692
- new MongoTransactionError('Cannot call abortTransaction after calling commitTransaction')
687
+ throw new MongoTransactionError(
688
+ 'Cannot call abortTransaction after calling commitTransaction'
693
689
  );
694
- return;
695
690
  }
696
691
  }
697
692
 
@@ -718,49 +713,38 @@ function endTransaction(
718
713
  Object.assign(command, { maxTimeMS: session.transaction.options.maxTimeMS });
719
714
  }
720
715
 
721
- function commandHandler(error?: Error) {
716
+ if (session.transaction.recoveryToken) {
717
+ command.recoveryToken = session.transaction.recoveryToken;
718
+ }
719
+
720
+ try {
721
+ // send the command
722
+ await executeOperation(
723
+ session.client,
724
+ new RunAdminCommandOperation(command, {
725
+ session,
726
+ readPreference: ReadPreference.primary,
727
+ bypassPinningCheck: true
728
+ })
729
+ );
730
+ if (command.abortTransaction) {
731
+ // always unpin on abort regardless of command outcome
732
+ session.unpin();
733
+ }
722
734
  if (commandName !== 'commitTransaction') {
723
735
  session.transaction.transition(TxnState.TRANSACTION_ABORTED);
724
736
  if (session.loadBalanced) {
725
737
  maybeClearPinnedConnection(session, { force: false });
726
738
  }
727
-
728
- // The spec indicates that we should ignore all errors on `abortTransaction`
729
- return callback();
730
- }
731
-
732
- session.transaction.transition(TxnState.TRANSACTION_COMMITTED);
733
- if (error instanceof MongoError) {
734
- if (
735
- isRetryableWriteError(error) ||
736
- error instanceof MongoWriteConcernError ||
737
- isMaxTimeMSExpiredError(error)
738
- ) {
739
- if (isUnknownTransactionCommitResult(error)) {
740
- error.addErrorLabel(MongoErrorLabel.UnknownTransactionCommitResult);
741
-
742
- // per txns spec, must unpin session in this case
743
- session.unpin({ error });
744
- }
745
- } else if (error.hasErrorLabel(MongoErrorLabel.TransientTransactionError)) {
746
- session.unpin({ error });
747
- }
739
+ } else {
740
+ session.transaction.transition(TxnState.TRANSACTION_COMMITTED);
748
741
  }
749
-
750
- callback(error);
751
- }
752
-
753
- if (session.transaction.recoveryToken) {
754
- command.recoveryToken = session.transaction.recoveryToken;
755
- }
756
-
757
- const handleFirstCommandAttempt = (error?: Error) => {
742
+ } catch (firstAttemptErr) {
758
743
  if (command.abortTransaction) {
759
744
  // always unpin on abort regardless of command outcome
760
745
  session.unpin();
761
746
  }
762
-
763
- if (error instanceof MongoError && isRetryableWriteError(error)) {
747
+ if (firstAttemptErr instanceof MongoError && isRetryableWriteError(firstAttemptErr)) {
764
748
  // SPEC-1185: apply majority write concern when retrying commitTransaction
765
749
  if (command.commitTransaction) {
766
750
  // per txns spec, must unpin session in this case
@@ -771,29 +755,65 @@ function endTransaction(
771
755
  });
772
756
  }
773
757
 
774
- executeOperation(
775
- session.client,
776
- new RunAdminCommandOperation(command, {
777
- session,
778
- readPreference: ReadPreference.primary,
779
- bypassPinningCheck: true
780
- })
781
- ).then(() => commandHandler(), commandHandler);
782
- return;
758
+ try {
759
+ await executeOperation(
760
+ session.client,
761
+ new RunAdminCommandOperation(command, {
762
+ session,
763
+ readPreference: ReadPreference.primary,
764
+ bypassPinningCheck: true
765
+ })
766
+ );
767
+ if (commandName !== 'commitTransaction') {
768
+ session.transaction.transition(TxnState.TRANSACTION_ABORTED);
769
+ if (session.loadBalanced) {
770
+ maybeClearPinnedConnection(session, { force: false });
771
+ }
772
+ } else {
773
+ session.transaction.transition(TxnState.TRANSACTION_COMMITTED);
774
+ }
775
+ } catch (secondAttemptErr) {
776
+ handleEndTransactionError(session, commandName, secondAttemptErr);
777
+ }
778
+ } else {
779
+ handleEndTransactionError(session, commandName, firstAttemptErr);
780
+ }
781
+ }
782
+ }
783
+
784
+ function handleEndTransactionError(
785
+ session: ClientSession,
786
+ commandName: 'abortTransaction' | 'commitTransaction',
787
+ error: Error
788
+ ) {
789
+ if (commandName !== 'commitTransaction') {
790
+ session.transaction.transition(TxnState.TRANSACTION_ABORTED);
791
+ if (session.loadBalanced) {
792
+ maybeClearPinnedConnection(session, { force: false });
783
793
  }
794
+ // The spec indicates that if the operation times out or fails with a non-retryable error, we should ignore all errors on `abortTransaction`
795
+ return;
796
+ }
797
+
798
+ session.transaction.transition(TxnState.TRANSACTION_COMMITTED);
799
+ if (error instanceof MongoError) {
800
+ if (
801
+ isRetryableWriteError(error) ||
802
+ error instanceof MongoWriteConcernError ||
803
+ isMaxTimeMSExpiredError(error)
804
+ ) {
805
+ if (isUnknownTransactionCommitResult(error)) {
806
+ error.addErrorLabel(MongoErrorLabel.UnknownTransactionCommitResult);
807
+
808
+ // per txns spec, must unpin session in this case
809
+ session.unpin({ error });
810
+ }
811
+ } else if (error.hasErrorLabel(MongoErrorLabel.TransientTransactionError)) {
812
+ session.unpin({ error });
813
+ }
814
+ }
784
815
 
785
- commandHandler(error);
786
- };
787
-
788
- // send the command
789
- executeOperation(
790
- session.client,
791
- new RunAdminCommandOperation(command, {
792
- session,
793
- readPreference: ReadPreference.primary,
794
- bypassPinningCheck: true
795
- })
796
- ).then(() => handleFirstCommandAttempt(), handleFirstCommandAttempt);
816
+ throw error;
797
817
  }
798
818
 
799
819
  /** @public */
@@ -1030,7 +1050,7 @@ export function applySession(
1030
1050
  return;
1031
1051
  }
1032
1052
 
1033
- export function updateSessionFromResponse(session: ClientSession, document: Document): void {
1053
+ export function updateSessionFromResponse(session: ClientSession, document: MongoDBResponse): void {
1034
1054
  if (document.$clusterTime) {
1035
1055
  _advanceClusterTime(session, document.$clusterTime);
1036
1056
  }
@@ -1046,7 +1066,7 @@ export function updateSessionFromResponse(session: ClientSession, document: Docu
1046
1066
  if (session?.[kSnapshotEnabled] && session[kSnapshotTime] == null) {
1047
1067
  // find and aggregate commands return atClusterTime on the cursor
1048
1068
  // distinct includes it in the response body
1049
- const atClusterTime = document.cursor?.atClusterTime || document.atClusterTime;
1069
+ const atClusterTime = document.atClusterTime;
1050
1070
  if (atClusterTime) {
1051
1071
  session[kSnapshotTime] = atClusterTime;
1052
1072
  }
package/src/timeout.ts ADDED
@@ -0,0 +1,96 @@
1
+ import { clearTimeout, setTimeout } from 'timers';
2
+
3
+ import { MongoInvalidArgumentError } from './error';
4
+ import { noop } from './utils';
5
+
6
+ /** @internal */
7
+ export class TimeoutError extends Error {
8
+ override get name(): 'TimeoutError' {
9
+ return 'TimeoutError';
10
+ }
11
+
12
+ constructor(message: string, options?: { cause?: Error }) {
13
+ super(message, options);
14
+ }
15
+
16
+ static is(error: unknown): error is TimeoutError {
17
+ return (
18
+ error != null && typeof error === 'object' && 'name' in error && error.name === 'TimeoutError'
19
+ );
20
+ }
21
+ }
22
+
23
+ type Executor = ConstructorParameters<typeof Promise<never>>[0];
24
+ type Reject = Parameters<ConstructorParameters<typeof Promise<never>>[0]>[1];
25
+ /**
26
+ * @internal
27
+ * This class is an abstraction over timeouts
28
+ * The Timeout class can only be in the pending or rejected states. It is guaranteed not to resolve
29
+ * if interacted with exclusively through its public API
30
+ * */
31
+ export class Timeout extends Promise<never> {
32
+ get [Symbol.toStringTag](): 'MongoDBTimeout' {
33
+ return 'MongoDBTimeout';
34
+ }
35
+
36
+ private id?: NodeJS.Timeout;
37
+
38
+ public readonly start: number;
39
+ public ended: number | null = null;
40
+ public duration: number;
41
+ public timedOut = false;
42
+
43
+ /** Create a new timeout that expires in `duration` ms */
44
+ private constructor(executor: Executor = () => null, duration: number) {
45
+ let reject!: Reject;
46
+
47
+ if (duration < 0) {
48
+ throw new MongoInvalidArgumentError('Cannot create a Timeout with a negative duration');
49
+ }
50
+
51
+ super((_, promiseReject) => {
52
+ reject = promiseReject;
53
+
54
+ executor(noop, promiseReject);
55
+ });
56
+
57
+ this.duration = duration;
58
+ this.start = Math.trunc(performance.now());
59
+
60
+ if (this.duration > 0) {
61
+ this.id = setTimeout(() => {
62
+ this.ended = Math.trunc(performance.now());
63
+ this.timedOut = true;
64
+ reject(new TimeoutError(`Expired after ${duration}ms`));
65
+ }, this.duration);
66
+ // Ensure we do not keep the Node.js event loop running
67
+ if (typeof this.id.unref === 'function') {
68
+ this.id.unref();
69
+ }
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Clears the underlying timeout. This method is idempotent
75
+ */
76
+ clear(): void {
77
+ clearTimeout(this.id);
78
+ this.id = undefined;
79
+ }
80
+
81
+ public static expires(durationMS: number): Timeout {
82
+ return new Timeout(undefined, durationMS);
83
+ }
84
+
85
+ static is(timeout: unknown): timeout is Timeout {
86
+ return (
87
+ typeof timeout === 'object' &&
88
+ timeout != null &&
89
+ Symbol.toStringTag in timeout &&
90
+ timeout[Symbol.toStringTag] === 'MongoDBTimeout' &&
91
+ 'then' in timeout &&
92
+ // eslint-disable-next-line github/no-then
93
+ typeof timeout.then === 'function'
94
+ );
95
+ }
96
+ }