mongodb 7.0.0 → 7.1.0-dev.20260205.sha.d2ad07f2

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 (104) hide show
  1. package/lib/bson.js +2 -1
  2. package/lib/bson.js.map +1 -1
  3. package/lib/change_stream.js +3 -1
  4. package/lib/change_stream.js.map +1 -1
  5. package/lib/client-side-encryption/state_machine.js +1 -0
  6. package/lib/client-side-encryption/state_machine.js.map +1 -1
  7. package/lib/cmap/auth/aws4.js +161 -0
  8. package/lib/cmap/auth/aws4.js.map +1 -0
  9. package/lib/cmap/auth/aws_temporary_credentials.js +1 -0
  10. package/lib/cmap/auth/aws_temporary_credentials.js.map +1 -1
  11. package/lib/cmap/auth/gssapi.js +2 -4
  12. package/lib/cmap/auth/gssapi.js.map +1 -1
  13. package/lib/cmap/auth/mongo_credentials.js +2 -1
  14. package/lib/cmap/auth/mongo_credentials.js.map +1 -1
  15. package/lib/cmap/auth/mongodb_aws.js +9 -14
  16. package/lib/cmap/auth/mongodb_aws.js.map +1 -1
  17. package/lib/cmap/auth/mongodb_oidc/k8s_machine_workflow.js +1 -0
  18. package/lib/cmap/auth/mongodb_oidc/k8s_machine_workflow.js.map +1 -1
  19. package/lib/cmap/auth/mongodb_oidc/token_machine_workflow.js +1 -0
  20. package/lib/cmap/auth/mongodb_oidc/token_machine_workflow.js.map +1 -1
  21. package/lib/cmap/connect.js +15 -0
  22. package/lib/cmap/connect.js.map +1 -1
  23. package/lib/cmap/connection.js +3 -3
  24. package/lib/cmap/connection.js.map +1 -1
  25. package/lib/cmap/connection_pool.js +8 -8
  26. package/lib/cmap/connection_pool.js.map +1 -1
  27. package/lib/cmap/connection_pool_events.js +3 -3
  28. package/lib/cmap/connection_pool_events.js.map +1 -1
  29. package/lib/cmap/handshake/client_metadata.js +7 -7
  30. package/lib/cmap/handshake/client_metadata.js.map +1 -1
  31. package/lib/cmap/wire_protocol/compression.js +18 -3
  32. package/lib/cmap/wire_protocol/compression.js.map +1 -1
  33. package/lib/connection_string.js +6 -0
  34. package/lib/connection_string.js.map +1 -1
  35. package/lib/db.js +0 -1
  36. package/lib/db.js.map +1 -1
  37. package/lib/deps.js +0 -13
  38. package/lib/deps.js.map +1 -1
  39. package/lib/error.js +5 -8
  40. package/lib/error.js.map +1 -1
  41. package/lib/gridfs/upload.js +8 -8
  42. package/lib/gridfs/upload.js.map +1 -1
  43. package/lib/mongo_client.js +1 -1
  44. package/lib/mongo_client.js.map +1 -1
  45. package/lib/mongo_logger.js +11 -5
  46. package/lib/mongo_logger.js.map +1 -1
  47. package/lib/operations/execute_operation.js +5 -4
  48. package/lib/operations/execute_operation.js.map +1 -1
  49. package/lib/operations/rename.js.map +1 -1
  50. package/lib/runtime_adapters.js +16 -0
  51. package/lib/runtime_adapters.js.map +1 -0
  52. package/lib/sdam/monitor.js +8 -8
  53. package/lib/sdam/monitor.js.map +1 -1
  54. package/lib/sdam/server.js +41 -29
  55. package/lib/sdam/server.js.map +1 -1
  56. package/lib/sdam/server_description.js +1 -1
  57. package/lib/sdam/server_description.js.map +1 -1
  58. package/lib/sdam/server_selection.js +135 -72
  59. package/lib/sdam/server_selection.js.map +1 -1
  60. package/lib/sdam/topology.js +9 -7
  61. package/lib/sdam/topology.js.map +1 -1
  62. package/lib/sdam/topology_description.js +1 -1
  63. package/lib/sdam/topology_description.js.map +1 -1
  64. package/lib/sessions.js +87 -23
  65. package/lib/sessions.js.map +1 -1
  66. package/lib/utils.js +21 -47
  67. package/lib/utils.js.map +1 -1
  68. package/mongodb.d.ts +39 -2
  69. package/package.json +10 -12
  70. package/src/bson.ts +1 -0
  71. package/src/change_stream.ts +3 -1
  72. package/src/client-side-encryption/state_machine.ts +1 -0
  73. package/src/cmap/auth/aws4.ts +207 -0
  74. package/src/cmap/auth/aws_temporary_credentials.ts +2 -0
  75. package/src/cmap/auth/gssapi.ts +8 -4
  76. package/src/cmap/auth/mongo_credentials.ts +2 -1
  77. package/src/cmap/auth/mongodb_aws.ts +10 -17
  78. package/src/cmap/auth/mongodb_oidc/k8s_machine_workflow.ts +1 -0
  79. package/src/cmap/auth/mongodb_oidc/token_machine_workflow.ts +1 -0
  80. package/src/cmap/connect.ts +19 -0
  81. package/src/cmap/connection.ts +7 -4
  82. package/src/cmap/connection_pool.ts +9 -9
  83. package/src/cmap/connection_pool_events.ts +4 -4
  84. package/src/cmap/handshake/client_metadata.ts +10 -9
  85. package/src/cmap/wire_protocol/compression.ts +17 -3
  86. package/src/connection_string.ts +7 -0
  87. package/src/db.ts +0 -1
  88. package/src/deps.ts +0 -60
  89. package/src/error.ts +4 -8
  90. package/src/gridfs/upload.ts +8 -8
  91. package/src/index.ts +2 -1
  92. package/src/mongo_client.ts +12 -2
  93. package/src/mongo_logger.ts +11 -6
  94. package/src/operations/execute_operation.ts +6 -5
  95. package/src/operations/rename.ts +6 -1
  96. package/src/runtime_adapters.ts +49 -0
  97. package/src/sdam/monitor.ts +10 -10
  98. package/src/sdam/server.ts +50 -32
  99. package/src/sdam/server_description.ts +8 -2
  100. package/src/sdam/server_selection.ts +188 -96
  101. package/src/sdam/topology.ts +20 -16
  102. package/src/sdam/topology_description.ts +1 -1
  103. package/src/sessions.ts +114 -30
  104. package/src/utils.ts +17 -74
