mongodb 6.2.0 → 6.3.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 (54) hide show
  1. package/lib/cmap/command_monitoring_events.js +2 -2
  2. package/lib/cmap/command_monitoring_events.js.map +1 -1
  3. package/lib/cmap/commands.js +54 -11
  4. package/lib/cmap/commands.js.map +1 -1
  5. package/lib/cmap/connect.js +1 -1
  6. package/lib/cmap/connect.js.map +1 -1
  7. package/lib/cmap/connection.js +401 -3
  8. package/lib/cmap/connection.js.map +1 -1
  9. package/lib/cmap/errors.js +1 -1
  10. package/lib/cmap/errors.js.map +1 -1
  11. package/lib/cmap/message_stream.js +3 -10
  12. package/lib/cmap/message_stream.js.map +1 -1
  13. package/lib/cmap/wire_protocol/compression.js +57 -1
  14. package/lib/cmap/wire_protocol/compression.js.map +1 -1
  15. package/lib/connection_string.js +29 -3
  16. package/lib/connection_string.js.map +1 -1
  17. package/lib/error.js +4 -2
  18. package/lib/error.js.map +1 -1
  19. package/lib/gridfs/download.js.map +1 -1
  20. package/lib/gridfs/upload.js.map +1 -1
  21. package/lib/mongo_client.js +14 -0
  22. package/lib/mongo_client.js.map +1 -1
  23. package/lib/mongo_logger.js +16 -7
  24. package/lib/mongo_logger.js.map +1 -1
  25. package/lib/sdam/monitor.js +61 -38
  26. package/lib/sdam/monitor.js.map +1 -1
  27. package/lib/sdam/server.js +9 -13
  28. package/lib/sdam/server.js.map +1 -1
  29. package/lib/sdam/topology.js.map +1 -1
  30. package/lib/sessions.js +2 -2
  31. package/lib/sessions.js.map +1 -1
  32. package/lib/utils.js +15 -1
  33. package/lib/utils.js.map +1 -1
  34. package/mongodb.d.ts +52 -16
  35. package/package.json +2 -2
  36. package/src/cmap/command_monitoring_events.ts +5 -5
  37. package/src/cmap/commands.ts +65 -8
  38. package/src/cmap/connect.ts +1 -1
  39. package/src/cmap/connection.ts +543 -4
  40. package/src/cmap/errors.ts +1 -1
  41. package/src/cmap/message_stream.ts +7 -21
  42. package/src/cmap/wire_protocol/compression.ts +73 -0
  43. package/src/connection_string.ts +31 -3
  44. package/src/error.ts +6 -2
  45. package/src/gridfs/download.ts +4 -2
  46. package/src/gridfs/upload.ts +8 -2
  47. package/src/index.ts +13 -6
  48. package/src/mongo_client.ts +59 -2
  49. package/src/mongo_logger.ts +72 -12
  50. package/src/sdam/monitor.ts +76 -45
  51. package/src/sdam/server.ts +10 -15
  52. package/src/sdam/topology.ts +2 -0
  53. package/src/sessions.ts +3 -2
  54. package/src/utils.ts +17 -0
@@ -1,3 +1,5 @@
1
+ import { once } from 'events';
2
+ import { on } from 'stream';
1
3
  import { clearTimeout, setTimeout } from 'timers';
2
4
  import { promisify } from 'util';
3
5
 
@@ -18,6 +20,7 @@ import {
18
20
  MongoMissingDependencyError,
19
21
  MongoNetworkError,
20
22
  MongoNetworkTimeoutError,
23
+ MongoParseError,
21
24
  MongoRuntimeError,
22
25
  MongoServerError,
23
26
  MongoWriteConcernError
@@ -27,6 +30,7 @@ import { type CancellationToken, TypedEventEmitter } from '../mongo_types';
27
30
  import type { ReadPreferenceLike } from '../read_preference';
28
31
  import { applySession, type ClientSession, updateSessionFromResponse } from '../sessions';
29
32
  import {
33
+ BufferPool,
30
34
  calculateDurationInMs,
31
35
  type Callback,
32
36
  HostAddress,
@@ -43,11 +47,19 @@ import {
43
47
  CommandStartedEvent,
44
48
  CommandSucceededEvent
45
49
  } from './command_monitoring_events';
46
- import { type BinMsg, Msg, Query, type Response, type WriteProtocolMessageType } from './commands';
50
+ import {
51
+ OpCompressedRequest,
52
+ OpMsgRequest,
53
+ type OpMsgResponse,
54
+ OpQueryRequest,
55
+ type OpQueryResponse,
56
+ type WriteProtocolMessageType
57
+ } from './commands';
47
58
  import type { Stream } from './connect';
48
59
  import type { ClientMetadata } from './handshake/client_metadata';
49
60
  import { MessageStream, type OperationDescription } from './message_stream';
50
61
  import { StreamDescription, type StreamDescriptionOptions } from './stream_description';
62
+ import { decompressResponse } from './wire_protocol/compression';
51
63
  import { getReadPreference, isSharded } from './wire_protocol/shared';
52
64
 
53
65
  /** @internal */
@@ -324,7 +336,7 @@ export class Connection extends TypedEventEmitter<ConnectionEvents> {
324
336
  }, 1).unref(); // No need for this timer to hold the event loop open
325
337
  }
