mongodb 4.1.3 → 4.2.2

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 (168) hide show
  1. package/README.md +15 -14
  2. package/lib/admin.js +5 -5
  3. package/lib/admin.js.map +1 -1
  4. package/lib/bson.js.map +1 -1
  5. package/lib/bulk/common.js +54 -56
  6. package/lib/bulk/common.js.map +1 -1
  7. package/lib/change_stream.js +13 -14
  8. package/lib/change_stream.js.map +1 -1
  9. package/lib/cmap/auth/gssapi.js +1 -1
  10. package/lib/cmap/auth/gssapi.js.map +1 -1
  11. package/lib/cmap/auth/mongocr.js +2 -2
  12. package/lib/cmap/auth/mongocr.js.map +1 -1
  13. package/lib/cmap/auth/mongodb_aws.js +3 -3
  14. package/lib/cmap/auth/mongodb_aws.js.map +1 -1
  15. package/lib/cmap/auth/plain.js +1 -1
  16. package/lib/cmap/auth/plain.js.map +1 -1
  17. package/lib/cmap/auth/scram.js +5 -5
  18. package/lib/cmap/auth/scram.js.map +1 -1
  19. package/lib/cmap/auth/x509.js +1 -1
  20. package/lib/cmap/auth/x509.js.map +1 -1
  21. package/lib/cmap/command_monitoring_events.js +15 -15
  22. package/lib/cmap/command_monitoring_events.js.map +1 -1
  23. package/lib/cmap/commands.js +10 -7
  24. package/lib/cmap/commands.js.map +1 -1
  25. package/lib/cmap/connect.js +2 -2
  26. package/lib/cmap/connect.js.map +1 -1
  27. package/lib/cmap/connection.js +15 -15
  28. package/lib/cmap/connection.js.map +1 -1
  29. package/lib/cmap/connection_pool.js +3 -3
  30. package/lib/cmap/connection_pool.js.map +1 -1
  31. package/lib/cmap/message_stream.js +2 -2
  32. package/lib/cmap/message_stream.js.map +1 -1
  33. package/lib/cmap/stream_description.js +4 -4
  34. package/lib/cmap/stream_description.js.map +1 -1
  35. package/lib/cmap/wire_protocol/constants.js +4 -4
  36. package/lib/collection.js +42 -41
  37. package/lib/collection.js.map +1 -1
  38. package/lib/connection_string.js +73 -45
  39. package/lib/connection_string.js.map +1 -1
  40. package/lib/cursor/abstract_cursor.js +16 -13
  41. package/lib/cursor/abstract_cursor.js.map +1 -1
  42. package/lib/cursor/aggregation_cursor.js +14 -14
  43. package/lib/cursor/aggregation_cursor.js.map +1 -1
  44. package/lib/cursor/find_cursor.js +23 -23
  45. package/lib/cursor/find_cursor.js.map +1 -1
  46. package/lib/db.js +20 -20
  47. package/lib/db.js.map +1 -1
  48. package/lib/deps.js +1 -1
  49. package/lib/deps.js.map +1 -1
  50. package/lib/error.js +47 -24
  51. package/lib/error.js.map +1 -1
  52. package/lib/gridfs/index.js +3 -3
  53. package/lib/gridfs/index.js.map +1 -1
  54. package/lib/gridfs/upload.js +1 -1
  55. package/lib/gridfs/upload.js.map +1 -1
  56. package/lib/logger.js +5 -5
  57. package/lib/logger.js.map +1 -1
  58. package/lib/mongo_client.js +7 -7
  59. package/lib/mongo_client.js.map +1 -1
  60. package/lib/mongo_types.js.map +1 -1
  61. package/lib/operations/add_user.js +3 -3
  62. package/lib/operations/add_user.js.map +1 -1
  63. package/lib/operations/aggregate.js +3 -4
  64. package/lib/operations/aggregate.js.map +1 -1
  65. package/lib/operations/bulk_write.js +1 -1
  66. package/lib/operations/bulk_write.js.map +1 -1
  67. package/lib/operations/command.js +7 -3
  68. package/lib/operations/command.js.map +1 -1
  69. package/lib/operations/common_functions.js +1 -1
  70. package/lib/operations/common_functions.js.map +1 -1
  71. package/lib/operations/connect.js +1 -1
  72. package/lib/operations/connect.js.map +1 -1
  73. package/lib/operations/count.js +1 -1
  74. package/lib/operations/count.js.map +1 -1
  75. package/lib/operations/create_collection.js +1 -1
  76. package/lib/operations/create_collection.js.map +1 -1
  77. package/lib/operations/delete.js +6 -6
  78. package/lib/operations/delete.js.map +1 -1
  79. package/lib/operations/distinct.js +4 -4
  80. package/lib/operations/distinct.js.map +1 -1
  81. package/lib/operations/drop.js +2 -2
  82. package/lib/operations/drop.js.map +1 -1
  83. package/lib/operations/estimated_document_count.js +2 -2
  84. package/lib/operations/estimated_document_count.js.map +1 -1
  85. package/lib/operations/execute_operation.js +24 -7
  86. package/lib/operations/execute_operation.js.map +1 -1
  87. package/lib/operations/find.js +8 -8
  88. package/lib/operations/find.js.map +1 -1
  89. package/lib/operations/find_and_modify.js +7 -7
  90. package/lib/operations/find_and_modify.js.map +1 -1
  91. package/lib/operations/get_more.js +28 -0
  92. package/lib/operations/get_more.js.map +1 -0
  93. package/lib/operations/indexes.js +14 -14
  94. package/lib/operations/indexes.js.map +1 -1
  95. package/lib/operations/insert.js +5 -5
  96. package/lib/operations/insert.js.map +1 -1
  97. package/lib/operations/list_collections.js +12 -7
  98. package/lib/operations/list_collections.js.map +1 -1
  99. package/lib/operations/list_databases.js +1 -1
  100. package/lib/operations/list_databases.js.map +1 -1
  101. package/lib/operations/map_reduce.js +6 -6
  102. package/lib/operations/map_reduce.js.map +1 -1
  103. package/lib/operations/operation.js +4 -2
  104. package/lib/operations/operation.js.map +1 -1
  105. package/lib/operations/remove_user.js +1 -1
  106. package/lib/operations/remove_user.js.map +1 -1
  107. package/lib/operations/rename.js +2 -2
  108. package/lib/operations/rename.js.map +1 -1
  109. package/lib/operations/set_profiling_level.js +1 -1
  110. package/lib/operations/set_profiling_level.js.map +1 -1
  111. package/lib/operations/stats.js +2 -2
  112. package/lib/operations/stats.js.map +1 -1
  113. package/lib/operations/update.js +12 -12
  114. package/lib/operations/update.js.map +1 -1
  115. package/lib/read_preference.js +4 -1
  116. package/lib/read_preference.js.map +1 -1
  117. package/lib/sdam/monitor.js +14 -14
  118. package/lib/sdam/monitor.js.map +1 -1
  119. package/lib/sdam/server.js +19 -12
  120. package/lib/sdam/server.js.map +1 -1
  121. package/lib/sdam/server_description.js +3 -3
  122. package/lib/sdam/server_description.js.map +1 -1
  123. package/lib/sdam/server_selection.js +36 -1
  124. package/lib/sdam/server_selection.js.map +1 -1
  125. package/lib/sdam/srv_polling.js +9 -9
  126. package/lib/sdam/srv_polling.js.map +1 -1
  127. package/lib/sdam/topology.js +29 -21
  128. package/lib/sdam/topology.js.map +1 -1
  129. package/lib/sdam/topology_description.js +34 -12
  130. package/lib/sdam/topology_description.js.map +1 -1
  131. package/lib/sessions.js +19 -19
  132. package/lib/sessions.js.map +1 -1
  133. package/lib/transactions.js.map +1 -1
  134. package/lib/utils.js +53 -46
  135. package/lib/utils.js.map +1 -1
  136. package/mongodb.d.ts +60 -37
  137. package/mongodb.ts34.d.ts +61 -37
  138. package/package.json +26 -30
  139. package/src/bson.ts +1 -0
  140. package/src/bulk/common.ts +50 -37
  141. package/src/change_stream.ts +10 -8
  142. package/src/cmap/commands.ts +11 -8
  143. package/src/cmap/connection.ts +1 -0
  144. package/src/cmap/stream_description.ts +3 -3
  145. package/src/cmap/wire_protocol/constants.ts +4 -4
  146. package/src/collection.ts +40 -25
  147. package/src/connection_string.ts +75 -36
  148. package/src/cursor/abstract_cursor.ts +9 -11
  149. package/src/db.ts +5 -6
  150. package/src/error.ts +51 -23
  151. package/src/mongo_client.ts +12 -0
  152. package/src/mongo_types.ts +11 -14
  153. package/src/operations/aggregate.ts +1 -2
  154. package/src/operations/command.ts +5 -0
  155. package/src/operations/create_collection.ts +1 -1
  156. package/src/operations/execute_operation.ts +22 -2
  157. package/src/operations/get_more.ts +49 -0
  158. package/src/operations/list_collections.ts +12 -4
  159. package/src/operations/operation.ts +5 -1
  160. package/src/read_preference.ts +4 -1
  161. package/src/sdam/server.ts +8 -0
  162. package/src/sdam/server_selection.ts +43 -0
  163. package/src/sdam/srv_polling.ts +12 -11
  164. package/src/sdam/topology.ts +27 -10
  165. package/src/sdam/topology_description.ts +35 -11
  166. package/src/sessions.ts +2 -1
  167. package/src/transactions.ts +2 -2
  168. package/src/utils.ts +67 -56