@@ -14,8 +14,8 @@ import {
14
14
  type EventEmitterWithState,
15
15
  makeStateMachine,
16
16
  noop,
17
- now,
18
- ns
17
+ ns,
18
+ processTimeMS
19
19
  } from '../utils';
20
20
  import { ServerType, STATE_CLOSED, STATE_CLOSING } from './common';
21
21
  import {
@@ -326,7 +326,7 @@ function checkServer(monitor: Monitor, callback: Callback<Document | null>) {
326
326
  );
327
327
  // We have not actually sent an outgoing handshake, but when we get the next response we
328
328
  // want the duration to reflect the time since we last heard from the server
329
- start = now();
329
+ start = processTimeMS();
330
330
  } else {
331
331
  monitor.rttPinger?.close();
332
332
  monitor.rttPinger = undefined;
@@ -360,7 +360,7 @@ function checkServer(monitor: Monitor, callback: Callback<Document | null>) {
360
360
  }
361
361
 
362
362
  // Record new start time before sending handshake
363
- start = now();
363
+ start = processTimeMS();
364
364
 
365
365
  if (isAwaitable) {
366
366
  awaited = true;
@@ -383,7 +383,7 @@ function checkServer(monitor: Monitor, callback: Callback<Document | null>) {
383
383
  const socket = await makeSocket(monitor.connectOptions);
384
384
  const connection = makeConnection(monitor.connectOptions, socket);
385
385
  // The start time is after socket creation but before the handshake
386
- start = now();
386
+ start = processTimeMS();
387
387
  try {
388
388
  await performInitialHandshake(connection, monitor.connectOptions);
389
389
  return connection;
@@ -426,7 +426,7 @@ function checkServer(monitor: Monitor, callback: Callback<Document | null>) {
426
426
  function monitorServer(monitor: Monitor) {
427
427
  return (callback: Callback) => {
428
428
  if (monitor.s.state === STATE_MONITORING) {
429
- process.nextTick(callback);
429
+ queueMicrotask(callback);
430
430
  return;
431
431
  }
432
432
  stateTransition(monitor, STATE_MONITORING);
@@ -532,7 +532,7 @@ export class RTTPinger {
532
532
  }
533
533
 
534
534
  private measureRoundTripTime() {
535
- const start = now();
535
+ const start = processTimeMS();
536
536
 
537
537
  if (this.closed) {
538
538
  return;
@@ -607,7 +607,7 @@ export class MonitorInterval {
607
607
  }
608
608
 
609
609
  wake() {
610
- const currentTime = now();
610
+ const currentTime = processTimeMS();
611
611
  const timeSinceLastCall = currentTime - this.lastExecutionEnded;
612
612
 
613
613
  // TODO(NODE-4674): Add error handling and logging to the monitor
@@ -651,7 +651,7 @@ export class MonitorInterval {
651
651
  }
652
652
 
653
653
  toJSON() {
654
- const currentTime = now();
654
+ const currentTime = processTimeMS();
655
655
  const timeSinceLastCall = currentTime - this.lastExecutionEnded;
656
656
  return {
657
657
  timerId: this.timerId != null ? 'set' : 'cleared',
@@ -684,7 +684,7 @@ export class MonitorInterval {
684
684
  this.isExecutionInProgress = true;
685
685
 
686
686
  this.fn(() => {
687
- this.lastExecutionEnded = now();
687
+ this.lastExecutionEnded = processTimeMS();
688
688
  this.isExecutionInProgress = false;
689
689
  this._reschedule(this.heartbeatFrequencyMS);
690
690
  });
@@ -22,12 +22,13 @@ import {
22
22
  import {
23
23
  type AnyError,
24
24
  isNodeShuttingDownError,
25
- isSDAMUnrecoverableError,
25
+ isStateChangeError,
26
26
  MONGODB_ERROR_CODES,
27
27
  MongoError,
28
28
  MongoErrorLabel,
29
29
  MongoNetworkError,
30
30
  MongoNetworkTimeoutError,
31
+ MongoParseError,
31
32
  MongoRuntimeError,
32
33
  MongoServerClosedError,
33
34
  type MongoServerError,
@@ -356,7 +357,7 @@ export class Server extends TypedEventEmitter<ServerEvents> {
356
357
  } catch (operationError) {
357
358
  if (
358
359
  operationError instanceof MongoError &&
359
- operationError.code === MONGODB_ERROR_CODES.Reauthenticate
360
+ operationError.code?.valueOf() === MONGODB_ERROR_CODES.Reauthenticate
360
361
  ) {
361
362
  reauthPromise = this.pool.reauthenticate(conn);
362
363
  reauthPromise.then(undefined, error => {
@@ -391,9 +392,7 @@ export class Server extends TypedEventEmitter<ServerEvents> {
391
392
  return;
392
393
  }
393
394
 
394
- const isStaleError =
395
- error.connectionGeneration && error.connectionGeneration < this.pool.generation;
396
- if (isStaleError) {
395
+ if (isStaleError(this, error)) {
397
396
  return;
398
397
  }
399
398
 
@@ -401,33 +400,46 @@ export class Server extends TypedEventEmitter<ServerEvents> {
401
400
  error instanceof MongoNetworkError && !(error instanceof MongoNetworkTimeoutError);
402
401
  const isNetworkTimeoutBeforeHandshakeError =
403
402
  error instanceof MongoNetworkError && error.beforeHandshake;
404
- const isAuthHandshakeError = error.hasErrorLabel(MongoErrorLabel.HandshakeError);
405
- if (isNetworkNonTimeoutError || isNetworkTimeoutBeforeHandshakeError || isAuthHandshakeError) {
406
- // In load balanced mode we never mark the server as unknown and always
407
- // clear for the specific service id.
403
+ const isAuthOrEstablishmentHandshakeError = error.hasErrorLabel(MongoErrorLabel.HandshakeError);
404
+ const isSystemOverloadError = error.hasErrorLabel(MongoErrorLabel.SystemOverloadedError);
405
+
406
+ // Perhaps questionable and divergent from the spec, but considering MongoParseErrors like state change errors was legacy behavior.
407
+ if (isStateChangeError(error) || error instanceof MongoParseError) {
408
+ const shouldClearPool = isNodeShuttingDownError(error);
409
+
410
+ // from the SDAM spec: The driver MUST synchronize clearing the pool with updating the topology.
411
+ // In load balanced mode: there is no monitoring, so there is no topology to update. We simply clear the pool.
412
+ // For other topologies: the `ResetPool` label instructs the topology to clear the server's pool in `updateServer()`.
413
+ if (!this.loadBalanced) {
414
+ if (shouldClearPool) {
415
+ error.addErrorLabel(MongoErrorLabel.ResetPool);
416
+ }
417
+ markServerUnknown(this, error);
418
+ queueMicrotask(() => this.requestCheck());
419
+ return;
420
+ }
421
+
422
+ if (connection && shouldClearPool) {
423
+ this.pool.clear({ serviceId: connection.serviceId });
424
+ }
425
+ } else if (
426
+ isNetworkNonTimeoutError ||
427
+ isNetworkTimeoutBeforeHandshakeError ||
428
+ isAuthOrEstablishmentHandshakeError
429
+ ) {
430
+ // Do NOT clear the pool if we encounter a system overloaded error.
431
+ if (isSystemOverloadError) {
432
+ return;
433
+ }
434
+ // from the SDAM spec: The driver MUST synchronize clearing the pool with updating the topology.
435
+ // In load balanced mode: there is no monitoring, so there is no topology to update. We simply clear the pool.
436
+ // For other topologies: the `ResetPool` label instructs the topology to clear the server's pool in `updateServer()`.
408
437
  if (!this.loadBalanced) {
409
438
  error.addErrorLabel(MongoErrorLabel.ResetPool);
410
439
  markServerUnknown(this, error);
411
440
  } else if (connection) {
412
441
  this.pool.clear({ serviceId: connection.serviceId });
413
442
  }
414
- } else {
415
- if (isSDAMUnrecoverableError(error)) {
416
- if (shouldHandleStateChangeError(this, error)) {
417
- const shouldClearPool = isNodeShuttingDownError(error);
418
- if (this.loadBalanced && connection && shouldClearPool) {
419
- this.pool.clear({ serviceId: connection.serviceId });
420
- }
421
-
422
- if (!this.loadBalanced) {
423
- if (shouldClearPool) {
424
- error.addErrorLabel(MongoErrorLabel.ResetPool);
425
- }
426
- markServerUnknown(this, error);
427
- process.nextTick(() => this.requestCheck());
428
- }
429
- }
430
- }
431
443
  }
432
444
  }
433
445
 
@@ -560,12 +572,6 @@ function connectionIsStale(pool: ConnectionPool, connection: Connection) {
560
572
  return connection.generation !== pool.generation;
561
573
  }
562
574
 
563
- function shouldHandleStateChangeError(server: Server, err: MongoError) {
564
- const etv = err.topologyVersion;
565
- const stv = server.description.topologyVersion;
566
- return compareTopologyVersion(stv, etv) < 0;
567
- }
568
-
569
575
  function inActiveTransaction(session: ClientSession | undefined, cmd: Document) {
570
576
  return session && session.inTransaction() && !isTransactionCommand(cmd);
571
577
  }
@@ -575,3 +581,15 @@ function inActiveTransaction(session: ClientSession | undefined, cmd: Document)
575
581
  function isRetryableWritesEnabled(topology: Topology) {
576
582
  return topology.s.options.retryWrites !== false;
577
583
  }
584
+
585
+ function isStaleError(server: Server, error: MongoError): boolean {
586
+ const currentGeneration = server.pool.generation;
587
+ const generation = error.connectionGeneration;
588
+
589
+ if (generation && generation < currentGeneration) {
590
+ return true;
591
+ }
592
+
593
+ const currentTopologyVersion = server.description.topologyVersion;
594
+ return compareTopologyVersion(currentTopologyVersion, error.topologyVersion) >= 0;
595
+ }
@@ -1,6 +1,12 @@
1
1
  import { type Document, Long, type ObjectId } from '../bson';
2
2
  import { type MongoError, MongoRuntimeError } from '../error';
3
- import { arrayStrictEqual, compareObjectId, errorStrictEqual, HostAddress, now } from '../utils';
3
+ import {
4
+ arrayStrictEqual,
5
+ compareObjectId,
6
+ errorStrictEqual,
7
+ HostAddress,
8
+ processTimeMS
9
+ } from '../utils';
4
10
  import { type ClusterTime, ServerType } from './common';
5
11
 
6
12
  const WRITABLE_SERVER_TYPES = new Set<ServerType>([
@@ -110,7 +116,7 @@ export class ServerDescription {
110
116
  this.maxWireVersion = hello?.maxWireVersion ?? 0;
111
117
  this.roundTripTime = options?.roundTripTime ?? -1;
112
118
  this.minRoundTripTime = options?.minRoundTripTime ?? 0;
113
- this.lastUpdateTime = now();
119
+ this.lastUpdateTime = processTimeMS();
114
120
  this.lastWriteDate = hello?.lastWrite?.lastWriteDate ?? 0;
115
121
  // NOTE: This actually builds the stack string instead of holding onto the getter and all its
116
122
  // associated references. This is done to prevent a memory leak.
@@ -1,4 +1,4 @@
1
- import { MongoInvalidArgumentError } from '../error';
1
+ import { MongoInvalidArgumentError, MongoRuntimeError } from '../error';
2
2
  import { ReadPreference } from '../read_preference';
3
3
  import { ServerType, TopologyType } from './common';
4
4
  import type { ServerDescription, TagSet } from './server_description';
@@ -15,21 +15,52 @@ export const MIN_SECONDARY_WRITE_WIRE_VERSION = 13;
15
15
  export type ServerSelector = (
16
16
  topologyDescription: TopologyDescription,
17
17
  servers: ServerDescription[],
18
- deprioritized?: ServerDescription[]
18
+ deprioritized: DeprioritizedServers
19
19
  ) => ServerDescription[];
20
20
 
21
+ /** @internal */
22
+ export class DeprioritizedServers {
23
+ private deprioritized: Set<string> = new Set();
24
+
25
+ constructor(descriptions?: Iterable<ServerDescription>) {
26
+ for (const description of descriptions ?? []) {
27
+ this.add(description);
28
+ }
29
+ }
30
+
31
+ add({ address }: ServerDescription) {
32
+ this.deprioritized.add(address);
33
+ }
34
+
35
+ has({ address }: ServerDescription): boolean {
36
+ return this.deprioritized.has(address);
37
+ }
38
+ }
39
+
40
+ function filterDeprioritized(
41
+ candidates: ServerDescription[],
42
+ deprioritized: DeprioritizedServers
43
+ ): ServerDescription[] {
44
+ const filtered = candidates.filter(candidate => !deprioritized.has(candidate));
45
+
46
+ return filtered.length ? filtered : candidates;
47
+ }
48
+
21
49
  /**
22
50
  * Returns a server selector that selects for writable servers
23
51
  */
24
52
  export function writableServerSelector(): ServerSelector {
25
53
  return function writableServer(
26
54
  topologyDescription: TopologyDescription,
27
- servers: ServerDescription[]
55
+ servers: ServerDescription[],
56
+ deprioritized: DeprioritizedServers
28
57
  ): ServerDescription[] {
29
- return latencyWindowReducer(
30
- topologyDescription,
31
- servers.filter((s: ServerDescription) => s.isWritable)
58
+ const eligibleServers = filterDeprioritized(
59
+ servers.filter(({ isWritable }) => isWritable),
60
+ deprioritized
32
61
  );
62
+
63
+ return latencyWindowReducer(topologyDescription, eligibleServers);
33
64
  };
34
65
  }
35
66
 
@@ -39,8 +70,9 @@ export function writableServerSelector(): ServerSelector {
39
70
  */
40
71
  export function sameServerSelector(description?: ServerDescription): ServerSelector {
41
72
  return function sameServerSelector(
42
- topologyDescription: TopologyDescription,
43
- servers: ServerDescription[]
73
+ _topologyDescription: TopologyDescription,
74
+ servers: ServerDescription[],
75
+ _deprioritized: DeprioritizedServers
44
76
  ): ServerDescription[] {
45
77
  if (!description) return [];
46
78
  // Filter the servers to match the provided description only if
@@ -56,7 +88,7 @@ export function sameServerSelector(description?: ServerDescription): ServerSelec
56
88
  * server potentially for a write on a secondary.
57
89
  */
58
90
  export function secondaryWritableServerSelector(
59
- wireVersion?: number,
91
+ wireVersion: number,
60
92
  readPreference?: ReadPreference
61
93
  ): ServerSelector {
62
94
  // If server version < 5.0, read preference always primary.
@@ -113,7 +145,7 @@ function maxStalenessReducer(
113
145
  primaryFilter
114
146
  )[0];
115
147
 
116
- return servers.reduce((result: ServerDescription[], server: ServerDescription) => {
148
+ return servers.filter((server: ServerDescription) => {
117
149
  const stalenessMS =
118
150
  server.lastUpdateTime -
119
151
  server.lastWriteDate -
@@ -122,12 +154,8 @@ function maxStalenessReducer(
122
154
 
123
155
  const staleness = stalenessMS / 1000;
124
156
  const maxStalenessSeconds = readPreference.maxStalenessSeconds ?? 0;
125
- if (staleness <= maxStalenessSeconds) {
126
- result.push(server);
127
- }
128
-
129
- return result;
130
- }, []);
157
+ return staleness <= maxStalenessSeconds;
158
+ });
131
159
  }
132
160
 
133
161
  if (topologyDescription.type === TopologyType.ReplicaSetNoPrimary) {
@@ -139,40 +167,38 @@ function maxStalenessReducer(
139
167
  s.lastWriteDate > max.lastWriteDate ? s : max
140
168
  );
141
169
 
142
- return servers.reduce((result: ServerDescription[], server: ServerDescription) => {
170
+ return servers.filter((server: ServerDescription) => {
143
171
  const stalenessMS =
144
172
  sMax.lastWriteDate - server.lastWriteDate + topologyDescription.heartbeatFrequencyMS;
145
173
 
146
174
  const staleness = stalenessMS / 1000;
147
175
  const maxStalenessSeconds = readPreference.maxStalenessSeconds ?? 0;
148
- if (staleness <= maxStalenessSeconds) {
149
- result.push(server);
150
- }
151
-
152
- return result;
153
- }, []);
176
+ return staleness <= maxStalenessSeconds;
177
+ });
154
178
  }
155
179
 
156
180
  return servers;
157
181
  }
158
182
 
159
183
  /**
160
- * Determines whether a server's tags match a given set of tags
184
+ * Determines whether a server's tags match a given set of tags.
185
+ *
186
+ * A tagset matches the server's tags if every k-v pair in the tagset
187
+ * is also in the server's tagset.
188
+ *
189
+ * Note that this does not requires that every k-v pair in the server's tagset is also
190
+ * in the client's tagset. The server's tagset is required only to be a superset of the
191
+ * client's tags.
192
+ *
193
+ * @see https://github.com/mongodb/specifications/blob/master/source/server-selection/server-selection.md#tag_sets
161
194
  *
162
195
  * @param tagSet - The requested tag set to match
163
196
  * @param serverTags - The server's tags
164
197
  */
165
198
  function tagSetMatch(tagSet: TagSet, serverTags: TagSet) {
166
- const keys = Object.keys(tagSet);
167
- const serverTagKeys = Object.keys(serverTags);
168
- for (let i = 0; i < keys.length; ++i) {
169
- const key = keys[i];
170
- if (serverTagKeys.indexOf(key) === -1 || serverTags[key] !== tagSet[key]) {
171
- return false;
172
- }
173
- }
174
-
175
- return true;
199
+ return Object.entries(tagSet).every(
200
+ ([key, value]) => serverTags[key] != null && serverTags[key] === value
201
+ );
176
202
  }
177
203
 
178
204
  /**
@@ -183,24 +209,17 @@ function tagSetMatch(tagSet: TagSet, serverTags: TagSet) {
183
209
  * @returns The list of servers matching the requested tags
184
210
  */
185
211
  function tagSetReducer(
186
- readPreference: ReadPreference,
212
+ { tags }: ReadPreference,
187
213
  servers: ServerDescription[]
188
214
  ): ServerDescription[] {
189
- if (
190
- readPreference.tags == null ||
191
- (Array.isArray(readPreference.tags) && readPreference.tags.length === 0)
192
- ) {
215
+ if (tags == null || tags.length === 0) {
216
+ // empty tag sets match all servers
193
217
  return servers;
194
218
  }
195
219
 
196
- for (let i = 0; i < readPreference.tags.length; ++i) {
197
- const tagSet = readPreference.tags[i];
198
- const serversMatchingTagset = servers.reduce(
199
- (matched: ServerDescription[], server: ServerDescription) => {
200
- if (tagSetMatch(tagSet, server.tags)) matched.push(server);
201
- return matched;
202
- },
203
- []
220
+ for (const tagSet of tags) {
221
+ const serversMatchingTagset = servers.filter((s: ServerDescription) =>
222
+ tagSetMatch(tagSet, s.tags)
204
223
  );
205
224
 
206
225
  if (serversMatchingTagset.length) {
@@ -231,10 +250,7 @@ function latencyWindowReducer(
231
250
  );
232
251
 
233
252
  const high = low + topologyDescription.localThresholdMS;
234
- return servers.reduce((result: ServerDescription[], server: ServerDescription) => {
235
- if (server.roundTripTime <= high && server.roundTripTime >= low) result.push(server);
236
- return result;
237
- }, []);
253
+ return servers.filter(server => server.roundTripTime <= high && server.roundTripTime >= low);
238
254
  }
239
255
 
240
256
  // filters
@@ -258,6 +274,107 @@ function loadBalancerFilter(server: ServerDescription): boolean {
258
274
  return server.type === ServerType.LoadBalancer;
259
275
  }
260
276
 
277
+ function isDeprioritizedFactory(
278
+ deprioritized: DeprioritizedServers
279
+ ): (server: ServerDescription) => boolean {
280
+ return server =>
281
+ // if any deprioritized servers equal the server, here we are.
282
+ !deprioritized.has(server);
283
+ }
284
+
285
+ function secondarySelector(
286
+ readPreference: ReadPreference,
287
+ topologyDescription: TopologyDescription,
288
+ servers: ServerDescription[],
289
+ deprioritized: DeprioritizedServers
290
+ ) {
291
+ const mode = readPreference.mode;
292
+ switch (mode) {
293
+ case 'primary':
294
+ // Note: no need to filter for deprioritized servers. A replica set has only one primary; that means that
295
+ // we are in one of two scenarios:
296
+ // 1. deprioritized servers is empty - return the primary.
297
+ // 2. deprioritized servers contains the primary - return the primary.
298
+ return servers.filter(primaryFilter);
299
+ case 'primaryPreferred': {
300
+ const primary = servers.filter(primaryFilter);
301
+
302
+ // If there is a primary and it is not deprioritized, use the primary. Otherwise,
303
+ // check for secondaries.
304
+ const eligiblePrimary = primary.filter(isDeprioritizedFactory(deprioritized));
305
+ if (eligiblePrimary.length) {
306
+ return eligiblePrimary;
307
+ }
308
+
309
+ // If we make it here, we either have:
310
+ // 1. a deprioritized primary
311
+ // 2. no eligible primary
312
+ // secondaries take precedence of deprioritized primaries.
313
+ const secondaries = tagSetReducer(
314
+ readPreference,
315
+ maxStalenessReducer(readPreference, topologyDescription, servers.filter(secondaryFilter))
316
+ );
317
+
318
+ const eligibleSecondaries = secondaries.filter(isDeprioritizedFactory(deprioritized));
319
+ if (eligibleSecondaries.length) {
320
+ return latencyWindowReducer(topologyDescription, eligibleSecondaries);
321
+ }
322
+
323
+ // if we make it here, we have no primaries or secondaries that not deprioritized.
324
+ // prefer the primary (which may not exist, if the topology has no primary).
325
+ // otherwise, return the secondaries (which also may not exist, but there is nothing else to check here).
326
+ return primary.length ? primary : latencyWindowReducer(topologyDescription, secondaries);
327
+ }
328
+ case 'nearest': {
329
+ const eligible = filterDeprioritized(
330
+ tagSetReducer(
331
+ readPreference,
332
+ maxStalenessReducer(readPreference, topologyDescription, servers.filter(nearestFilter))
333
+ ),
334
+ deprioritized
335
+ );
336
+ return latencyWindowReducer(topologyDescription, eligible);
337
+ }
338
+ case 'secondary':
339
+ case 'secondaryPreferred': {
340
+ const secondaries = tagSetReducer(
341
+ readPreference,
342
+ maxStalenessReducer(readPreference, topologyDescription, servers.filter(secondaryFilter))
343
+ );
344
+ const eligibleSecondaries = secondaries.filter(isDeprioritizedFactory(deprioritized));
345
+
346
+ if (eligibleSecondaries.length) {
347
+ return latencyWindowReducer(topologyDescription, eligibleSecondaries);
348
+ }
349
+
350
+ // we have no eligible secondaries, try for a primary if we can.
351
+ if (mode === ReadPreference.SECONDARY_PREFERRED) {
352
+ const primary = servers.filter(primaryFilter);
353
+
354
+ // unlike readPreference=primary, here we do filter for deprioritized servers.
355
+ // if the primary is deprioritized, deprioritized secondaries take precedence.
356
+ const eligiblePrimary = primary.filter(isDeprioritizedFactory(deprioritized));
357
+ if (eligiblePrimary.length) return eligiblePrimary;
358
+
359
+ // we have no eligible primary nor secondaries that have not been deprioritized
360
+ return secondaries.length
361
+ ? latencyWindowReducer(topologyDescription, secondaries)
362
+ : primary;
363
+ }
364
+
365
+ // return all secondaries in the latency window.
366
+ return latencyWindowReducer(topologyDescription, secondaries);
367
+ }
368
+
369
+ default: {
370
+ const _exhaustiveCheck: never = mode;
371
+ throw new MongoRuntimeError(
372
+ `unexpected readPreference=${mode} (should never happen). Please report a bug in the Node driver Jira project.`
373
+ );
374
+ }
375
+ }
376
+ }
377
+
261
378
  /**
262
379
  * Returns a function which selects servers based on a provided read preference
263
380
  *
@@ -271,53 +388,28 @@ export function readPreferenceServerSelector(readPreference: ReadPreference): Se
271
388
  return function readPreferenceServers(
272
389
  topologyDescription: TopologyDescription,
273
390
  servers: ServerDescription[],
274
- deprioritized: ServerDescription[] = []
391
+ deprioritized: DeprioritizedServers
275
392
  ): ServerDescription[] {
276
- if (topologyDescription.type === TopologyType.LoadBalanced) {
277
- return servers.filter(loadBalancerFilter);
278
- }
279
-
280
- if (topologyDescription.type === TopologyType.Unknown) {
281
- return [];
282
- }
283
-
284
- if (topologyDescription.type === TopologyType.Single) {
285
- return latencyWindowReducer(topologyDescription, servers.filter(knownFilter));
286
- }
287
-
288
- if (topologyDescription.type === TopologyType.Sharded) {
289
- const filtered = servers.filter(server => {
290
- return !deprioritized.includes(server);
291
- });
292
- const selectable = filtered.length > 0 ? filtered : deprioritized;
293
- return latencyWindowReducer(topologyDescription, selectable.filter(knownFilter));
294
- }
295
-
296
- const mode = readPreference.mode;
297
- if (mode === ReadPreference.PRIMARY) {
298
- return servers.filter(primaryFilter);
299
- }
300
-
301
- if (mode === ReadPreference.PRIMARY_PREFERRED) {
302
- const result = servers.filter(primaryFilter);
303
- if (result.length) {
304
- return result;
393
+ switch (topologyDescription.type) {
394
+ case 'Single':
395
+ return latencyWindowReducer(topologyDescription, servers.filter(knownFilter));
396
+ case 'ReplicaSetNoPrimary':
397
+ case 'ReplicaSetWithPrimary':
398
+ return secondarySelector(readPreference, topologyDescription, servers, deprioritized);
399
+ case 'Sharded': {
400
+ const selectable = filterDeprioritized(servers, deprioritized);
401
+ return latencyWindowReducer(topologyDescription, selectable.filter(knownFilter));
402
+ }
403
+ case 'Unknown':
404
+ return [];
405
+ case 'LoadBalanced':
406
+ return servers.filter(loadBalancerFilter);
407
+ default: {
408
+ const _exhaustiveCheck: never = topologyDescription.type;
409
+ throw new MongoRuntimeError(
410
+ `unexpected topology type: ${topologyDescription.type} (this should never happen). Please file a bug in the Node driver Jira project.`
411
+ );
305
412
  }
306
413
  }
307
-
308
- const filter = mode === ReadPreference.NEAREST ? nearestFilter : secondaryFilter;
309
- const selectedServers = latencyWindowReducer(
310
- topologyDescription,
311
- tagSetReducer(
312
- readPreference,
313
- maxStalenessReducer(readPreference, topologyDescription, servers.filter(filter))
314
- )
315
- );
316
-
317
- if (mode === ReadPreference.SECONDARY_PREFERRED && selectedServers.length === 0) {
318
- return servers.filter(primaryFilter);
319
- }
320
-
321
- return selectedServers;
322
414
  };
323
415
  }