mongodb 7.1.1 → 7.2.0

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 (102) hide show
  1. package/README.md +11 -0
  2. package/lib/bson.js +26 -5
  3. package/lib/bson.js.map +1 -1
  4. package/lib/change_stream.js +4 -0
  5. package/lib/change_stream.js.map +1 -1
  6. package/lib/client-side-encryption/auto_encrypter.js +19 -10
  7. package/lib/client-side-encryption/auto_encrypter.js.map +1 -1
  8. package/lib/client-side-encryption/client_encryption.js +1 -3
  9. package/lib/client-side-encryption/client_encryption.js.map +1 -1
  10. package/lib/cmap/auth/aws4.js +4 -4
  11. package/lib/cmap/auth/aws4.js.map +1 -1
  12. package/lib/cmap/auth/gssapi.js +3 -6
  13. package/lib/cmap/auth/gssapi.js.map +1 -1
  14. package/lib/cmap/auth/mongodb_aws.js +3 -2
  15. package/lib/cmap/auth/mongodb_aws.js.map +1 -1
  16. package/lib/cmap/auth/mongodb_oidc/azure_machine_workflow.js +3 -3
  17. package/lib/cmap/auth/mongodb_oidc/azure_machine_workflow.js.map +1 -1
  18. package/lib/cmap/auth/mongodb_oidc/gcp_machine_workflow.js +3 -3
  19. package/lib/cmap/auth/mongodb_oidc/gcp_machine_workflow.js.map +1 -1
  20. package/lib/cmap/auth/mongodb_oidc/k8s_machine_workflow.js +3 -3
  21. package/lib/cmap/auth/mongodb_oidc/k8s_machine_workflow.js.map +1 -1
  22. package/lib/cmap/auth/mongodb_oidc/token_machine_workflow.js +3 -3
  23. package/lib/cmap/auth/mongodb_oidc/token_machine_workflow.js.map +1 -1
  24. package/lib/cmap/auth/mongodb_oidc.js +4 -4
  25. package/lib/cmap/auth/mongodb_oidc.js.map +1 -1
  26. package/lib/cmap/auth/plain.js +1 -1
  27. package/lib/cmap/auth/plain.js.map +1 -1
  28. package/lib/cmap/auth/scram.js +53 -40
  29. package/lib/cmap/auth/scram.js.map +1 -1
  30. package/lib/cmap/commands.js +46 -39
  31. package/lib/cmap/commands.js.map +1 -1
  32. package/lib/cmap/connect.js +1 -0
  33. package/lib/cmap/connect.js.map +1 -1
  34. package/lib/cmap/connection.js +5 -2
  35. package/lib/cmap/connection.js.map +1 -1
  36. package/lib/cmap/handshake/client_metadata.js +3 -4
  37. package/lib/cmap/handshake/client_metadata.js.map +1 -1
  38. package/lib/cmap/wire_protocol/compression.js +8 -7
  39. package/lib/cmap/wire_protocol/compression.js.map +1 -1
  40. package/lib/cmap/wire_protocol/on_data.js.map +1 -1
  41. package/lib/cmap/wire_protocol/on_demand/document.js +9 -9
  42. package/lib/cmap/wire_protocol/on_demand/document.js.map +1 -1
  43. package/lib/connection_string.js +21 -5
  44. package/lib/connection_string.js.map +1 -1
  45. package/lib/gridfs/download.js +2 -1
  46. package/lib/gridfs/download.js.map +1 -1
  47. package/lib/gridfs/upload.js +7 -7
  48. package/lib/gridfs/upload.js.map +1 -1
  49. package/lib/mongo_client.js.map +1 -1
  50. package/lib/operations/execute_operation.js +114 -41
  51. package/lib/operations/execute_operation.js.map +1 -1
  52. package/lib/operations/operation.js +1 -0
  53. package/lib/operations/operation.js.map +1 -1
  54. package/lib/runtime_adapters.js +32 -0
  55. package/lib/runtime_adapters.js.map +1 -0
  56. package/lib/sdam/srv_polling.js +1 -1
  57. package/lib/sdam/srv_polling.js.map +1 -1
  58. package/lib/sdam/topology.js +4 -2
  59. package/lib/sdam/topology.js.map +1 -1
  60. package/lib/sessions.js +124 -79
  61. package/lib/sessions.js.map +1 -1
  62. package/lib/utils.js +10 -33
  63. package/lib/utils.js.map +1 -1
  64. package/mongodb.d.ts +45 -2
  65. package/package.json +30 -21
  66. package/src/bson.ts +28 -5
  67. package/src/change_stream.ts +5 -0
  68. package/src/client-side-encryption/auto_encrypter.ts +17 -11
  69. package/src/client-side-encryption/client_encryption.ts +1 -3
  70. package/src/cmap/auth/auth_provider.ts +1 -1
  71. package/src/cmap/auth/aws4.ts +5 -5
  72. package/src/cmap/auth/gssapi.ts +9 -6
  73. package/src/cmap/auth/mongodb_aws.ts +2 -2
  74. package/src/cmap/auth/mongodb_oidc/azure_machine_workflow.ts +1 -1
  75. package/src/cmap/auth/mongodb_oidc/gcp_machine_workflow.ts +1 -1
  76. package/src/cmap/auth/mongodb_oidc/k8s_machine_workflow.ts +1 -1
  77. package/src/cmap/auth/mongodb_oidc/token_machine_workflow.ts +1 -1
  78. package/src/cmap/auth/mongodb_oidc.ts +4 -4
  79. package/src/cmap/auth/plain.ts +2 -2
  80. package/src/cmap/auth/scram.ts +82 -55
  81. package/src/cmap/commands.ts +70 -51
  82. package/src/cmap/connect.ts +2 -0
  83. package/src/cmap/connection.ts +11 -4
  84. package/src/cmap/handshake/client_metadata.ts +6 -6
  85. package/src/cmap/wire_protocol/compression.ts +18 -14
  86. package/src/cmap/wire_protocol/on_data.ts +5 -5
  87. package/src/cmap/wire_protocol/on_demand/document.ts +12 -14
  88. package/src/connection_string.ts +26 -8
  89. package/src/deps.ts +4 -4
  90. package/src/gridfs/download.ts +2 -2
  91. package/src/gridfs/upload.ts +13 -12
  92. package/src/index.ts +1 -0
  93. package/src/mongo_client.ts +24 -0
  94. package/src/operations/client_bulk_write/command_builder.ts +1 -1
  95. package/src/operations/execute_operation.ts +146 -45
  96. package/src/operations/operation.ts +8 -0
  97. package/src/runtime_adapters.ts +64 -0
  98. package/src/sdam/srv_polling.ts +1 -1
  99. package/src/sdam/topology.ts +10 -7
  100. package/src/sessions.ts +140 -96
  101. package/src/utils.ts +21 -40
  102. package/tsconfig.json +1 -1