326
338
 
327
- onMessage(message: BinMsg | Response) {
339
+ onMessage(message: OpMsgResponse | OpQueryResponse) {
328
340
  const delayedTimeoutId = this[kDelayedTimeoutId];
329
341
  if (delayedTimeoutId != null) {
330
342
  clearTimeout(delayedTimeoutId);
@@ -540,8 +552,8 @@ export class Connection extends TypedEventEmitter<ConnectionEvents> {
540
552
  );
541
553
 
542
554
  const message = shouldUseOpMsg
543
- ? new Msg(ns.db, cmd, commandOptions)
544
- : new Query(ns.db, cmd, commandOptions);
555
+ ? new OpMsgRequest(ns.db, cmd, commandOptions)
556
+ : new OpQueryRequest(ns.db, cmd, commandOptions);
545
557
 
546
558
  try {
547
559
  write(this, message, commandOptions, callback);
@@ -757,3 +769,530 @@ function write(
757
769
  operationDescription.cb();
758
770
  }
759
771
  }
772
+
773
+ /** in-progress connection layer */
774
+
775
+ /** @internal */
776
+ export class ModernConnection extends TypedEventEmitter<ConnectionEvents> {
777
+ id: number | '<monitor>';
778
+ address: string;
779
+ socketTimeoutMS: number;
780
+ monitorCommands: boolean;
781
+ /** Indicates that the connection (including underlying TCP socket) has been closed. */
782
+ closed: boolean;
783
+ lastHelloMS?: number;
784
+ serverApi?: ServerApi;
785
+ helloOk?: boolean;
786
+ commandAsync: (
787
+ ns: MongoDBNamespace,
788
+ cmd: Document,
789
+ options: CommandOptions | undefined
790
+ ) => Promise<Document>;
791
+ /** @internal */
792
+ authContext?: AuthContext;
793
+
794
+ /**@internal */
795
+ [kDelayedTimeoutId]: NodeJS.Timeout | null;
796
+ /** @internal */
797
+ [kDescription]: StreamDescription;
798
+ /** @internal */
799
+ [kGeneration]: number;
800
+ /** @internal */
801
+ [kLastUseTime]: number;
802
+ /** @internal */
803
+ [kQueue]: Map<number, OperationDescription>;
804
+ /** @internal */
805
+ [kMessageStream]: MessageStream;
806
+ /** @internal */
807
+ socket: Stream;
808
+ /** @internal */
809
+ [kHello]: Document | null;
810
+ /** @internal */
811
+ [kClusterTime]: Document | null;
812
+
813
+ /** @event */
814
+ static readonly COMMAND_STARTED = COMMAND_STARTED;
815
+ /** @event */
816
+ static readonly COMMAND_SUCCEEDED = COMMAND_SUCCEEDED;
817
+ /** @event */
818
+ static readonly COMMAND_FAILED = COMMAND_FAILED;
819
+ /** @event */
820
+ static readonly CLUSTER_TIME_RECEIVED = CLUSTER_TIME_RECEIVED;
821
+ /** @event */
822
+ static readonly CLOSE = CLOSE;
823
+ /** @event */
824
+ static readonly MESSAGE = MESSAGE;
825
+ /** @event */
826
+ static readonly PINNED = PINNED;
827
+ /** @event */
828
+ static readonly UNPINNED = UNPINNED;
829
+
830
+ constructor(stream: Stream, options: ConnectionOptions) {
831
+ super();
832
+
833
+ this.commandAsync = promisify(
834
+ (
835
+ ns: MongoDBNamespace,
836
+ cmd: Document,
837
+ options: CommandOptions | undefined,
838
+ callback: Callback
839
+ ) => this.command(ns, cmd, options, callback as any)
840
+ );
841
+
842
+ this.id = options.id;
843
+ this.address = streamIdentifier(stream, options);
844
+ this.socketTimeoutMS = options.socketTimeoutMS ?? 0;
845
+ this.monitorCommands = options.monitorCommands;
846
+ this.serverApi = options.serverApi;
847
+ this.closed = false;
848
+ this[kHello] = null;
849
+ this[kClusterTime] = null;
850
+
851
+ this[kDescription] = new StreamDescription(this.address, options);
852
+ this[kGeneration] = options.generation;
853
+ this[kLastUseTime] = now();
854
+
855
+ // setup parser stream and message handling
856
+ this[kQueue] = new Map();
857
+ this[kMessageStream] = new MessageStream({
858
+ ...options,
859
+ maxBsonMessageSize: this.hello?.maxBsonMessageSize
860
+ });
861
+ this.socket = stream;
862
+
863
+ this[kDelayedTimeoutId] = null;
864
+
865
+ this[kMessageStream].on('message', message => this.onMessage(message));
866
+ this[kMessageStream].on('error', error => this.onError(error));
867
+ this.socket.on('close', () => this.onClose());
868
+ this.socket.on('timeout', () => this.onTimeout());
869
+ this.socket.on('error', () => {
870
+ /* ignore errors, listen to `close` instead */
871
+ });
872
+
873
+ // hook the message stream up to the passed in stream
874
+ this.socket.pipe(this[kMessageStream]);
875
+ this[kMessageStream].pipe(this.socket);
876
+ }
877
+
878
+ get description(): StreamDescription {
879
+ return this[kDescription];
880
+ }
881
+
882
+ get hello(): Document | null {
883
+ return this[kHello];
884
+ }
885
+
886
+ // the `connect` method stores the result of the handshake hello on the connection
887
+ set hello(response: Document | null) {
888
+ this[kDescription].receiveResponse(response);
889
+ this[kDescription] = Object.freeze(this[kDescription]);
890
+
891
+ // TODO: remove this, and only use the `StreamDescription` in the future
892
+ this[kHello] = response;
893
+ }
894
+
895
+ // Set the whether the message stream is for a monitoring connection.
896
+ set isMonitoringConnection(value: boolean) {
897
+ this[kMessageStream].isMonitoringConnection = value;
898
+ }
899
+
900
+ get isMonitoringConnection(): boolean {
901
+ return this[kMessageStream].isMonitoringConnection;
902
+ }
903
+
904
+ get serviceId(): ObjectId | undefined {
905
+ return this.hello?.serviceId;
906
+ }
907
+
908
+ get loadBalanced(): boolean {
909
+ return this.description.loadBalanced;
910
+ }
911
+
912
+ get generation(): number {
913
+ return this[kGeneration] || 0;
914
+ }
915
+
916
+ set generation(generation: number) {
917
+ this[kGeneration] = generation;
918
+ }
919
+
920
+ get idleTime(): number {
921
+ return calculateDurationInMs(this[kLastUseTime]);
922
+ }
923
+
924
+ get clusterTime(): Document | null {
925
+ return this[kClusterTime];
926
+ }
927
+
928
+ get stream(): Stream {
929
+ return this.socket;
930
+ }
931
+
932
+ get hasSessionSupport(): boolean {
933
+ return this.description.logicalSessionTimeoutMinutes != null;
934
+ }
935
+
936
+ get supportsOpMsg(): boolean {
937
+ return (
938
+ this.description != null &&
939
+ maxWireVersion(this as any as Connection) >= 6 &&
940
+ !this.description.__nodejs_mock_server__
941
+ );
942
+ }
943
+
944
+ markAvailable(): void {
945
+ this[kLastUseTime] = now();
946
+ }
947
+
948
+ onError(error: Error) {
949
+ this.cleanup(true, error);
950
+ }
951
+
952
+ onClose() {
953
+ const message = `connection ${this.id} to ${this.address} closed`;
954
+ this.cleanup(true, new MongoNetworkError(message));
955
+ }
956
+
957
+ onTimeout() {
958
+ this[kDelayedTimeoutId] = setTimeout(() => {
959
+ const message = `connection ${this.id} to ${this.address} timed out`;
960
+ const beforeHandshake = this.hello == null;
961
+ this.cleanup(true, new MongoNetworkTimeoutError(message, { beforeHandshake }));
962
+ }, 1).unref(); // No need for this timer to hold the event loop open
963
+ }
964
+
965
+ onMessage(message: OpMsgResponse | OpQueryResponse) {
966
+ const delayedTimeoutId = this[kDelayedTimeoutId];
967
+ if (delayedTimeoutId != null) {
968
+ clearTimeout(delayedTimeoutId);
969
+ this[kDelayedTimeoutId] = null;
970
+ }
971
+
972
+ const socketTimeoutMS = this.socket.timeout ?? 0;
973
+ this.socket.setTimeout(0);
974
+
975
+ // always emit the message, in case we are streaming
976
+ this.emit('message', message);
977
+ let operationDescription = this[kQueue].get(message.responseTo);
978
+
979
+ if (!operationDescription && this.isMonitoringConnection) {
980
+ // This is how we recover when the initial hello's requestId is not
981
+ // the responseTo when hello responses have been skipped:
982
+
983
+ // First check if the map is of invalid size
984
+ if (this[kQueue].size > 1) {
985
+ this.cleanup(true, new MongoRuntimeError(INVALID_QUEUE_SIZE));
986
+ } else {
987
+ // Get the first orphaned operation description.
988
+ const entry = this[kQueue].entries().next();
989
+ if (entry.value != null) {
990
+ const [requestId, orphaned]: [number, OperationDescription] = entry.value;
991
+ // If the orphaned operation description exists then set it.
992
+ operationDescription = orphaned;
993
+ // Remove the entry with the bad request id from the queue.
994
+ this[kQueue].delete(requestId);
995
+ }
996
+ }
997
+ }
998
+
999
+ if (!operationDescription) {
1000
+ return;
1001
+ }
1002
+
1003
+ const callback = operationDescription.cb;
1004
+
1005
+ // SERVER-45775: For exhaust responses we should be able to use the same requestId to
1006
+ // track response, however the server currently synthetically produces remote requests
1007
+ // making the `responseTo` change on each response
1008
+ this[kQueue].delete(message.responseTo);
1009
+ if ('moreToCome' in message && message.moreToCome) {
1010
+ // If the operation description check above does find an orphaned
1011
+ // description and sets the operationDescription then this line will put one
1012
+ // back in the queue with the correct requestId and will resolve not being able
1013
+ // to find the next one via the responseTo of the next streaming hello.
1014
+ this[kQueue].set(message.requestId, operationDescription);
1015
+ this.socket.setTimeout(socketTimeoutMS);
1016
+ }
1017
+
1018
+ try {
1019
+ // Pass in the entire description because it has BSON parsing options
1020
+ message.parse(operationDescription);
1021
+ } catch (err) {
1022
+ // If this error is generated by our own code, it will already have the correct class applied
1023
+ // if it is not, then it is coming from a catastrophic data parse failure or the BSON library
1024
+ // in either case, it should not be wrapped
1025
+ callback(err);
1026
+ return;
1027
+ }
1028
+
1029
+ if (message.documents[0]) {
1030
+ const document: Document = message.documents[0];
1031
+ const session = operationDescription.session;
1032
+ if (session) {
1033
+ updateSessionFromResponse(session, document);
1034
+ }
1035
+
1036
+ if (document.$clusterTime) {
1037
+ this[kClusterTime] = document.$clusterTime;
1038
+ this.emit(Connection.CLUSTER_TIME_RECEIVED, document.$clusterTime);
1039
+ }
1040
+
1041
+ if (document.writeConcernError) {
1042
+ callback(new MongoWriteConcernError(document.writeConcernError, document), document);
1043
+ return;
1044
+ }
1045
+
1046
+ if (document.ok === 0 || document.$err || document.errmsg || document.code) {
1047
+ callback(new MongoServerError(document));
1048
+ return;
1049
+ }
1050
+ }
1051
+
1052
+ callback(undefined, message.documents[0]);
1053
+ }
1054
+
1055
+ destroy(options: DestroyOptions, callback?: Callback): void {
1056
+ if (this.closed) {
1057
+ process.nextTick(() => callback?.());
1058
+ return;
1059
+ }
1060
+ if (typeof callback === 'function') {
1061
+ this.once('close', () => process.nextTick(() => callback()));
1062
+ }
1063
+
1064
+ // load balanced mode requires that these listeners remain on the connection
1065
+ // after cleanup on timeouts, errors or close so we remove them before calling
1066
+ // cleanup.
1067
+ this.removeAllListeners(Connection.PINNED);
1068
+ this.removeAllListeners(Connection.UNPINNED);
1069
+ const message = `connection ${this.id} to ${this.address} closed`;
1070
+ this.cleanup(options.force, new MongoNetworkError(message));
1071
+ }
1072
+
1073
+ /**
1074
+ * A method that cleans up the connection. When `force` is true, this method
1075
+ * forcibly destroys the socket.
1076
+ *
1077
+ * If an error is provided, any in-flight operations will be closed with the error.
1078
+ *
1079
+ * This method does nothing if the connection is already closed.
1080
+ */
1081
+ private cleanup(force: boolean, error?: Error): void {
1082
+ if (this.closed) {
1083
+ return;
1084
+ }
1085
+
1086
+ this.closed = true;
1087
+
1088
+ const completeCleanup = () => {
1089
+ for (const op of this[kQueue].values()) {
1090
+ op.cb(error);
1091
+ }
1092
+
1093
+ this[kQueue].clear();
1094
+
1095
+ this.emit(Connection.CLOSE);
1096
+ };
1097
+
1098
+ this.socket.removeAllListeners();
1099
+ this[kMessageStream].removeAllListeners();
1100
+
1101
+ this[kMessageStream].destroy();
1102
+
1103
+ if (force) {
1104
+ this.socket.destroy();
1105
+ completeCleanup();
1106
+ return;
1107
+ }
1108
+
1109
+ if (!this.socket.writableEnded) {
1110
+ this.socket.end(() => {
1111
+ this.socket.destroy();
1112
+ completeCleanup();
1113
+ });
1114
+ } else {
1115
+ completeCleanup();
1116
+ }
1117
+ }
1118
+
1119
+ command(
1120
+ ns: MongoDBNamespace,
1121
+ command: Document,
1122
+ options: CommandOptions | undefined,
1123
+ callback: Callback
1124
+ ): void {
1125
+ let cmd = { ...command };
1126
+
1127
+ const readPreference = getReadPreference(options);
1128
+ const session = options?.session;
1129
+
1130
+ let clusterTime = this.clusterTime;
1131
+
1132
+ if (this.serverApi) {
1133
+ const { version, strict, deprecationErrors } = this.serverApi;
1134
+ cmd.apiVersion = version;
1135
+ if (strict != null) cmd.apiStrict = strict;
1136
+ if (deprecationErrors != null) cmd.apiDeprecationErrors = deprecationErrors;
1137
+ }
1138
+
1139
+ if (this.hasSessionSupport && session) {
1140
+ if (
1141
+ session.clusterTime &&
1142
+ clusterTime &&
1143
+ session.clusterTime.clusterTime.greaterThan(clusterTime.clusterTime)
1144
+ ) {
1145
+ clusterTime = session.clusterTime;
1146
+ }
1147
+
1148
+ const err = applySession(session, cmd, options);
1149
+ if (err) {
1150
+ return callback(err);
1151
+ }
1152
+ } else if (session?.explicit) {
1153
+ return callback(new MongoCompatibilityError('Current topology does not support sessions'));
1154
+ }
1155
+
1156
+ // if we have a known cluster time, gossip it
1157
+ if (clusterTime) {
1158
+ cmd.$clusterTime = clusterTime;
1159
+ }
1160
+
1161
+ if (
1162
+ // @ts-expect-error ModernConnections cannot be passed as connections
1163
+ isSharded(this) &&
1164
+ !this.supportsOpMsg &&
1165
+ readPreference &&
1166
+ readPreference.mode !== 'primary'
1167
+ ) {
1168
+ cmd = {
1169
+ $query: cmd,
1170
+ $readPreference: readPreference.toJSON()
1171
+ };
1172
+ }
1173
+
1174
+ const commandOptions: Document = Object.assign(
1175
+ {
1176
+ numberToSkip: 0,
1177
+ numberToReturn: -1,
1178
+ checkKeys: false,
1179
+ // This value is not overridable
1180
+ secondaryOk: readPreference.secondaryOk()
1181
+ },
1182
+ options
1183
+ );
1184
+
1185
+ const message = this.supportsOpMsg
1186
+ ? new OpMsgRequest(ns.db, cmd, commandOptions)
1187
+ : new OpQueryRequest(ns.db, cmd, commandOptions);
1188
+
1189
+ try {
1190
+ write(this as any as Connection, message, commandOptions, callback);
1191
+ } catch (err) {
1192
+ callback(err);
1193
+ }
1194
+ }
1195
+ }
1196
+
1197
+ const kDefaultMaxBsonMessageSize = 1024 * 1024 * 16 * 4;
1198
+
1199
+ /**
1200
+ * @internal
1201
+ *
1202
+ * This helper reads chucks of data out of a socket and buffers them until it has received a
1203
+ * full wire protocol message.
1204
+ *
1205
+ * By itself, produces an infinite async generator of wire protocol messages and consumers must end
1206
+ * the stream by calling `return` on the generator.
1207
+ *
1208
+ * Note that `for-await` loops call `return` automatically when the loop is exited.
1209
+ */
1210
+ export async function* readWireProtocolMessages(
1211
+ connection: ModernConnection
1212
+ ): AsyncGenerator<Buffer> {
1213
+ const bufferPool = new BufferPool();
1214
+ const maxBsonMessageSize = connection.hello?.maxBsonMessageSize ?? kDefaultMaxBsonMessageSize;
1215
+ for await (const [chunk] of on(connection.socket, 'data')) {
1216
+ bufferPool.append(chunk);
1217
+ const sizeOfMessage = bufferPool.getInt32();
1218
+
1219
+ if (sizeOfMessage == null) {
1220
+ continue;
1221
+ }
1222
+
1223
+ if (sizeOfMessage < 0) {
1224
+ throw new MongoParseError(`Invalid message size: ${sizeOfMessage}`);
1225
+ }
1226
+
1227
+ if (sizeOfMessage > maxBsonMessageSize) {
1228
+ throw new MongoParseError(
1229
+ `Invalid message size: ${sizeOfMessage}, max allowed: ${maxBsonMessageSize}`
1230
+ );
1231
+ }
1232
+
1233
+ if (sizeOfMessage > bufferPool.length) {
1234
+ continue;
1235
+ }
1236
+
1237
+ yield bufferPool.read(sizeOfMessage);
1238
+ }
1239
+ }
1240
+
1241
+ /**
1242
+ * @internal
1243
+ *
1244
+ * Writes an OP_MSG or OP_QUERY request to the socket, optionally compressing the command. This method
1245
+ * waits until the socket's buffer has emptied (the Nodejs socket `drain` event has fired).
1246
+ */
1247
+ export async function writeCommand(
1248
+ connection: ModernConnection,
1249
+ command: WriteProtocolMessageType,
1250
+ options: Partial<Pick<OperationDescription, 'agreedCompressor' | 'zlibCompressionLevel'>>
1251
+ ): Promise<void> {
1252
+ const drained = once(connection.socket, 'drain');
1253
+ const finalCommand =
1254
+ options.agreedCompressor === 'none' || !OpCompressedRequest.canCompress(command)
1255
+ ? command
1256
+ : new OpCompressedRequest(command, {
1257
+ agreedCompressor: options.agreedCompressor ?? 'none',
1258
+ zlibCompressionLevel: options.zlibCompressionLevel ?? 0
1259
+ });
1260
+ const buffer = Buffer.concat(await finalCommand.toBin());
1261
+ connection.socket.push(buffer);
1262
+ await drained;
1263
+ }
1264
+
1265
+ /**
1266
+ * @internal
1267
+ *
1268
+ * Returns an async generator that yields full wire protocol messages from the underlying socket. This function
1269
+ * yields messages until `moreToCome` is false or not present in a response, or the caller cancels the request
1270
+ * by calling `return` on the generator.
1271
+ *
1272
+ * Note that `for-await` loops call `return` automatically when the loop is exited.
1273
+ */
1274
+ export async function* readMany(
1275
+ connection: ModernConnection
1276
+ ): AsyncGenerator<OpMsgResponse | OpQueryResponse> {
1277
+ for await (const message of readWireProtocolMessages(connection)) {
1278
+ const response = await decompressResponse(message);
1279
+ yield response;
1280
+
1281
+ if (!('moreToCome' in response) || !response.moreToCome) {
1282
+ return;
1283
+ }
1284
+ }
1285
+ }
1286
+
1287
+ /**
1288
+ * @internal
1289
+ *
1290
+ * Reads a single wire protocol message out of a connection.
1291
+ */
1292
+ export async function read(connection: ModernConnection): Promise<OpMsgResponse | OpQueryResponse> {
1293
+ for await (const value of readMany(connection)) {
1294
+ return value;
1295
+ }
1296
+
1297
+ throw new MongoRuntimeError('unable to read message off of connection');
1298
+ }
@@ -56,7 +56,7 @@ export class PoolClearedError extends MongoNetworkError {
56
56
  super(errorMessage, pool.serverError ? { cause: pool.serverError } : undefined);
57
57
  this.address = pool.address;
58
58
 
59
- this.addErrorLabel(MongoErrorLabel.RetryableWriteError);
59
+ this.addErrorLabel(MongoErrorLabel.PoolRequstedRetry);
60
60
  }
61
61
 
62
62
  override get name(): string {
@@ -5,19 +5,13 @@ import { MongoDecompressionError, MongoParseError } from '../error';
5
5
  import type { ClientSession } from '../sessions';
6
6
  import { BufferPool, type Callback } from '../utils';
7
7
  import {
8
- BinMsg,
9
8
  type MessageHeader,
10
- Msg,
11
- Response,
9
+ OpCompressedRequest,
10
+ OpMsgResponse,
11
+ OpQueryResponse,
12
12
  type WriteProtocolMessageType
13
13
  } from './commands';
14
- import {
15
- compress,
16
- Compressor,
17
- type CompressorName,
18
- decompress,
19
- uncompressibleCommands
20
- } from './wire_protocol/compression';
14
+ import { compress, Compressor, type CompressorName, decompress } from './wire_protocol/compression';
21
15
  import { OP_COMPRESSED, OP_MSG } from './wire_protocol/constants';
22
16
 
23
17
  const MESSAGE_HEADER_SIZE = 16;
@@ -85,7 +79,7 @@ export class MessageStream extends Duplex {
85
79
  operationDescription: OperationDescription
86
80
  ): void {
87
81
  const agreedCompressor = operationDescription.agreedCompressor ?? 'none';
88
- if (agreedCompressor === 'none' || !canCompress(command)) {
82
+ if (agreedCompressor === 'none' || !OpCompressedRequest.canCompress(command)) {
89
83
  const data = command.toBin();
90
84
  this.push(Array.isArray(data) ? Buffer.concat(data) : data);
91
85
  return;
@@ -128,14 +122,6 @@ export class MessageStream extends Duplex {
128
122
  }
129
123
  }
130
124
 
131
- // Return whether a command contains an uncompressible command term
132
- // Will return true if command contains no uncompressible command terms
133
- function canCompress(command: WriteProtocolMessageType) {
134
- const commandDoc = command instanceof Msg ? command.command : command.query;
135
- const commandName = Object.keys(commandDoc)[0];
136
- return !uncompressibleCommands.has(commandName);
137
- }
138
-
139
125
  function processIncomingData(stream: MessageStream, callback: Callback<Buffer>): void {
140
126
  const buffer = stream[kBuffer];
141
127
  const sizeOfMessage = buffer.getInt32();
@@ -179,7 +165,7 @@ function processIncomingData(stream: MessageStream, callback: Callback<Buffer>):
179
165
  return false;
180
166
  };
181
167
 
182
- let ResponseType = messageHeader.opCode === OP_MSG ? BinMsg : Response;
168
+ let ResponseType = messageHeader.opCode === OP_MSG ? OpMsgResponse : OpQueryResponse;
183
169
  if (messageHeader.opCode !== OP_COMPRESSED) {
184
170
  const messageBody = message.subarray(MESSAGE_HEADER_SIZE);
185
171
 
@@ -205,7 +191,7 @@ function processIncomingData(stream: MessageStream, callback: Callback<Buffer>):
205
191
  const compressedBuffer = message.slice(MESSAGE_HEADER_SIZE + 9);
206
192
 
207
193
  // recalculate based on wrapped opcode
208
- ResponseType = messageHeader.opCode === OP_MSG ? BinMsg : Response;
194
+ ResponseType = messageHeader.opCode === OP_MSG ? OpMsgResponse : OpQueryResponse;
209
195
  decompress(compressorID, compressedBuffer).then(
210
196
  messageBody => {
211
197
  if (messageBody.length !== messageHeader.length) {