@@ -15,6 +15,8 @@ const LIST_COLLECTIONS_WIRE_VERSION = 3;
15
15
  export interface ListCollectionsOptions extends CommandOperationOptions {
16
16
  /** Since 4.0: If true, will only return the collection name in the response, and will omit additional info */
17
17
  nameOnly?: boolean;
18
+ /** Since 4.0: If true and nameOnly is true, allows a user without the required privilege (i.e. listCollections action on the database) to run the command when access control is enforced. */
19
+ authorizedCollections?: boolean;
18
20
  /** The batchSize for the returned command cursor or if pre 2.8 the systems batch collection */
19
21
  batchSize?: number;
20
22
  }
@@ -25,6 +27,7 @@ export class ListCollectionsOperation extends CommandOperation<string[]> {
25
27
  db: Db;
26
28
  filter: Document;
27
29
  nameOnly: boolean;
30
+ authorizedCollections: boolean;
28
31
  batchSize?: number;
29
32
 
30
33
  constructor(db: Db, filter: Document, options?: ListCollectionsOptions) {
@@ -34,6 +37,7 @@ export class ListCollectionsOperation extends CommandOperation<string[]> {
34
37
  this.db = db;
35
38
  this.filter = filter;
36
39
  this.nameOnly = !!this.options.nameOnly;
40
+ this.authorizedCollections = !!this.options.authorizedCollections;
37
41
 
38
42
  if (typeof this.options.batchSize === 'number') {
39
43
  this.batchSize = this.options.batchSize;
@@ -90,14 +94,18 @@ export class ListCollectionsOperation extends CommandOperation<string[]> {
90
94
  return;
91
95
  }
92
96
 
93
- const command = {
97
+ return super.executeCommand(server, session, this.generateCommand(), callback);
98
+ }
99
+
100
+ /* This is here for the purpose of unit testing the final command that gets sent. */
101
+ generateCommand(): Document {
102
+ return {
94
103
  listCollections: 1,
95
104
  filter: this.filter,
96
105
  cursor: this.batchSize ? { batchSize: this.batchSize } : {},
97
- nameOnly: this.nameOnly
106
+ nameOnly: this.nameOnly,
107
+ authorizedCollections: this.authorizedCollections
98
108
  };
99
-
100
- return super.executeCommand(server, session, command, callback);
101
109
  }
102
110
  }
103
111
 
@@ -10,7 +10,8 @@ export const Aspect = {
10
10
  RETRYABLE: Symbol('RETRYABLE'),
11
11
  EXPLAINABLE: Symbol('EXPLAINABLE'),
12
12
  SKIP_COLLATION: Symbol('SKIP_COLLATION'),
13
- CURSOR_CREATING: Symbol('CURSOR_CREATING')
13
+ CURSOR_CREATING: Symbol('CURSOR_CREATING'),
14
+ CURSOR_ITERATING: Symbol('CURSOR_ITERATING')
14
15
  } as const;
15
16
 
16
17
  /** @public */
@@ -31,6 +32,7 @@ export interface OperationOptions extends BSONSerializeOptions {
31
32
 
32
33
  /** @internal Hints to `executeOperation` that this operation should not unpin on an ended transaction */
33
34
  bypassPinningCheck?: boolean;
35
+ omitReadPreference?: boolean;
34
36
  }
35
37
 
36
38
  /** @internal */
@@ -49,6 +51,7 @@ export abstract class AbstractOperation<TResult = any> {
49
51
  readPreference: ReadPreference;
50
52
  server!: Server;
51
53
  bypassPinningCheck: boolean;
54
+ trySecondaryWrite: boolean;
52
55
 
53
56
  // BSON serialization options
54
57
  bsonOptions?: BSONSerializeOptions;
@@ -72,6 +75,7 @@ export abstract class AbstractOperation<TResult = any> {
72
75
 
73
76
  this.options = options;
74
77
  this.bypassPinningCheck = !!options.bypassPinningCheck;
78
+ this.trySecondaryWrite = false;
75
79
  }
76
80
 
77
81
  abstract execute(server: Server, session: ClientSession, callback: Callback<TResult>): void;
@@ -156,7 +156,10 @@ export class ReadPreference {
156
156
  }
157
157
 
158
158
  if (typeof readPreference === 'string') {
159
- return new ReadPreference(readPreference as ReadPreferenceMode, readPreferenceTags);
159
+ return new ReadPreference(readPreference as ReadPreferenceMode, readPreferenceTags, {
160
+ maxStalenessSeconds: options.maxStalenessSeconds,
161
+ hedge: options.hedge
162
+ });
160
163
  } else if (!(readPreference instanceof ReadPreference) && typeof readPreference === 'object') {
161
164
  const mode = readPreference.mode || readPreference.preference;
162
165
  if (mode && typeof mode === 'string') {
@@ -299,6 +299,14 @@ export class Server extends TypedEventEmitter<ServerEvents> {
299
299
  // Clone the options
300
300
  const finalOptions = Object.assign({}, options, { wireProtocolCommand: false });
301
301
 
302
+ // There are cases where we need to flag the read preference not to get sent in
303
+ // the command, such as pre-5.0 servers attempting to perform an aggregate write
304
+ // with a non-primary read preference. In this case the effective read preference
305
+ // (primary) is not the same as the provided and must be removed completely.
306
+ if (finalOptions.omitReadPreference) {
307
+ delete finalOptions.readPreference;
308
+ }
309
+
302
310
  // error if collation not supported
303
311
  if (collationNotSupported(this, cmd)) {
304
312
  callback(new MongoCompatibilityError(`Server ${this.name} does not support collation`));
@@ -8,6 +8,9 @@ import type { ServerDescription, TagSet } from './server_description';
8
8
  const IDLE_WRITE_PERIOD = 10000;
9
9
  const SMALLEST_MAX_STALENESS_SECONDS = 90;
10
10
 
11
+ // Minimum version to try writes on secondaries.
12
+ export const MIN_SECONDARY_WRITE_WIRE_VERSION = 13;
13
+
11
14
  /** @public */
12
15
  export type ServerSelector = (
13
16
  topologyDescription: TopologyDescription,
@@ -28,6 +31,46 @@ export function writableServerSelector(): ServerSelector {
28
31
  );
29
32
  }
30
33
 
34
+ /**
35
+ * The purpose of this selector is to select the same server, only
36
+ * if it is in a state that it can have commands sent to it.
37
+ */
38
+ export function sameServerSelector(description?: ServerDescription): ServerSelector {
39
+ return (
40
+ topologyDescription: TopologyDescription,
41
+ servers: ServerDescription[]
42
+ ): ServerDescription[] => {
43
+ if (!description) return [];
44
+ // Filter the servers to match the provided description only if
45
+ // the type is not unknown.
46
+ return servers.filter(sd => {
47
+ return sd.address === description.address && sd.type !== ServerType.Unknown;
48
+ });
49
+ };
50
+ }
51
+
52
+ /**
53
+ * Returns a server selector that uses a read preference to select a
54
+ * server potentially for a write on a secondary.
55
+ */
56
+ export function secondaryWritableServerSelector(
57
+ wireVersion?: number,
58
+ readPreference?: ReadPreference
59
+ ): ServerSelector {
60
+ // If server version < 5.0, read preference always primary.
61
+ // If server version >= 5.0...
62
+ // - If read preference is supplied, use that.
63
+ // - If no read preference is supplied, use primary.
64
+ if (
65
+ !readPreference ||
66
+ !wireVersion ||
67
+ (wireVersion && wireVersion < MIN_SECONDARY_WRITE_WIRE_VERSION)
68
+ ) {
69
+ return readPreferenceServerSelector(ReadPreference.primary);
70
+ }
71
+ return readPreferenceServerSelector(readPreference);
72
+ }
73
+
31
74
  /**
32
75
  * Reduces the passed in array of servers by the rules of the "Max Staleness" specification
33
76
  * found here: https://github.com/mongodb/specifications/blob/master/source/max-staleness/max-staleness.rst
@@ -29,18 +29,15 @@ export class SrvPollingEvent {
29
29
  this.srvRecords = srvRecords;
30
30
  }
31
31
 
32
- addresses(): Map<string, HostAddress> {
33
- return new Map(
34
- this.srvRecords.map(record => {
35
- const host = new HostAddress(`${record.name}:${record.port}`);
36
- return [host.toString(), host];
37
- })
38
- );
32
+ hostnames(): Set<string> {
33
+ return new Set(this.srvRecords.map(r => HostAddress.fromSrvRecord(r).toString()));
39
34
  }
40
35
  }
41
36
 
42
37
  /** @internal */
43
38
  export interface SrvPollerOptions extends LoggerOptions {
39
+ srvServiceName: string;
40
+ srvMaxHosts: number;
44
41
  srvHost: string;
45
42
  heartbeatFrequencyMS: number;
46
43
  }
@@ -58,6 +55,8 @@ export class SrvPoller extends TypedEventEmitter<SrvPollerEvents> {
58
55
  logger: Logger;
59
56
  haMode: boolean;
60
57
  generation: number;
58
+ srvMaxHosts: number;
59
+ srvServiceName: string;
61
60
  _timeout?: NodeJS.Timeout;
62
61
 
63
62
  /** @event */
@@ -71,8 +70,10 @@ export class SrvPoller extends TypedEventEmitter<SrvPollerEvents> {
71
70
  }
72
71
 
73
72
  this.srvHost = options.srvHost;
73
+ this.srvMaxHosts = options.srvMaxHosts ?? 0;
74
+ this.srvServiceName = options.srvServiceName ?? 'mongodb';
74
75
  this.rescanSrvIntervalMS = 60000;
75
- this.heartbeatFrequencyMS = options.heartbeatFrequencyMS || 10000;
76
+ this.heartbeatFrequencyMS = options.heartbeatFrequencyMS ?? 10000;
76
77
  this.logger = new Logger('srvPoller', options);
77
78
 
78
79
  this.haMode = false;
@@ -82,7 +83,7 @@ export class SrvPoller extends TypedEventEmitter<SrvPollerEvents> {
82
83
  }
83
84
 
84
85
  get srvAddress(): string {
85
- return `_mongodb._tcp.${this.srvHost}`;
86
+ return `_${this.srvServiceName}._tcp.${this.srvHost}`;
86
87
  }
87
88
 
88
89
  get intervalMS(): number {
@@ -143,13 +144,13 @@ export class SrvPoller extends TypedEventEmitter<SrvPollerEvents> {
143
144
  }
144
145
 
145
146
  const finalAddresses: dns.SrvRecord[] = [];
146
- srvRecords.forEach((record: dns.SrvRecord) => {
147
+ for (const record of srvRecords) {
147
148
  if (matchesParentDomain(record.name, this.srvHost)) {
148
149
  finalAddresses.push(record);
149
150
  } else {
150
151
  this.parentDomainMismatch(record);
151
152
  }
152
- });
153
+ }
153
154
 
154
155
  if (!finalAddresses.length) {
155
156
  this.failure('No valid addresses found at host');
@@ -27,7 +27,8 @@ import {
27
27
  HostAddress,
28
28
  ns,
29
29
  emitWarning,
30
- EventEmitterWithState
30
+ EventEmitterWithState,
31
+ shuffle
31
32
  } from '../utils';
32
33
  import {
33
34
  TopologyType,
@@ -140,6 +141,8 @@ export interface TopologyPrivate {
140
141
 
141
142
  /** @public */
142
143
  export interface TopologyOptions extends BSONSerializeOptions, ServerOptions {
144
+ srvMaxHosts: number;
145
+ srvServiceName: string;
143
146
  hosts: HostAddress[];
144
147
  retryWrites: boolean;
145
148
  retryReads: boolean;
@@ -182,8 +185,8 @@ export type TopologyEvents = {
182
185
  topologyOpening(event: TopologyOpeningEvent): void;
183
186
  topologyDescriptionChanged(event: TopologyDescriptionChangedEvent): void;
184
187
  error(error: Error): void;
185
- /** TODO(NODE-3273) - remove error @internal */
186
- open(error: undefined, topology: Topology): void;
188
+ /** @internal */
189
+ open(topology: Topology): void;
187
190
  close(): void;
188
191
  timeout(): void;
189
192
  } & Omit<ServerEvents, 'connect'> &
@@ -290,8 +293,15 @@ export class Topology extends TypedEventEmitter<TopologyEvents> {
290
293
  const topologyType = topologyTypeFromOptions(options);
291
294
  const topologyId = globalTopologyCounter++;
292
295
 
296
+ const selectedHosts =
297
+ options.srvMaxHosts == null ||
298
+ options.srvMaxHosts === 0 ||
299
+ options.srvMaxHosts >= seedlist.length
300
+ ? seedlist
301
+ : shuffle(seedlist, options.srvMaxHosts);
302
+
293
303
  const serverDescriptions = new Map();
294
- for (const hostAddress of seedlist) {
304
+ for (const hostAddress of selectedHosts) {
295
305
  serverDescriptions.set(hostAddress.toString(), new ServerDescription(hostAddress));
296
306
  }
297
307
 
@@ -339,7 +349,9 @@ export class Topology extends TypedEventEmitter<TopologyEvents> {
339
349
  options.srvPoller ??
340
350
  new SrvPoller({
341
351
  heartbeatFrequencyMS: this.s.heartbeatFrequencyMS,
342
- srvHost: options.srvHost
352
+ srvHost: options.srvHost,
353
+ srvMaxHosts: options.srvMaxHosts,
354
+ srvServiceName: options.srvServiceName
343
355
  });
344
356
 
345
357
  this.on(Topology.TOPOLOGY_DESCRIPTION_CHANGED, this.s.detectShardedTopology);
@@ -363,7 +375,10 @@ export class Topology extends TypedEventEmitter<TopologyEvents> {
363
375
 
364
376
  private detectSrvRecords(ev: SrvPollingEvent) {
365
377
  const previousTopologyDescription = this.s.description;
366
- this.s.description = this.s.description.updateFromSrvPollingEvent(ev);
378
+ this.s.description = this.s.description.updateFromSrvPollingEvent(
379
+ ev,
380
+ this.s.options.srvMaxHosts
381
+ );
367
382
  if (this.s.description === previousTopologyDescription) {
368
383
  // Nothing changed, so return
369
384
  return;
@@ -456,8 +471,7 @@ export class Topology extends TypedEventEmitter<TopologyEvents> {
456
471
  }
457
472
 
458
473
  stateTransition(this, STATE_CONNECTED);
459
- // TODO(NODE-3273) - remove err
460
- this.emit(Topology.OPEN, err, this);
474
+ this.emit(Topology.OPEN, this);
461
475
  this.emit(Topology.CONNECT, this);
462
476
 
463
477
  if (typeof callback === 'function') callback(undefined, this);
@@ -467,8 +481,7 @@ export class Topology extends TypedEventEmitter<TopologyEvents> {
467
481
  }
468
482
 
469
483
  stateTransition(this, STATE_CONNECTED);
470
- // TODO(NODE-3273) - remove err
471
- this.emit(Topology.OPEN, err, this);
484
+ this.emit(Topology.OPEN, this);
472
485
  this.emit(Topology.CONNECT, this);
473
486
 
474
487
  if (typeof callback === 'function') callback(undefined, this);
@@ -797,6 +810,10 @@ export class Topology extends TypedEventEmitter<TopologyEvents> {
797
810
  return result;
798
811
  }
799
812
 
813
+ get commonWireVersion(): number | undefined {
814
+ return this.description.commonWireVersion;
815
+ }
816
+
800
817
  get logicalSessionTimeoutMinutes(): number | undefined {
801
818
  return this.description.logicalSessionTimeoutMinutes;
802
819
  }
@@ -4,6 +4,7 @@ import { TopologyType, ServerType } from './common';
4
4
  import type { ObjectId, Document } from '../bson';
5
5
  import type { SrvPollingEvent } from './srv_polling';
6
6
  import { MongoError, MongoRuntimeError } from '../error';
7
+ import { shuffle } from '../utils';
7
8
 
8
9
  // constants related to compatibility checks
9
10
  const MIN_SUPPORTED_SERVER_VERSION = WIRE_CONSTANTS.MIN_SUPPORTED_SERVER_VERSION;
@@ -139,23 +140,46 @@ export class TopologyDescription {
139
140
  * Returns a new TopologyDescription based on the SrvPollingEvent
140
141
  * @internal
141
142
  */
142
- updateFromSrvPollingEvent(ev: SrvPollingEvent): TopologyDescription {
143
- const newAddresses = ev.addresses();
144
- const serverDescriptions = new Map(this.servers);
145
- for (const address of this.servers.keys()) {
146
- if (newAddresses.has(address)) {
147
- newAddresses.delete(address);
148
- } else {
149
- serverDescriptions.delete(address);
143
+ updateFromSrvPollingEvent(ev: SrvPollingEvent, srvMaxHosts = 0): TopologyDescription {
144
+ /** The SRV addresses defines the set of addresses we should be using */
145
+ const incomingHostnames = ev.hostnames();
146
+ const currentHostnames = new Set(this.servers.keys());
147
+
148
+ const hostnamesToAdd = new Set<string>(incomingHostnames);
149
+ const hostnamesToRemove = new Set<string>();
150
+ for (const hostname of currentHostnames) {
151
+ // filter hostnamesToAdd (made from incomingHostnames) down to what is *not* present in currentHostnames
152
+ hostnamesToAdd.delete(hostname);
153
+ if (!incomingHostnames.has(hostname)) {
154
+ // If the SRV Records no longer include this hostname
155
+ // we have to stop using it
156
+ hostnamesToRemove.add(hostname);
150
157
  }
151
158
  }
152
159
 
153
- if (serverDescriptions.size === this.servers.size && newAddresses.size === 0) {
160
+ if (hostnamesToAdd.size === 0 && hostnamesToRemove.size === 0) {
161
+ // No new hosts to add and none to remove
154
162
  return this;
155
163
  }
156
164
 
157
- for (const [address, host] of newAddresses) {
158
- serverDescriptions.set(address, new ServerDescription(host));
165
+ const serverDescriptions = new Map(this.servers);
166
+ for (const removedHost of hostnamesToRemove) {
167
+ serverDescriptions.delete(removedHost);
168
+ }
169
+
170
+ if (hostnamesToAdd.size > 0) {
171
+ if (srvMaxHosts === 0) {
172
+ // Add all!
173
+ for (const hostToAdd of hostnamesToAdd) {
174
+ serverDescriptions.set(hostToAdd, new ServerDescription(hostToAdd));
175
+ }
176
+ } else if (serverDescriptions.size < srvMaxHosts) {
177
+ // Add only the amount needed to get us back to srvMaxHosts
178
+ const selectedHosts = shuffle(hostnamesToAdd, srvMaxHosts - serverDescriptions.size);
179
+ for (const selectedHostToAdd of selectedHosts) {
180
+ serverDescriptions.set(selectedHostToAdd, new ServerDescription(selectedHostToAdd));
181
+ }
182
+ }
159
183
  }
160
184
 
161
185
  return new TopologyDescription(
package/src/sessions.ts CHANGED
@@ -8,6 +8,7 @@ import {
8
8
  MongoError,
9
9
  MongoInvalidArgumentError,
10
10
  isRetryableError,
11
+ isRetryableEndTransactionError,
11
12
  MongoCompatibilityError,
12
13
  MongoNetworkError,
13
14
  MongoWriteConcernError,
@@ -767,7 +768,7 @@ function endTransaction(session: ClientSession, commandName: string, callback: C
767
768
  session.unpin();
768
769
  }
769
770
 
770
- if (err && isRetryableError(err as MongoError)) {
771
+ if (err && isRetryableEndTransactionError(err as MongoError)) {
771
772
  // SPEC-1185: apply majority write concern when retrying commitTransaction
772
773
  if (command.commitTransaction) {
773
774
  // per txns spec, must unpin session in this case
@@ -1,6 +1,6 @@
1
1
  import { ReadPreference } from './read_preference';
2
2
  import { MongoRuntimeError, MongoTransactionError } from './error';
3
- import { ReadConcern } from './read_concern';
3
+ import { ReadConcern, ReadConcernLike } from './read_concern';
4
4
  import { WriteConcern } from './write_concern';
5
5
  import type { Server } from './sdam/server';
6
6
  import type { CommandOperationOptions } from './operations/command';
@@ -63,7 +63,7 @@ const COMMITTED_STATES: Set<TxnState> = new Set([
63
63
  export interface TransactionOptions extends CommandOperationOptions {
64
64
  // TODO(NODE-3344): These options use the proper class forms of these settings, it should accept the basic enum values too
65
65
  /** A default read concern for commands in this transaction */
66
- readConcern?: ReadConcern;
66
+ readConcern?: ReadConcernLike;
67
67
  /** A default writeConcern for commands in this transaction */
68
68
  writeConcern?: WriteConcern;
69
69
  /** A default read preference for commands in this transaction */
package/src/utils.ts CHANGED
@@ -28,6 +28,7 @@ import type { CommandOperationOptions, OperationParent } from './operations/comm
28
28
  import { ReadPreference } from './read_preference';
29
29
  import { URL } from 'url';
30
30
  import { MAX_SUPPORTED_WIRE_VERSION } from './cmap/wire_protocol/constants';
31
+ import type { SrvRecord } from 'dns';
31
32
 
32
33
  /**
33
34
  * MongoDB Driver style callback
@@ -41,23 +42,6 @@ export const MAX_JS_INT = Number.MAX_SAFE_INTEGER + 1;
41
42
 
42
43
  export type AnyOptions = Document;
43
44
 
44
- /**
45
- * Add a readonly enumerable property.
46
- * @internal
47
- */
48
- export function getSingleProperty(
49
- obj: AnyOptions,
50
- name: string | number | symbol,
51
- value: unknown
52
- ): void {
53
- Object.defineProperty(obj, name, {
54
- enumerable: true,
55
- get() {
56
- return value;
57
- }
58
- });
59
- }
60
-
61
45
  /**
62
46
  * Throws if collectionName is not a valid mongodb collection namespace.
63
47
  * @internal
@@ -185,17 +169,6 @@ export function isObject(arg: unknown): arg is object {
185
169
  return '[object Object]' === Object.prototype.toString.call(arg);
186
170
  }
187
171
 
188
- /** @internal */
189
- export function decorateCommand(command: Document, options: Document, exclude: string[]): Document {
190
- for (const name in options) {
191
- if (!exclude.includes(name)) {
192
- command[name] = options[name];
193
- }
194
- }
195
-
196
- return command;
197
- }
198
-
199
172
  /** @internal */
200
173
  export function mergeOptions<T, S>(target: T, source: S): T & S {
201
174
  return { ...target, ...source };
@@ -657,11 +630,6 @@ export function databaseNamespace(ns: string): string {
657
630
  return ns.split('.')[0];
658
631
  }
659
632
 
660
- /** @internal */
661
- export function collectionNamespace(ns: string): string {
662
- return ns.split('.').slice(1).join('.');
663
- }
664
-
665
633
  /**
666
634
  * Synchronously Generate a UUIDv4
667
635
  * @internal
@@ -987,7 +955,7 @@ export function makeInterruptibleAsyncInterval(
987
955
  ): InterruptibleAsyncInterval {
988
956
  let timerId: NodeJS.Timeout | undefined;
989
957
  let lastCallTime: number;
990
- let lastWakeTime: number;
958
+ let cannotBeExpedited = false;
991
959
  let stopped = false;
992
960
 
993
961
  options = options ?? {};
@@ -998,10 +966,8 @@ export function makeInterruptibleAsyncInterval(
998
966
 
999
967
  function wake() {
1000
968
  const currentTime = clock();
1001
- const timeSinceLastWake = currentTime - lastWakeTime;
1002
- const timeSinceLastCall = currentTime - lastCallTime;
1003
- const timeUntilNextCall = interval - timeSinceLastCall;
1004
- lastWakeTime = currentTime;
969
+ const nextScheduledCallTime = lastCallTime + interval;
970
+ const timeUntilNextCall = nextScheduledCallTime - currentTime;
1005
971
 
1006
972
  // For the streaming protocol: there is nothing obviously stopping this
1007
973
  // interval from being woken up again while we are waiting "infinitely"
@@ -1009,8 +975,17 @@ export function makeInterruptibleAsyncInterval(
1009
975
  // never completes, the `timeUntilNextCall` will continue to grow
1010
976
  // negatively unbounded, so it will never trigger a reschedule here.
1011
977
 
978
+ // This is possible in virtualized environments like AWS Lambda where our
979
+ // clock is unreliable. In these cases the timer is "running" but never
980
+ // actually completes, so we want to execute immediately and then attempt
981
+ // to reschedule.
982
+ if (timeUntilNextCall < 0) {
983
+ executeAndReschedule();
984
+ return;
985
+ }
986
+
1012
987
  // debounce multiple calls to wake within the `minInterval`
1013
- if (timeSinceLastWake < minInterval) {
988
+ if (cannotBeExpedited) {
1014
989
  return;
1015
990
  }
1016
991
 
@@ -1018,14 +993,7 @@ export function makeInterruptibleAsyncInterval(
1018
993
  // faster than the `minInterval`
1019
994
  if (timeUntilNextCall > minInterval) {
1020
995
  reschedule(minInterval);
1021
- }
1022
-
1023
- // This is possible in virtualized environments like AWS Lambda where our
1024
- // clock is unreliable. In these cases the timer is "running" but never
1025
- // actually completes, so we want to execute immediately and then attempt
1026
- // to reschedule.
1027
- if (timeUntilNextCall < 0) {
1028
- executeAndReschedule();
996
+ cannotBeExpedited = true;
1029
997
  }
1030
998
  }
1031
999
 
@@ -1037,7 +1005,7 @@ export function makeInterruptibleAsyncInterval(
1037
1005
  }
1038
1006
 
1039
1007
  lastCallTime = 0;
1040
- lastWakeTime = 0;
1008
+ cannotBeExpedited = false;
1041
1009
  }
1042
1010
 
1043
1011
  function reschedule(ms?: number) {
@@ -1050,7 +1018,7 @@ export function makeInterruptibleAsyncInterval(
1050
1018
  }
1051
1019
 
1052
1020
  function executeAndReschedule() {
1053
- lastWakeTime = 0;
1021
+ cannotBeExpedited = false;
1054
1022
  lastCallTime = clock();
1055
1023
 
1056
1024
  fn(err => {
@@ -1128,8 +1096,9 @@ export function isSuperset(set: Set<any> | any[], subset: Set<any> | any[]): boo
1128
1096
  return true;
1129
1097
  }
1130
1098
 
1131
- export function setDifference(setA: Iterable<any>, setB: Iterable<any>): Set<any> {
1132
- const difference = new Set(setA);
1099
+ /** Returns the items that are uniquely in setA */
1100
+ export function setDifference<T>(setA: Iterable<T>, setB: Iterable<T>): Set<T> {
1101
+ const difference = new Set<T>(setA);
1133
1102
  for (const elem of setB) {
1134
1103
  difference.delete(elem);
1135
1104
  }
@@ -1179,11 +1148,11 @@ export function isRecord(
1179
1148
  * but instead something that is good enough for the purposes of
1180
1149
  * command monitoring.
1181
1150
  */
1182
- export function deepCopy<T extends any>(value: T): T {
1151
+ export function deepCopy<T>(value: T): T {
1183
1152
  if (value == null) {
1184
1153
  return value;
1185
1154
  } else if (Array.isArray(value)) {
1186
- return value.map(item => deepCopy(item)) as T;
1155
+ return value.map(item => deepCopy(item)) as unknown as T;
1187
1156
  } else if (isRecord(value)) {
1188
1157
  const res = {} as any;
1189
1158
  for (const key in value) {
@@ -1198,11 +1167,11 @@ export function deepCopy<T extends any>(value: T): T {
1198
1167
  case 'date':
1199
1168
  return new ctor(Number(value));
1200
1169
  case 'map':
1201
- return new Map(value as any) as T;
1170
+ return new Map(value as any) as unknown as T;
1202
1171
  case 'set':
1203
- return new Set(value as any) as T;
1172
+ return new Set(value as any) as unknown as T;
1204
1173
  case 'buffer':
1205
- return Buffer.from(value as Buffer) as T;
1174
+ return Buffer.from(value as Buffer) as unknown as T;
1206
1175
  }
1207
1176
  }
1208
1177
 
@@ -1352,6 +1321,14 @@ export class HostAddress {
1352
1321
  Object.freeze(this);
1353
1322
  }
1354
1323
 
1324
+ [Symbol.for('nodejs.util.inspect.custom')](): string {
1325
+ return this.inspect();
1326
+ }
1327
+
1328
+ inspect(): string {
1329
+ return `new HostAddress('${this.toString(true)}')`;
1330
+ }
1331
+
1355
1332
  /**
1356
1333
  * @param ipv6Brackets - optionally request ipv6 bracket notation required for connection strings
1357
1334
  */
@@ -1368,6 +1345,10 @@ export class HostAddress {
1368
1345
  static fromString(s: string): HostAddress {
1369
1346
  return new HostAddress(s);
1370
1347
  }
1348
+
1349
+ static fromSrvRecord({ name, port }: SrvRecord): HostAddress {
1350
+ return HostAddress.fromString(`${name}:${port}`);
1351
+ }
1371
1352
  }
1372
1353
 
1373
1354
  export const DEFAULT_PK_FACTORY = {
@@ -1438,3 +1419,33 @@ export function parsePackageVersion({ version }: { version: string }): {
1438
1419
  const [major, minor, patch] = version.split('.').map((n: string) => Number.parseInt(n, 10));
1439
1420
  return { major, minor, patch };
1440
1421
  }
1422
+
1423
+ /**
1424
+ * Fisher–Yates Shuffle
1425
+ *
1426
+ * Reference: https://bost.ocks.org/mike/shuffle/
1427
+ * @param sequence - items to be shuffled
1428
+ * @param limit - Defaults to `0`. If nonzero shuffle will slice the randomized array e.g, `.slice(0, limit)` otherwise will return the entire randomized array.
1429
+ */
1430
+ export function shuffle<T>(sequence: Iterable<T>, limit = 0): Array<T> {
1431
+ const items = Array.from(sequence); // shallow copy in order to never shuffle the input
1432
+
1433
+ if (limit > items.length) {
1434
+ throw new MongoRuntimeError('Limit must be less than the number of items');
1435
+ }
1436
+
1437
+ let remainingItemsToShuffle = items.length;
1438
+ const lowerBound = limit % items.length === 0 ? 1 : items.length - limit;
1439
+ while (remainingItemsToShuffle > lowerBound) {
1440
+ // Pick a remaining element
1441
+ const randomIndex = Math.floor(Math.random() * remainingItemsToShuffle);
1442
+ remainingItemsToShuffle -= 1;
1443
+
1444
+ // And swap it with the current element
1445
+ const swapHold = items[remainingItemsToShuffle];
1446
+ items[remainingItemsToShuffle] = items[randomIndex];
1447
+ items[randomIndex] = swapHold;
1448
+ }
1449
+
1450
+ return limit % items.length === 0 ? items : items.slice(lowerBound);
1451
+ }