package/src/sessions.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { setTimeout } from 'timers/promises';
2
2
 
3
- import { Binary, type Document, Long, type Timestamp } from './bson';
3
+ import { Binary, ByteUtils, type Document, Long, type Timestamp } from './bson';
4
4
  import type { CommandOptions, Connection } from './cmap/connection';
5
5
  import { ConnectionPoolMetrics } from './cmap/metrics';
6
6
  import { type MongoDBResponse } from './cmap/wire_protocol/responses';
@@ -17,6 +17,7 @@ import {
17
17
  MongoErrorLabel,
18
18
  MongoExpiredSessionError,
19
19
  MongoInvalidArgumentError,
20
+ MongoOperationTimeoutError,
20
21
  MongoRuntimeError,
21
22
  MongoServerError,
22
23
  MongoTransactionError,
@@ -37,7 +38,6 @@ import {
37
38
  TxnState
38
39
  } from './transactions';
39
40
  import {
40
- ByteUtils,
41
41
  calculateDurationInMs,
42
42
  commandSupportsReadConcern,
43
43
  isPromiseLike,
@@ -468,7 +468,11 @@ export class ClientSession
468
468
  } else {
469
469
  const wcKeys = Object.keys(wc);
470
470
  if (wcKeys.length > 2 || (!wcKeys.includes('wtimeoutMS') && !wcKeys.includes('wTimeoutMS')))
471
- // if the write concern was specified with wTimeoutMS, then we set both wtimeoutMS and wTimeoutMS, guaranteeing at least two keys, so if we have more than two keys, then we can automatically assume that we should add the write concern to the command. If it has 2 or fewer keys, we need to check that those keys aren't the wtimeoutMS or wTimeoutMS options before we add the write concern to the command
471
+ // if the write concern was specified with wTimeoutMS, then we set both wtimeoutMS
472
+ // and wTimeoutMS, guaranteeing at least two keys, so if we have more than two keys,
473
+ // then we can automatically assume that we should add the write concern to the command.
474
+ // If it has 2 or fewer keys, we need to check that those keys aren't the wtimeoutMS
475
+ // or wTimeoutMS options before we add the write concern to the command
472
476
  WriteConcern.apply(command, { ...wc, wtimeoutMS: undefined });
473
477
  }
474
478
  }
@@ -494,6 +498,7 @@ export class ClientSession
494
498
  readPreference: ReadPreference.primary,
495
499
  bypassPinningCheck: true
496
500
  });
501
+ operation.maxAttempts = this.clientOptions.maxAdaptiveRetries + 1;
497
502
 
498
503
  const timeoutContext =
499
504
  this.timeoutContext ??
@@ -511,6 +516,12 @@ export class ClientSession
511
516
  return;
512
517
  } catch (firstCommitError) {
513
518
  this.commitAttempted = true;
519
+
520
+ const remainingAttempts = this.clientOptions.maxAdaptiveRetries + 1 - operation.attemptsMade;
521
+ if (remainingAttempts <= 0) {
522
+ throw firstCommitError;
523
+ }
524
+
514
525
  if (firstCommitError instanceof MongoError && isRetryableWriteError(firstCommitError)) {
515
526
  // SPEC-1185: apply majority write concern when retrying commitTransaction
516
527
  WriteConcern.apply(command, { wtimeoutMS: 10000, ...wc, w: 'majority' });
@@ -518,15 +529,13 @@ export class ClientSession
518
529
  this.unpin({ force: true });
519
530
 
520
531
  try {
521
- await executeOperation(
522
- this.client,
523
- new RunCommandOperation(new MongoDBNamespace('admin'), command, {
524
- session: this,
525
- readPreference: ReadPreference.primary,
526
- bypassPinningCheck: true
527
- }),
528
- timeoutContext
529
- );
532
+ const op = new RunCommandOperation(new MongoDBNamespace('admin'), command, {
533
+ session: this,
534
+ readPreference: ReadPreference.primary,
535
+ bypassPinningCheck: true
536
+ });
537
+ op.maxAttempts = remainingAttempts;
538
+ await executeOperation(this.client, op, timeoutContext);
530
539
  return;
531
540
  } catch (retryCommitError) {
532
541
  // If the retry failed, we process that error instead of the original
@@ -716,7 +725,7 @@ export class ClientSession
716
725
  timeoutMS?: number;
717
726
  }
718
727
  ): Promise<T> {
719
- const MAX_TIMEOUT = 120000;
728
+ const MAX_TIMEOUT = 120_000;
720
729
 
721
730
  const timeoutMS = options?.timeoutMS ?? this.timeoutMS ?? null;
722
731
  this.timeoutContext =
@@ -728,10 +737,27 @@ export class ClientSession
728
737
  })
729
738
  : null;
730
739
 
731
- // 1. Record the current monotonic time, which will be used to enforce the 120-second timeout before later retry attempts.
732
- const startTime = this.timeoutContext?.csotEnabled() // This is strictly to appease TS. We must narrow the context to a CSOT context before accessing `.start`.
733
- ? this.timeoutContext.start
734
- : processTimeMS();
740
+ // 1. Define the following:
741
+ // 1.1 Record the current monotonic time, which will be used to enforce the 120-second / CSOT timeout before later retry attempts.
742
+ // 1.2 Set `transactionAttempt` to `0`.
743
+ // 1.3 Set `TIMEOUT_MS` to be `timeoutMS` if given, otherwise MAX_TIMEOUT (120-seconds).
744
+
745
+ // Timeout Error propagation
746
+ // When the previously encountered error needs to be propagated because there is no more time for another attempt,
747
+ // and it is not already a timeout error, then:
748
+ // - A timeout error MUST be propagated instead. It MUST expose the previously encountered error as specified in
749
+ // the "Errors" section of the CSOT specification.
750
+ // - If exposing the previously encountered error from a timeout error is impossible in a driver, then the driver
751
+ // is exempt from the requirement and MUST propagate the previously encountered error as is. The timeout error
752
+ // MUST copy all error labels from the previously encountered error.
753
+
754
+ // The spec describes timeout checks as "elapsed time < TIMEOUT_MS" (where elapsed = now - start).
755
+ // We precompute `deadline = now + remainingTimeMS` so each check becomes simply `now < deadline`.
756
+ const csotEnabled = !!this.timeoutContext?.csotEnabled();
757
+ const remainingTimeMS = this.timeoutContext?.csotEnabled()
758
+ ? this.timeoutContext.remainingTimeMS
759
+ : MAX_TIMEOUT;
760
+ const deadline = processTimeMS() + remainingTimeMS;
735
761
 
736
762
  let committed = false;
737
763
  let result: T;
@@ -740,20 +766,20 @@ export class ClientSession
740
766
 
741
767
  try {
742
768
  retryTransaction: for (
743
- // 2. Set `transactionAttempt` to `0`.
744
769
  let transactionAttempt = 0, isRetry = false;
745
770
  !committed;
746
771
  ++transactionAttempt, isRetry = transactionAttempt > 0
747
772
  ) {
748
773
  // 2. If `transactionAttempt` > 0:
749
774
  if (isRetry) {
750
- // 2.i If elapsed time + `backoffMS` > `TIMEOUT_MS`, then raise the previously encountered error. If the elapsed time of
751
- // `withTransaction` is less than TIMEOUT_MS, calculate the backoffMS to be
752
- // `jitter * min(BACKOFF_INITIAL * 1.5 ** (transactionAttempt - 1), BACKOFF_MAX)`. sleep for `backoffMS`.
753
- // 2.i.i jitter is a random float between \[0, 1)
754
- // 2.i.ii `transactionAttempt` is the variable defined in step 1.
755
- // 2.i.iii `BACKOFF_INITIAL` is 5ms
756
- // 2.i.iv `BACKOFF_MAX` is 500ms
775
+ // 2.1 Calculate backoffMS to be jitter * min(BACKOFF_INITIAL * 1.5 ** (transactionAttempt - 1), BACKOFF_MAX).
776
+ // If elapsed time + backoffMS > TIMEOUT_MS, then propagate the previously encountered error to the caller of
777
+ // withTransaction as per timeout error propagation and return immediately. Otherwise, sleep for backoffMS.
778
+ // 2.1.1 jitter is a random float between [0, 1), optionally including 1, depending on what is most natural
779
+ // for the given driver language.
780
+ // 2.1.2 transactionAttempt is the variable defined in step 1.
781
+ // 2.1.3 BACKOFF_INITIAL is 5ms
782
+ // 2.1.4 BACKOFF_MAX is 500ms
757
783
  const BACKOFF_INITIAL_MS = 5;
758
784
  const BACKOFF_MAX_MS = 500;
759
785
  const BACKOFF_GROWTH = 1.5;
@@ -765,30 +791,30 @@ export class ClientSession
765
791
  BACKOFF_MAX_MS
766
792
  );
767
793
 
768
- const willExceedTransactionDeadline =
769
- (this.timeoutContext?.csotEnabled() &&
770
- backoffMS > this.timeoutContext.remainingTimeMS) ||
771
- processTimeMS() + backoffMS > startTime + MAX_TIMEOUT;
772
-
773
- if (willExceedTransactionDeadline) {
774
- throw (
794
+ if (processTimeMS() + backoffMS >= deadline) {
795
+ throw makeTimeoutError(
775
796
  lastError ??
776
- new MongoRuntimeError(
777
- `Transaction retry did not record an error: should never occur. Please file a bug.`
778
- )
797
+ new MongoRuntimeError(
798
+ `Transaction retry did not record an error: should never occur. Please file a bug.`
799
+ ),
800
+ csotEnabled
779
801
  );
780
802
  }
781
803
 
782
804
  await setTimeout(backoffMS);
783
805
  }
784
806
 
785
- // 3. Invoke startTransaction on the session
786
- // 4. If `startTransaction` reported an error, propagate that error to the caller of `withTransaction` and return immediately.
787
- this.startTransaction(options); // may throw on error
807
+ // 3. Invoke startTransaction on the session and increment transactionAttempt. If TransactionOptions were
808
+ // specified in the call to withTransaction, those MUST be used for startTransaction. Note that
809
+ // ClientSession.defaultTransactionOptions will be used in the absence of any explicit TransactionOptions.
810
+ // 4. If startTransaction reported an error, propagate that error to the caller of withTransaction as is and
811
+ // return immediately.
812
+ this.startTransaction(options);
788
813
 
789
814
  try {
790
- // 5. Invoke the callback.
791
- // 6. Control returns to withTransaction. (continued below)
815
+ // 5. Invoke the callback. Drivers MUST ensure that the ClientSession can be accessed within the callback
816
+ // (e.g. pass ClientSession as the first parameter, rely on lexical scoping). Drivers MAY pass additional
817
+ // parameters as needed (e.g. user data solicited by withTransaction).
792
818
  const promise = fn(this);
793
819
  if (!isPromiseLike(promise)) {
794
820
  throw new MongoInvalidArgumentError(
@@ -796,21 +822,21 @@ export class ClientSession
796
822
  );
797
823
  }
798
824
 
825
+ // 6. Control returns to withTransaction. Determine the current state of the ClientSession and whether the
826
+ // callback reported an error (e.g. thrown exception, error output parameter).
799
827
  result = await promise;
800
828
 
801
- // 6. (cont.) Determine the current state of the ClientSession (continued below)
829
+ // 8. If the ClientSession is in the "no transaction", "transaction aborted", or "transaction committed"
830
+ // state, assume the callback intentionally aborted or committed the transaction and return immediately.
802
831
  if (
803
832
  this.transaction.state === TxnState.NO_TRANSACTION ||
804
833
  this.transaction.state === TxnState.TRANSACTION_COMMITTED ||
805
834
  this.transaction.state === TxnState.TRANSACTION_ABORTED
806
835
  ) {
807
- // 8. If the ClientSession is in the "no transaction", "transaction aborted", or "transaction committed" state,
808
- // assume the callback intentionally aborted or committed the transaction and return immediately.
809
836
  return result;
810
837
  }
811
- // 5. (cont.) and whether the callback reported an error
812
- // 7. If the callback reported an error:
813
838
  } catch (fnError) {
839
+ // 7. If the callback reported an error
814
840
  if (!(fnError instanceof MongoError) || fnError instanceof MongoInvalidArgumentError) {
815
841
  // This first preemptive abort regardless of TxnState isn't spec,
816
842
  // and it's unclear whether it's serving a practical purpose, but this logic is OLD
@@ -818,83 +844,70 @@ export class ClientSession
818
844
  throw fnError;
819
845
  }
820
846
 
847
+ lastError = fnError;
848
+
849
+ // 7.1 If the ClientSession is in the "starting transaction" or "transaction in progress"
850
+ // state, invoke abortTransaction on the session.
821
851
  if (
822
852
  this.transaction.state === TxnState.STARTING_TRANSACTION ||
823
853
  this.transaction.state === TxnState.TRANSACTION_IN_PROGRESS
824
854
  ) {
825
- // 7.i If the ClientSession is in the "starting transaction" or "transaction in progress" state,
826
- // invoke abortTransaction on the session
827
855
  await this.abortTransaction();
828
856
  }
829
857
 
830
- if (
831
- fnError.hasErrorLabel(MongoErrorLabel.TransientTransactionError) &&
832
- (this.timeoutContext?.csotEnabled() || processTimeMS() - startTime < MAX_TIMEOUT)
833
- ) {
834
- // 7.ii If the callback's error includes a "TransientTransactionError" label and the elapsed time of `withTransaction`
835
- // is less than 120 seconds, jump back to step two.
836
- lastError = fnError;
858
+ // 7.2 If the callback's error includes a "TransientTransactionError" label, jump back to step two.
859
+ if (fnError.hasErrorLabel(MongoErrorLabel.TransientTransactionError)) {
860
+ if (processTimeMS() >= deadline) {
861
+ throw makeTimeoutError(lastError, csotEnabled);
862
+ }
837
863
  continue retryTransaction;
838
864
  }
839
865
 
840
- // 7.iii If the callback's error includes a "UnknownTransactionCommitResult" label, the callback must have manually committed a transaction,
841
- // propagate the callback's error to the caller of withTransaction and return immediately.
842
- // The 7.iii check is redundant with 6.iv, so we don't write code for it
843
- // 7.iv Otherwise, propagate the callback's error to the caller of withTransaction and return immediately.
866
+ // 7.3 If the callback's error includes a "UnknownTransactionCommitResult" label, the callback must
867
+ // have manually committed a transaction, propagate the callback's error to the caller of withTransaction
868
+ // as is and return immediately.
869
+ // 7.4 Otherwise, propagate the callback's error to the caller of withTransaction as is and return immediately.
844
870
  throw fnError;
845
871
  }
846
872
 
847
873
  retryCommit: while (!committed) {
848
874
  try {
849
- /*
850
- * We will rely on ClientSession.commitTransaction() to
851
- * apply a majority write concern if commitTransaction is
852
- * being retried (see: DRIVERS-601)
853
- */
854
875
  // 9. Invoke commitTransaction on the session.
855
876
  await this.commitTransaction();
856
877
  committed = true;
857
- // 10. If commitTransaction reported an error:
858
878
  } catch (commitError) {
859
- // If CSOT is enabled, we repeatedly retry until timeoutMS expires. This is enforced by providing a
860
- // timeoutContext to each async API, which know how to cancel themselves (i.e., the next retry will
861
- // abort the withTransaction call).
862
- // If CSOT is not enabled, do we still have time remaining or have we timed out?
863
- const hasTimedOut =
864
- !this.timeoutContext?.csotEnabled() && processTimeMS() - startTime >= MAX_TIMEOUT;
865
-
866
- if (!hasTimedOut) {
867
- /*
868
- * Note: a maxTimeMS error will have the MaxTimeMSExpired
869
- * code (50) and can be reported as a top-level error or
870
- * inside writeConcernError, ex.
871
- * { ok:0, code: 50, codeName: 'MaxTimeMSExpired' }
872
- * { ok:1, writeConcernError: { code: 50, codeName: 'MaxTimeMSExpired' } }
873
- */
874
- if (
875
- !isMaxTimeMSExpiredError(commitError) &&
876
- commitError.hasErrorLabel(MongoErrorLabel.UnknownTransactionCommitResult)
877
- ) {
878
- // 10.i If the `commitTransaction` error includes a "UnknownTransactionCommitResult" label and the error is not
879
- // MaxTimeMSExpired and the elapsed time of `withTransaction` is less than 120 seconds, jump back to step eight.
880
- continue retryCommit;
879
+ // 10. If commitTransaction reported an error:
880
+ lastError = commitError;
881
+
882
+ // 10.1 If the commitTransaction error includes a UnknownTransactionCommitResult label and the error is
883
+ // not MaxTimeMSExpired
884
+ if (
885
+ commitError.hasErrorLabel(MongoErrorLabel.UnknownTransactionCommitResult) &&
886
+ !isMaxTimeMSExpiredError(commitError)
887
+ ) {
888
+ // 10.1.1 If the elapsed time of withTransaction exceeded TIMEOUT_MS, propagate the commitTransaction
889
+ // error to the caller of withTransaction as per timeout error propagation and return immediately.
890
+ if (processTimeMS() >= deadline) {
891
+ throw makeTimeoutError(commitError, csotEnabled);
881
892
  }
893
+ // 10.1.2 Otherwise, jump back to step nine. We will trust commitTransaction to apply a majority write
894
+ // concern on retry attempts (see: Majority write concern is used when retrying commitTransaction).
895
+ continue retryCommit;
896
+ }
882
897
 
883
- if (commitError.hasErrorLabel(MongoErrorLabel.TransientTransactionError)) {
884
- // 10.ii If the commitTransaction error includes a "TransientTransactionError" label
885
- // and the elapsed time of withTransaction is less than 120 seconds, jump back to step two.
886
- lastError = commitError;
887
-
888
- continue retryTransaction;
889
- }
898
+ // 10.2 If the commitTransaction error includes a TransientTransactionError label, jump back to step two.
899
+ if (commitError.hasErrorLabel(MongoErrorLabel.TransientTransactionError)) {
900
+ continue retryTransaction;
890
901
  }
891
902
 
892
- // 10.iii Otherwise, propagate the commitTransaction error to the caller of withTransaction and return immediately.
903
+ // 10.3 Otherwise, propagate the commitTransaction error to the caller of withTransaction as is and return
904
+ // immediately.
893
905
  throw commitError;
894
906
  }
895
907
  }
896
908
  }
897
909
 
910
+ // 11. The transaction was committed successfully. Return immediately.
898
911
  // @ts-expect-error Result is always defined if we reach here, the for-loop above convinces TS it is not.
899
912
  return result;
900
913
  } finally {
@@ -903,6 +916,25 @@ export class ClientSession
903
916
  }
904
917
  }
905
918
 
919
+ function makeTimeoutError(cause: Error, csotEnabled: boolean): Error {
920
+ // Async APIs know how to cancel themselves and might return CSOT error
921
+ if (cause instanceof MongoOperationTimeoutError) {
922
+ return cause;
923
+ }
924
+ if (csotEnabled) {
925
+ const timeoutError = new MongoOperationTimeoutError('Timed out during withTransaction', {
926
+ cause
927
+ });
928
+ if (cause instanceof MongoError) {
929
+ for (const label of cause.errorLabels) {
930
+ timeoutError.addErrorLabel(label);
931
+ }
932
+ }
933
+ return timeoutError;
934
+ }
935
+ return cause;
936
+ }
937
+
906
938
  const NON_DETERMINISTIC_WRITE_CONCERN_ERRORS = new Set([
907
939
  'CannotSatisfyWriteConcern',
908
940
  'UnknownReplWriteConcern',
@@ -1018,7 +1050,7 @@ export class ServerSession {
1018
1050
  /** @internal */
1019
1051
  constructor(cloned?: ServerSession | null) {
1020
1052
  if (cloned != null) {
1021
- const idBytes = Buffer.allocUnsafe(16);
1053
+ const idBytes = ByteUtils.allocateUnsafe(16);
1022
1054
  idBytes.set(cloned.id.id.buffer);
1023
1055
  this.id = { id: new Binary(idBytes, cloned.id.id.sub_type) };
1024
1056
  this.lastUse = cloned.lastUse;
@@ -1203,7 +1235,6 @@ export function applySession(
1203
1235
  command.autocommit = false;
1204
1236
 
1205
1237
  if (session.transaction.state === TxnState.STARTING_TRANSACTION) {
1206
- session.transaction.transition(TxnState.TRANSACTION_IN_PROGRESS);
1207
1238
  command.startTransaction = true;
1208
1239
 
1209
1240
  const readConcern =
@@ -1241,4 +1272,17 @@ export function updateSessionFromResponse(session: ClientSession, document: Mong
1241
1272
  session.snapshotTime = atClusterTime;
1242
1273
  }
1243
1274
  }
1275
+
1276
+ if (session.transaction.state === TxnState.STARTING_TRANSACTION) {
1277
+ if (document.ok === 1) {
1278
+ session.transaction.transition(TxnState.TRANSACTION_IN_PROGRESS);
1279
+ } else {
1280
+ const error = new MongoServerError(document.toObject());
1281
+ const isRetryableError = error.hasErrorLabel(MongoErrorLabel.RetryableError);
1282
+
1283
+ if (!isRetryableError) {
1284
+ session.transaction.transition(TxnState.TRANSACTION_IN_PROGRESS);
1285
+ }
1286
+ }
1287
+ }
1244
1288
  }
package/src/utils.ts CHANGED
@@ -1,4 +1,3 @@
1
- import * as crypto from 'crypto';
2
1
  import type { SrvRecord } from 'dns';
3
2
  import { type EventEmitter } from 'events';
4
3
  import { promises as fs } from 'fs';
@@ -6,7 +5,14 @@ import * as http from 'http';
6
5
  import * as process from 'process';
7
6
  import { clearTimeout, setTimeout } from 'timers';
8
7
 
9
- import { deserialize, type Document, ObjectId, resolveBSONOptions } from './bson';
8
+ import {
9
+ ByteUtils,
10
+ deserialize,
11
+ type Document,
12
+ NumberUtils,
13
+ ObjectId,
14
+ resolveBSONOptions
15
+ } from './bson';
10
16
  import type { Connection } from './cmap/connection';
11
17
  import { MAX_SUPPORTED_WIRE_VERSION } from './cmap/wire_protocol/constants';
12
18
  import type { Collection } from './collection';
@@ -44,26 +50,6 @@ export type Callback<T = any> = (error?: AnyError, result?: T) => void;
44
50
 
45
51
  export type AnyOptions = Document;
46
52
 
47
- export const ByteUtils = {
48
- toLocalBufferType(this: void, buffer: Buffer | Uint8Array): Buffer {
49
- return Buffer.isBuffer(buffer)
50
- ? buffer
51
- : Buffer.from(buffer.buffer, buffer.byteOffset, buffer.byteLength);
52
- },
53
-
54
- equals(this: void, seqA: Uint8Array, seqB: Uint8Array) {
55
- return ByteUtils.toLocalBufferType(seqA).equals(seqB);
56
- },
57
-
58
- compare(this: void, seqA: Uint8Array, seqB: Uint8Array) {
59
- return ByteUtils.toLocalBufferType(seqA).compare(seqB);
60
- },
61
-
62
- toBase64(this: void, uint8array: Uint8Array) {
63
- return ByteUtils.toLocalBufferType(uint8array).toString('base64');
64
- }
65
- };
66
-
67
53
  /**
68
54
  * Returns true if value is a Uint8Array or a Buffer
69
55
  * @param value - any value that may be a Uint8Array
@@ -332,8 +318,8 @@ export function* makeCounter(seed = 0): Generator<number> {
332
318
  * Synchronously Generate a UUIDv4
333
319
  * @internal
334
320
  */
335
- export function uuidV4(): Buffer {
336
- const result = crypto.randomBytes(16);
321
+ export function uuidV4(): Uint8Array {
322
+ const result = crypto.getRandomValues(new Uint8Array(16));
337
323
  result[6] = (result[6] & 0x0f) | 0x40;
338
324
  result[8] = (result[8] & 0x3f) | 0x80;
339
325
  return result;
@@ -807,7 +793,7 @@ export class List<T = unknown> {
807
793
  * @internal
808
794
  */
809
795
  export class BufferPool {
810
- private buffers: List<Buffer>;
796
+ private buffers: List<Uint8Array>;
811
797
  private totalByteLength: number;
812
798
 
813
799
  constructor() {
@@ -820,7 +806,7 @@ export class BufferPool {
820
806
  }
821
807
 
822
808
  /** Adds a buffer to the internal buffer pool list */
823
- append(buffer: Buffer): void {
809
+ append(buffer: Uint8Array): void {
824
810
  this.buffers.push(buffer);
825
811
  this.totalByteLength += buffer.length;
826
812
  }
@@ -835,13 +821,13 @@ export class BufferPool {
835
821
  }
836
822
  const firstBuffer = this.buffers.first();
837
823
  if (firstBuffer != null && firstBuffer.byteLength >= 4) {
838
- return firstBuffer.readInt32LE(0);
824
+ return NumberUtils.getInt32LE(firstBuffer, 0);
839
825
  }
840
826
 
841
827
  // Unlikely case: an int32 is split across buffers.
842
828
  // Use read and put the returned buffer back on top
843
829
  const top4Bytes = this.read(4);
844
- const value = top4Bytes.readInt32LE(0);
830
+ const value = NumberUtils.getInt32LE(top4Bytes, 0);
845
831
 
846
832
  // Put it back.
847
833
  this.totalByteLength += 4;
@@ -851,19 +837,19 @@ export class BufferPool {
851
837
  }
852
838
 
853
839
  /** Reads the requested number of bytes, optionally consuming them */
854
- read(size: number): Buffer {
840
+ read(size: number): Uint8Array {
855
841
  if (typeof size !== 'number' || size < 0) {
856
842
  throw new MongoInvalidArgumentError('Argument "size" must be a non-negative number');
857
843
  }
858
844
 
859
845
  // oversized request returns empty buffer
860
846
  if (size > this.totalByteLength) {
861
- return Buffer.alloc(0);
847
+ return ByteUtils.allocate(0);
862
848
  }
863
849
 
864
850
  // We know we have enough, we just don't know how it is spread across chunks
865
851
  // TODO(NODE-4732): alloc API should change based on raw option
866
- const result = Buffer.allocUnsafe(size);
852
+ const result = ByteUtils.allocateUnsafe(size);
867
853
 
868
854
  for (let bytesRead = 0; bytesRead < size; ) {
869
855
  const buffer = this.buffers.shift();
@@ -1253,13 +1239,8 @@ export function squashError(_error: unknown) {
1253
1239
  return;
1254
1240
  }
1255
1241
 
1256
- export const randomBytes = (size: number) => {
1257
- return new Promise<Buffer>((resolve, reject) => {
1258
- crypto.randomBytes(size, (error: Error | null, buf: Buffer) => {
1259
- if (error) return reject(error);
1260
- resolve(buf);
1261
- });
1262
- });
1242
+ export const randomBytes = (size: number): Promise<Uint8Array> => {
1243
+ return Promise.resolve(crypto.getRandomValues(new Uint8Array(size)));
1263
1244
  };
1264
1245
 
1265
1246
  /**
@@ -1345,10 +1326,10 @@ export function decorateDecryptionResult(
1345
1326
  ): void {
1346
1327
  if (isTopLevelDecorateCall) {
1347
1328
  // The original value could have been either a JS object or a BSON buffer
1348
- if (Buffer.isBuffer(original)) {
1329
+ if (ByteUtils.isUint8Array(original)) {
1349
1330
  original = deserialize(original);
1350
1331
  }
1351
- if (Buffer.isBuffer(decrypted)) {
1332
+ if (ByteUtils.isUint8Array(decrypted)) {
1352
1333
  throw new MongoRuntimeError('Expected result of decryption to be deserialized BSON object');
1353
1334
  }
1354
1335
  }
package/tsconfig.json CHANGED
@@ -43,4 +43,4 @@
43
43
  "include": [
44
44
  "src/**/*"
45
45
  ]
46
- }
46
+ }