From ec5d24cebc63820e0ae4115be4850e6c71c7344a Mon Sep 17 00:00:00 2001 From: Sergey Zelenov Date: Thu, 29 Oct 2026 16:53:58 +0100 Subject: [PATCH 01/11] feat(NODE-7958): make timeoutMS a no-op and deprecate it --- src/change_stream.ts | 13 +--- .../client_encryption.ts | 29 +------ src/client-side-encryption/kms_options.ts | 1 + src/collection.ts | 59 +------------- src/connection_string.ts | 11 ++- src/cursor/abstract_cursor.ts | 60 ++++----------- src/cursor/aggregation_cursor.ts | 17 +--- src/cursor/find_cursor.ts | 19 +---- src/cursor/run_command_cursor.ts | 4 +- src/db.ts | 61 +-------------- src/explain.ts | 8 -- src/gridfs/download.ts | 10 +-- src/gridfs/index.ts | 24 +++++- src/gridfs/upload.ts | 13 +--- src/mongo_client.ts | 65 +--------------- src/operations/command.ts | 8 +- src/operations/operation.ts | 2 +- src/operations/run_command.ts | 2 +- src/sessions.ts | 77 ++++--------------- src/timeout.ts | 15 +--- src/utils.ts | 39 ++++------ ...tion.prose.28.kms_connect_callback.test.ts | 3 + .../client-side-encryption/driver.test.ts | 14 +++- ...ient_side_operations_timeout.prose.test.ts | 6 +- ...lient_side_operations_timeout.spec.test.ts | 10 ++- ...lient_side_operations_timeout.unit.test.ts | 5 ++ .../node_csot.test.ts | 6 +- .../crud/client_bulk_write.test.ts | 7 +- test/integration/crud/explain.test.ts | 3 +- .../crud/find_cursor_methods.test.ts | 4 +- .../node-specific/abstract_cursor.test.ts | 3 +- .../node-specific/auto_connect.test.ts | 5 ++ .../node-specific/timeout_ms_noop.test.ts | 63 +++++++++++++++ .../read-write-concern/write_concern.test.ts | 3 +- test/unit/assorted/uri_options.spec.test.ts | 14 +++- .../client_encryption.test.ts | 28 +------ test/unit/cursor/aggregation_cursor.test.ts | 6 +- test/unit/mongo_client.test.ts | 30 ++++---- test/unit/sessions.test.ts | 43 +++-------- test/unit/timeout.test.ts | 5 +- test/unit/utils.test.ts | 61 ++++++++++++++- 41 files changed, 316 insertions(+), 540 deletions(-) create mode 100644 test/integration/node-specific/timeout_ms_noop.test.ts diff --git a/src/change_stream.ts b/src/change_stream.ts index 189fe3ee6d7..a459dda4dca 100644 --- a/src/change_stream.ts +++ b/src/change_stream.ts @@ -20,7 +20,7 @@ import type { AggregateOptions } from './operations/aggregate'; import type { OperationParent } from './operations/command'; import { DeprioritizedServers } from './sdam/server_selection'; import type { ServerSessionId } from './sessions'; -import { CSOTTimeoutContext, type TimeoutContext } from './timeout'; +import { type TimeoutContext } from './timeout'; import { type AnyOptions, getTopology, type MongoDBNamespace, squashError } from './utils'; const CHANGE_DOMAIN_TYPES = { @@ -647,18 +647,14 @@ export class ChangeStream< this.pipeline = pipeline; this.options = { ...options }; - let serverSelectionTimeoutMS: number; delete this.options.writeConcern; if (parent instanceof Collection) { this.type = CHANGE_DOMAIN_TYPES.COLLECTION; - serverSelectionTimeoutMS = parent.s.db.client.options.serverSelectionTimeoutMS; } else if (parent instanceof Db) { this.type = CHANGE_DOMAIN_TYPES.DATABASE; - serverSelectionTimeoutMS = parent.client.options.serverSelectionTimeoutMS; } else if (parent instanceof MongoClient) { this.type = CHANGE_DOMAIN_TYPES.CLUSTER; - serverSelectionTimeoutMS = parent.options.serverSelectionTimeoutMS; } else { throw new MongoChangeStreamError( 'Parent provided to ChangeStream constructor must be an instance of Collection, Db, or MongoClient' @@ -690,13 +686,6 @@ export class ChangeStream< this.cursorStream?.removeAllListeners('data'); } }); - - if (this.options.timeoutMS != null) { - this.timeoutContext = new CSOTTimeoutContext({ - timeoutMS: this.options.timeoutMS, - serverSelectionTimeoutMS - }); - } } /** The cached resume token that is used to resume after the most recently returned change. */ diff --git a/src/client-side-encryption/client_encryption.ts b/src/client-side-encryption/client_encryption.ts index 884a9ed0bd1..28ed20f3280 100644 --- a/src/client-side-encryption/client_encryption.ts +++ b/src/client-side-encryption/client_encryption.ts @@ -987,34 +987,7 @@ export interface ClientEncryptionOptions { keyExpirationMS?: number; /** - * @experimental - * The timeout setting to be used for all the operations on ClientEncryption. - * - * When provided, `timeoutMS` is used as the timeout for each operation executed on - * the ClientEncryption object. For example: - * - * ```typescript - * const clientEncryption = new ClientEncryption(client, { - * timeoutMS: 1_000 - * kmsProviders: { local: { key: '' } } - * }); - * - * // `1_000` is used as the timeout for createDataKey call - * await clientEncryption.createDataKey('local'); - * ``` - * - * If `timeoutMS` is configured on the provided client, the client's `timeoutMS` value - * will be used unless `timeoutMS` is also provided as a client encryption option. - * - * ```typescript - * const client = new MongoClient('', { timeoutMS: 2_000 }); - * - * // timeoutMS is set to 1_000 on clientEncryption - * const clientEncryption = new ClientEncryption(client, { - * timeoutMS: 1_000 - * kmsProviders: { local: { key: '' } } - * }); - * ``` + * @deprecated `timeoutMS` has no effect and will be removed in a future release. */ timeoutMS?: number; } diff --git a/src/client-side-encryption/kms_options.ts b/src/client-side-encryption/kms_options.ts index 005d11548a4..a33c288b821 100644 --- a/src/client-side-encryption/kms_options.ts +++ b/src/client-side-encryption/kms_options.ts @@ -88,6 +88,7 @@ export type ClientEncryptionSocketOptions = Pick< export type KMSConnectCallback = (options: { host: string; port: number; + /** @deprecated `timeoutMS` has no effect and will be removed in a future release. */ timeoutMS?: number; signal: AbortSignal; }) => Promise; diff --git a/src/collection.ts b/src/collection.ts index 16617c0b715..855e6a8fc82 100644 --- a/src/collection.ts +++ b/src/collection.ts @@ -122,8 +122,8 @@ export interface CollectionOptions extends BSONSerializeOptions, WriteConcernOpt /** The preferred read preference (ReadPreference.PRIMARY, ReadPreference.PRIMARY_PREFERRED, ReadPreference.SECONDARY, ReadPreference.SECONDARY_PREFERRED, ReadPreference.NEAREST). */ readPreference?: ReadPreferenceLike; /** - * @experimental * Specifies the time an operation will run until it throws a timeout error + * @deprecated `timeoutMS` has no effect and will be removed in a future release. */ timeoutMS?: number; } @@ -277,8 +277,9 @@ export class Collection { this.s.collectionHint = normalizeHintField(v); } + /** @deprecated `timeoutMS` has no effect and will be removed in a future release. */ public get timeoutMS(): number | undefined { - return this.s.options.timeoutMS; + return undefined; } /** @@ -1170,60 +1171,6 @@ export class Collection { * expectType(change.fullDocument); * }); * ``` - * - * @remarks - * When `timeoutMS` is configured for a change stream, it will have different behaviour depending - * on whether the change stream is in iterator mode or emitter mode. In both cases, a change - * stream will time out if it does not receive a change event within `timeoutMS` of the last change - * event. - * - * Note that if a change stream is consistently timing out when watching a collection, database or - * client that is being changed, then this may be due to the server timing out before it can finish - * processing the existing oplog. To address this, restart the change stream with a higher - * `timeoutMS`. - * - * If the change stream times out the initial aggregate operation to establish the change stream on - * the server, then the client will close the change stream. If the getMore calls to the server - * time out, then the change stream will be left open, but will throw a MongoOperationTimeoutError - * when in iterator mode and emit an error event that returns a MongoOperationTimeoutError in - * emitter mode. - * - * To determine whether or not the change stream is still open following a timeout, check the - * {@link ChangeStream.closed} getter. - * - * @example - * In iterator mode, if a next() call throws a timeout error, it will attempt to resume the change stream. - * The next call can just be retried after this succeeds. - * ```ts - * const changeStream = collection.watch([], { timeoutMS: 100 }); - * try { - * await changeStream.next(); - * } catch (e) { - * if (e instanceof MongoOperationTimeoutError && !changeStream.closed) { - * await changeStream.next(); - * } - * throw e; - * } - * ``` - * - * @example - * In emitter mode, if the change stream goes `timeoutMS` without emitting a change event, it will - * emit an error event that returns a MongoOperationTimeoutError, but will not close the change - * stream unless the resume attempt fails. There is no need to re-establish change listeners as - * this will automatically continue emitting change events once the resume attempt completes. - * - * ```ts - * const changeStream = collection.watch([], { timeoutMS: 100 }); - * changeStream.on('change', console.log); - * changeStream.on('error', e => { - * if (e instanceof MongoOperationTimeoutError && !changeStream.closed) { - * // do nothing - * } else { - * changeStream.close(); - * } - * }); - * ``` - * * @param pipeline - An array of {@link https://www.mongodb.com/docs/manual/reference/operator/aggregation-pipeline/|aggregation pipeline stages} through which to pass change stream documents. This allows for filtering (using $match) and manipulating the change stream documents. * @param options - Optional settings for the command * @typeParam TLocal - Type of the data being detected by the change stream diff --git a/src/connection_string.ts b/src/connection_string.ts index e1c74927873..0ffaacb783e 100644 --- a/src/connection_string.ts +++ b/src/connection_string.ts @@ -380,12 +380,14 @@ export function parseOptions( setOption(mongoOptions, key, descriptor, [DEFAULT_OPTIONS.get(key)]); } } else { - const { deprecated } = descriptor; + const { deprecated, ignored } = descriptor; if (deprecated) { const deprecatedMsg = typeof deprecated === 'string' ? `: ${deprecated}` : ''; emitWarning(`${key} is a deprecated option${deprecatedMsg}`); } + if (ignored) continue; + setOption(mongoOptions, key, descriptor, values); } } @@ -638,6 +640,8 @@ interface OptionDescriptor { default?: any; deprecated?: boolean | string; + /** The option is accepted but has no effect, so it is never stored on the resolved options. */ + ignored?: boolean; /** * @param name - the original option name * @param options - the options so far for resolution @@ -1105,7 +1109,6 @@ export const OPTIONS = { type: 'string' }, socketTimeoutMS: { - // TODO(NODE-6491): deprecated: 'Please use timeoutMS instead', default: 0, type: 'uint' }, @@ -1122,7 +1125,8 @@ export const OPTIONS = { type: 'boolean' }, timeoutMS: { - type: 'uint' + deprecated: 'timeoutMS has no effect and will be removed in a future release', + ignored: true }, tls: { type: 'boolean' @@ -1176,7 +1180,6 @@ export const OPTIONS = { } }, waitQueueTimeoutMS: { - // TODO(NODE-6491): deprecated: 'Please use timeoutMS instead', default: 0, type: 'uint' }, diff --git a/src/cursor/abstract_cursor.ts b/src/cursor/abstract_cursor.ts index f5f0b14d99f..5d302608d22 100644 --- a/src/cursor/abstract_cursor.ts +++ b/src/cursor/abstract_cursor.ts @@ -67,7 +67,6 @@ function removeActiveCursor(this: AbstractCursor) { /** * @public - * @experimental * Specifies how `timeoutMS` is applied to the cursor. Can be either `'cursorLifeTime'` or `'iteration'` * When set to `'iteration'`, the deadline specified by `timeoutMS` applies to each call of * `cursor.next()`. @@ -93,6 +92,7 @@ function removeActiveCursor(this: AbstractCursor) { * const cursor = collection.find({}, { timeoutMS: 1000, timeoutMode: 'cursorLifetime' }); * const docs = await cursor.toArray(); // This entire line will throw a timeout error if all batches are not fetched and returned within 1000ms. * ``` + * @deprecated `timeoutMode` has no effect and will be removed in a future release. */ export const CursorTimeoutMode = Object.freeze({ ITERATION: 'iteration', @@ -101,7 +101,7 @@ export const CursorTimeoutMode = Object.freeze({ /** * @public - * @experimental + * @deprecated `timeoutMode` has no effect and will be removed in a future release. */ export type CursorTimeoutMode = (typeof CursorTimeoutMode)[keyof typeof CursorTimeoutMode]; @@ -143,11 +143,13 @@ export interface AbstractCursorOptions extends BSONSerializeOptions { */ awaitData?: boolean; noCursorTimeout?: boolean; - /** Specifies the time an operation will run until it throws a timeout error. See {@link AbstractCursorOptions.timeoutMode} for more details on how this option applies to cursors. */ + /** + * Specifies the time an operation will run until it throws a timeout error. + * @deprecated `timeoutMS` has no effect and will be removed in a future release. + */ timeoutMS?: number; /** * @public - * @experimental * Specifies how `timeoutMS` is applied to the cursor. Can be either `'cursorLifeTime'` or `'iteration'` * When set to `'iteration'`, the deadline specified by `timeoutMS` applies to each call of * `cursor.next()`. @@ -173,6 +175,7 @@ export interface AbstractCursorOptions extends BSONSerializeOptions { * const cursor = collection.find({}, { timeoutMS: 1000, timeoutMode: 'cursorLifetime' }); * const docs = await cursor.toArray(); // This entire line will throw a timeout error if all batches are not fetched and returned within 1000ms. * ``` + * @deprecated `timeoutMode` has no effect and will be removed in a future release. */ timeoutMode?: CursorTimeoutMode; @@ -273,50 +276,10 @@ export abstract class AbstractCursor< ? options.readPreference : ReadPreference.primary, ...pluckBSONSerializeOptions(options), - timeoutMS: options?.timeoutContext?.csotEnabled() - ? options.timeoutContext.timeoutMS - : options.timeoutMS, tailable: options.tailable, awaitData: options.awaitData }; - if (this.cursorOptions.timeoutMS != null) { - if (options.timeoutMode == null) { - if (options.tailable) { - if (options.awaitData) { - if ( - options.maxAwaitTimeMS != null && - options.maxAwaitTimeMS >= this.cursorOptions.timeoutMS - ) - throw new MongoInvalidArgumentError( - 'Cannot specify maxAwaitTimeMS >= timeoutMS for a tailable awaitData cursor' - ); - } - - this.cursorOptions.timeoutMode = CursorTimeoutMode.ITERATION; - } else { - this.cursorOptions.timeoutMode = CursorTimeoutMode.LIFETIME; - } - } else { - if (options.tailable && options.timeoutMode === CursorTimeoutMode.LIFETIME) { - throw new MongoInvalidArgumentError( - "Cannot set tailable cursor's timeoutMode to LIFETIME" - ); - } - this.cursorOptions.timeoutMode = options.timeoutMode; - } - } else { - if (options.timeoutMode != null) - throw new MongoInvalidArgumentError('Cannot set timeoutMode without setting timeoutMS'); - } - - // Set for initial command - this.cursorOptions.omitMaxTimeMS = - this.cursorOptions.timeoutMS != null && - ((this.cursorOptions.timeoutMode === CursorTimeoutMode.ITERATION && - !this.cursorOptions.tailable) || - (this.cursorOptions.tailable && !this.cursorOptions.awaitData)); - const readConcern = ReadConcern.fromOptions(options); if (readConcern) { this.cursorOptions.readConcern = readConcern; @@ -635,9 +598,14 @@ export abstract class AbstractCursor< /** * Frees any client-side resources used by the cursor. + * + * @param _options - `timeoutMS` is deprecated and has no effect */ - async close(options?: { timeoutMS?: number }): Promise { - await this.cleanup(options?.timeoutMS); + async close(_options?: { + /** @deprecated `timeoutMS` has no effect and will be removed in a future release. */ + timeoutMS?: number; + }): Promise { + await this.cleanup(); } /** diff --git a/src/cursor/aggregation_cursor.ts b/src/cursor/aggregation_cursor.ts index 07201e1c304..830320c289c 100644 --- a/src/cursor/aggregation_cursor.ts +++ b/src/cursor/aggregation_cursor.ts @@ -1,11 +1,6 @@ import type { Document } from '../bson'; import { MongoAPIError } from '../error'; -import { - Explain, - type ExplainCommandOptions, - type ExplainVerbosityLike, - validateExplainTimeoutOptions -} from '../explain'; +import { type ExplainCommandOptions, type ExplainVerbosityLike } from '../explain'; import type { MongoClient } from '../mongo_client'; import { type Abortable } from '../mongo_types'; import { AggregateOperation, type AggregateOptions } from '../operations/aggregate'; @@ -77,16 +72,6 @@ export class AggregationCursor extends ExplainableCursor session, signal: this.signal }; - if (options.explain) { - try { - validateExplainTimeoutOptions(options, Explain.fromOptions(options)); - } catch { - throw new MongoAPIError( - 'timeoutMS cannot be used with explain when explain is specified in aggregateOptions' - ); - } - } - const aggregateOperation = new AggregateOperation(this.namespace, this.pipeline, options); const response = await executeOperation(this.client, aggregateOperation, this.timeoutContext); diff --git a/src/cursor/find_cursor.ts b/src/cursor/find_cursor.ts index c66a2a3e45f..6dda40922aa 100644 --- a/src/cursor/find_cursor.ts +++ b/src/cursor/find_cursor.ts @@ -1,12 +1,7 @@ import { type Document } from '../bson'; import { CursorResponse } from '../cmap/wire_protocol/responses'; -import { MongoAPIError, MongoInvalidArgumentError, MongoTailableCursorError } from '../error'; -import { - Explain, - type ExplainCommandOptions, - type ExplainVerbosityLike, - validateExplainTimeoutOptions -} from '../explain'; +import { MongoInvalidArgumentError, MongoTailableCursorError } from '../error'; +import { type ExplainCommandOptions, type ExplainVerbosityLike } from '../explain'; import type { MongoClient } from '../mongo_client'; import { type Abortable } from '../mongo_types'; import type { CollationOptions } from '../operations/command'; @@ -77,16 +72,6 @@ export class FindCursor extends ExplainableCursor { signal: this.signal }; - if (options.explain) { - try { - validateExplainTimeoutOptions(options, Explain.fromOptions(options)); - } catch { - throw new MongoAPIError( - 'timeoutMS cannot be used with explain when explain is specified in findOptions' - ); - } - } - const findOperation = new FindOperation(this.namespace, this.cursorFilter, options); const response = await executeOperation(this.client, findOperation, this.timeoutContext); diff --git a/src/cursor/run_command_cursor.ts b/src/cursor/run_command_cursor.ts index 997fe1ffebc..f7d3b56cfb6 100644 --- a/src/cursor/run_command_cursor.ts +++ b/src/cursor/run_command_cursor.ts @@ -20,15 +20,14 @@ export type RunCursorCommandOptions = { readPreference?: ReadPreferenceLike; session?: ClientSession; /** - * @experimental * Specifies the time an operation will run until it throws a timeout error. Note that if * `maxTimeMS` is provided in the command in addition to setting `timeoutMS` in the options, then * the original value of `maxTimeMS` will be overwritten. + * @deprecated `timeoutMS` has no effect and will be removed in a future release. */ timeoutMS?: number; /** * @public - * @experimental * Specifies how `timeoutMS` is applied to the cursor. Can be either `'cursorLifeTime'` or `'iteration'` * When set to `'iteration'`, the deadline specified by `timeoutMS` applies to each call of * `cursor.next()`. @@ -54,6 +53,7 @@ export type RunCursorCommandOptions = { * const cursor = collection.find({}, { timeoutMS: 1000, timeoutMode: 'cursorLifetime' }); * const docs = await cursor.toArray(); // This entire line will throw a timeout error if all batches are not fetched and returned within 1000ms. * ``` + * @deprecated `timeoutMode` has no effect and will be removed in a future release. */ timeoutMode?: CursorTimeoutMode; tailable?: boolean; diff --git a/src/db.ts b/src/db.ts index 7aa21ac0888..50b8b14a19c 100644 --- a/src/db.ts +++ b/src/db.ts @@ -64,8 +64,7 @@ const DB_OPTIONS_ALLOW_LIST = [ 'enableUtf8Validation', 'promoteValues', 'compression', - 'retryWrites', - 'timeoutMS' + 'retryWrites' ]; /** @internal */ @@ -94,8 +93,8 @@ export interface DbOptions extends BSONSerializeOptions, WriteConcernOptions { /** Should retry failed writes */ retryWrites?: boolean; /** - * @experimental * Specifies the time an operation will run until it throws a timeout error + * @deprecated `timeoutMS` has no effect and will be removed in a future release. */ timeoutMS?: number; } @@ -224,8 +223,9 @@ export class Db { return this.s.namespace.toString(); } + /** @deprecated `timeoutMS` has no effect and will be removed in a future release. */ public get timeoutMS(): number | undefined { - return this.s.options?.timeoutMS; + return undefined; } /** @@ -542,59 +542,6 @@ export class Db { * watch() accepts two generic arguments for distinct use cases: * - The first is to provide the schema that may be defined for all the collections within this database * - The second is to override the shape of the change stream document entirely, if it is not provided the type will default to ChangeStreamDocument of the first argument - * - * @remarks - * When `timeoutMS` is configured for a change stream, it will have different behaviour depending - * on whether the change stream is in iterator mode or emitter mode. In both cases, a change - * stream will time out if it does not receive a change event within `timeoutMS` of the last change - * event. - * - * Note that if a change stream is consistently timing out when watching a collection, database or - * client that is being changed, then this may be due to the server timing out before it can finish - * processing the existing oplog. To address this, restart the change stream with a higher - * `timeoutMS`. - * - * If the change stream times out the initial aggregate operation to establish the change stream on - * the server, then the client will close the change stream. If the getMore calls to the server - * time out, then the change stream will be left open, but will throw a MongoOperationTimeoutError - * when in iterator mode and emit an error event that returns a MongoOperationTimeoutError in - * emitter mode. - * - * To determine whether or not the change stream is still open following a timeout, check the - * {@link ChangeStream.closed} getter. - * - * @example - * In iterator mode, if a next() call throws a timeout error, it will attempt to resume the change stream. - * The next call can just be retried after this succeeds. - * ```ts - * const changeStream = collection.watch([], { timeoutMS: 100 }); - * try { - * await changeStream.next(); - * } catch (e) { - * if (e instanceof MongoOperationTimeoutError && !changeStream.closed) { - * await changeStream.next(); - * } - * throw e; - * } - * ``` - * - * @example - * In emitter mode, if the change stream goes `timeoutMS` without emitting a change event, it will - * emit an error event that returns a MongoOperationTimeoutError, but will not close the change - * stream unless the resume attempt fails. There is no need to re-establish change listeners as - * this will automatically continue emitting change events once the resume attempt completes. - * - * ```ts - * const changeStream = collection.watch([], { timeoutMS: 100 }); - * changeStream.on('change', console.log); - * changeStream.on('error', e => { - * if (e instanceof MongoOperationTimeoutError && !changeStream.closed) { - * // do nothing - * } else { - * changeStream.close(); - * } - * }); - * ``` * @param pipeline - An array of {@link https://www.mongodb.com/docs/manual/reference/operator/aggregation-pipeline/|aggregation pipeline stages} through which to pass change stream documents. This allows for filtering (using $match) and manipulating the change stream documents. * @param options - Optional settings for the command * @typeParam TSchema - Type of the data being detected by the change stream diff --git a/src/explain.ts b/src/explain.ts index 6100e1ae11a..8652cc404a4 100644 --- a/src/explain.ts +++ b/src/explain.ts @@ -1,5 +1,4 @@ import { type Document } from './bson'; -import { MongoAPIError } from './error'; /** @public */ export const ExplainVerbosity = Object.freeze({ @@ -90,13 +89,6 @@ export class Explain { } } -export function validateExplainTimeoutOptions(options: Document, explain?: Explain) { - const { maxTimeMS, timeoutMS } = options; - if (timeoutMS != null && (maxTimeMS != null || explain?.maxTimeMS != null)) { - throw new MongoAPIError('Cannot use maxTimeMS with timeoutMS for explain commands.'); - } -} - /** * Applies an explain to a given command. * @internal diff --git a/src/gridfs/download.ts b/src/gridfs/download.ts index 84e8890223c..9bd0ef226ae 100644 --- a/src/gridfs/download.ts +++ b/src/gridfs/download.ts @@ -13,7 +13,7 @@ import { import type { FindOptions } from '../operations/find'; import type { ReadPreference } from '../read_preference'; import type { Sort } from '../sort'; -import { CSOTTimeoutContext } from '../timeout'; +import { type CSOTTimeoutContext } from '../timeout'; import type { Callback } from '../utils'; import type { GridFSChunk } from './upload'; @@ -31,8 +31,8 @@ export interface GridFSBucketReadStreamOptions { */ end?: number; /** - * @experimental * Specifies the time an operation will run until it throws a timeout error + * @deprecated `timeoutMS` has no effect and will be removed in a future release. */ timeoutMS?: number; } @@ -151,11 +151,7 @@ export class GridFSBucketReadStream extends Readable { end: 0, ...options }, - readPreference, - timeoutContext: - options?.timeoutMS != null - ? new CSOTTimeoutContext({ timeoutMS: options.timeoutMS, serverSelectionTimeoutMS: 0 }) - : undefined + readPreference }; } diff --git a/src/gridfs/index.ts b/src/gridfs/index.ts index d1a97478aa4..08b2d49022d 100644 --- a/src/gridfs/index.ts +++ b/src/gridfs/index.ts @@ -39,9 +39,9 @@ export interface GridFSBucketOptions extends WriteConcernOptions { /** Read preference to be passed to read operations */ readPreference?: ReadPreference; /** - * @experimental * Specifies the lifetime duration of a gridFS stream. If any async operations are in progress * when this timeout expires, the stream will throw a timeout error. + * @deprecated `timeoutMS` has no effect and will be removed in a future release. */ timeoutMS?: number; } @@ -159,7 +159,13 @@ export class GridFSBucket extends TypedEventEmitter { * * @param id - The id of the file doc */ - async delete(id: ObjectId, options?: { timeoutMS: number }): Promise { + async delete( + id: ObjectId, + options?: { + /** @deprecated `timeoutMS` has no effect and will be removed in a future release. */ + timeoutMS?: number; + } + ): Promise { const { timeoutMS } = resolveOptions(this.s.db, options); let timeoutContext: CSOTTimeoutContext | undefined = undefined; @@ -232,7 +238,14 @@ export class GridFSBucket extends TypedEventEmitter { * @param id - the id of the file to rename * @param filename - new name for the file */ - async rename(id: ObjectId, filename: string, options?: { timeoutMS: number }): Promise { + async rename( + id: ObjectId, + filename: string, + options?: { + /** @deprecated `timeoutMS` has no effect and will be removed in a future release. */ + timeoutMS?: number; + } + ): Promise { const filter = { _id: { $eq: id } }; const update = { $set: { filename } }; const { matchedCount } = await this.s._filesCollection.updateOne(filter, update, options); @@ -242,7 +255,10 @@ export class GridFSBucket extends TypedEventEmitter { } /** Removes this bucket's files collection, followed by its chunks collection. */ - async drop(options?: { timeoutMS: number }): Promise { + async drop(options?: { + /** @deprecated `timeoutMS` has no effect and will be removed in a future release. */ + timeoutMS?: number; + }): Promise { const { timeoutMS } = resolveOptions(this.s.db, options); let timeoutContext: CSOTTimeoutContext | undefined = undefined; diff --git a/src/gridfs/upload.ts b/src/gridfs/upload.ts index 6c4b8e099f3..c6056cf3ef8 100644 --- a/src/gridfs/upload.ts +++ b/src/gridfs/upload.ts @@ -9,8 +9,8 @@ import { MongoError, MongoOperationTimeoutError } from '../error'; -import { CSOTTimeoutContext } from '../timeout'; -import { type Callback, resolveTimeoutOptions, squashError } from '../utils'; +import { type CSOTTimeoutContext } from '../timeout'; +import { type Callback, squashError } from '../utils'; import type { WriteConcernOptions } from '../write_concern'; import { WriteConcern } from './../write_concern'; import type { GridFSFile } from './download'; @@ -33,8 +33,8 @@ export interface GridFSBucketWriteStreamOptions extends WriteConcernOptions { /** Object to store in the file document's `metadata` field */ metadata?: Document; /** - * @experimental * Specifies the time an operation will run until it throws a timeout error + * @deprecated `timeoutMS` has no effect and will be removed in a future release. */ timeoutMS?: number; } @@ -132,13 +132,6 @@ export class GridFSBucketWriteStream extends Writable { errored: false, aborted: false }; - - if (options.timeoutMS != null) - this.timeoutContext = new CSOTTimeoutContext({ - timeoutMS: options.timeoutMS, - serverSelectionTimeoutMS: resolveTimeoutOptions(this.bucket.s.db.client, {}) - .serverSelectionTimeoutMS - }); } /** diff --git a/src/mongo_client.ts b/src/mongo_client.ts index f4c0cde2eb4..b879c861081 100644 --- a/src/mongo_client.ts +++ b/src/mongo_client.ts @@ -143,8 +143,8 @@ export interface MongoClientOptions extends BSONSerializeOptions, SupportedNodeC /** Specifies the name of the replica set, if the mongod is a member of a replica set. */ replicaSet?: string; /** - * @experimental * Specifies the time an operation will run until it throws a timeout error + * @deprecated `timeoutMS` has no effect and will be removed in a future release. */ timeoutMS?: number; /** Enables or disables TLS/SSL for the connection. */ @@ -562,8 +562,9 @@ export class MongoClient extends TypedEventEmitter implements return this.s.bsonOptions; } + /** @deprecated `timeoutMS` has no effect and will be removed in a future release. */ get timeoutMS(): number | undefined { - return this.s.options.timeoutMS; + return undefined; } /** @@ -874,12 +875,6 @@ export class MongoClient extends TypedEventEmitter implements * **Connection Pooling Impact:** Calling `connect` will populate the connection pool with one connection * to a server selected by the client's configured `readPreference` (defaults to primary). * - * @remarks - * **Timeout Behavior:** When using `timeoutMS`, the connection establishment time does not count against - * the timeout for subsequent operations. This means `connect` runs without a `timeoutMS` limit, while - * your database operations will still respect the configured timeout. If you need predictable operation - * timing with `timeoutMS`, call `connect` explicitly before performing operations. - * * @see https://www.mongodb.com/docs/manual/reference/connection-string/ */ static async connect(url: string, options?: MongoClientOptions): Promise { @@ -961,59 +956,6 @@ export class MongoClient extends TypedEventEmitter implements * watch() accepts two generic arguments for distinct use cases: * - The first is to provide the schema that may be defined for all the data within the current cluster * - The second is to override the shape of the change stream document entirely, if it is not provided the type will default to ChangeStreamDocument of the first argument - * - * @remarks - * When `timeoutMS` is configured for a change stream, it will have different behaviour depending - * on whether the change stream is in iterator mode or emitter mode. In both cases, a change - * stream will time out if it does not receive a change event within `timeoutMS` of the last change - * event. - * - * Note that if a change stream is consistently timing out when watching a collection, database or - * client that is being changed, then this may be due to the server timing out before it can finish - * processing the existing oplog. To address this, restart the change stream with a higher - * `timeoutMS`. - * - * If the change stream times out the initial aggregate operation to establish the change stream on - * the server, then the client will close the change stream. If the getMore calls to the server - * time out, then the change stream will be left open, but will throw a MongoOperationTimeoutError - * when in iterator mode and emit an error event that returns a MongoOperationTimeoutError in - * emitter mode. - * - * To determine whether or not the change stream is still open following a timeout, check the - * {@link ChangeStream.closed} getter. - * - * @example - * In iterator mode, if a next() call throws a timeout error, it will attempt to resume the change stream. - * The next call can just be retried after this succeeds. - * ```ts - * const changeStream = collection.watch([], { timeoutMS: 100 }); - * try { - * await changeStream.next(); - * } catch (e) { - * if (e instanceof MongoOperationTimeoutError && !changeStream.closed) { - * await changeStream.next(); - * } - * throw e; - * } - * ``` - * - * @example - * In emitter mode, if the change stream goes `timeoutMS` without emitting a change event, it will - * emit an error event that returns a MongoOperationTimeoutError, but will not close the change - * stream unless the resume attempt fails. There is no need to re-establish change listeners as - * this will automatically continue emitting change events once the resume attempt completes. - * - * ```ts - * const changeStream = collection.watch([], { timeoutMS: 100 }); - * changeStream.on('change', console.log); - * changeStream.on('error', e => { - * if (e instanceof MongoOperationTimeoutError && !changeStream.closed) { - * // do nothing - * } else { - * changeStream.close(); - * } - * }); - * ``` * @param pipeline - An array of {@link https://www.mongodb.com/docs/manual/reference/operator/aggregation-pipeline/|aggregation pipeline stages} through which to pass change stream documents. This allows for filtering (using $match) and manipulating the change stream documents. * @param options - Optional settings for the command * @typeParam TSchema - Type of the data being detected by the change stream @@ -1173,7 +1115,6 @@ export interface MongoOptions * TODO: NODE-5671 - remove internal flag */ mongodbLogPath?: 'stderr' | 'stdout' | MongoDBLogWritable; - timeoutMS?: number; /** @internal */ __skipPingOnConnect?: boolean; diff --git a/src/operations/command.ts b/src/operations/command.ts index ebaba8607ea..10fc22c734d 100644 --- a/src/operations/command.ts +++ b/src/operations/command.ts @@ -2,12 +2,7 @@ import { type Connection } from '..'; import type { BSONSerializeOptions, Document } from '../bson'; import { MIN_SUPPORTED_RAW_DATA_WIRE_VERSION } from '../cmap/wire_protocol/constants'; import { MongoInvalidArgumentError } from '../error'; -import { - decorateWithExplain, - Explain, - type ExplainOptions, - validateExplainTimeoutOptions -} from '../explain'; +import { decorateWithExplain, Explain, type ExplainOptions } from '../explain'; import { ReadConcern } from '../read_concern'; import type { ReadPreference } from '../read_preference'; import type { ServerCommandOptions } from '../sdam/server'; @@ -99,7 +94,6 @@ export abstract class CommandOperation extends AbstractOperation { if (this.hasAspect(Aspect.EXPLAINABLE)) { this.explain = Explain.fromOptions(options); - if (this.explain) validateExplainTimeoutOptions(this.options, this.explain); } else if (options?.explain != null) { throw new MongoInvalidArgumentError(`Option "explain" is not supported on this command`); } diff --git a/src/operations/operation.ts b/src/operations/operation.ts index 488afc96a88..5f44a471b53 100644 --- a/src/operations/operation.ts +++ b/src/operations/operation.ts @@ -39,8 +39,8 @@ export interface OperationOptions extends BSONSerializeOptions { omitMaxTimeMS?: boolean; /** - * @experimental * Specifies the time an operation will run until it throws a timeout error + * @deprecated `timeoutMS` has no effect and will be removed in a future release. */ timeoutMS?: number; } diff --git a/src/operations/run_command.ts b/src/operations/run_command.ts index 9f3e505f7e8..b9dc8ee536a 100644 --- a/src/operations/run_command.ts +++ b/src/operations/run_command.ts @@ -16,8 +16,8 @@ export type RunCommandOptions = { /** The read preference */ readPreference?: ReadPreferenceLike; /** - * @experimental * Specifies the time an operation will run until it throws a timeout error + * @deprecated `timeoutMS` has no effect and will be removed in a future release. */ timeoutMS?: number; /** @internal */ diff --git a/src/sessions.ts b/src/sessions.ts index 72c35257434..e9ba121d032 100644 --- a/src/sessions.ts +++ b/src/sessions.ts @@ -30,7 +30,7 @@ import { RunCommandOperation } from './operations/run_command'; import { ReadConcernLevel } from './read_concern'; import { ReadPreference } from './read_preference'; import { _advanceClusterTime, type ClusterTime, TopologyType } from './sdam/common'; -import { TimeoutContext } from './timeout'; +import { type TimeoutContext } from './timeout'; import { isTransactionCommand, Transaction, @@ -60,9 +60,7 @@ export interface ClientSessionOptions { defaultTransactionOptions?: TransactionOptions; /** * @public - * @experimental - * An overriding timeoutMS value to use for a client-side timeout. - * If not provided the session uses the timeoutMS specified on the MongoClient. + * @deprecated `timeoutMS` has no effect and will be removed in a future release. */ defaultTimeoutMS?: number; @@ -92,7 +90,7 @@ export interface EndSessionOptions { force?: boolean; forceClear?: boolean; - /** Specifies the time an operation will run until it throws a timeout error */ + /** @deprecated `timeoutMS` has no effect and will be removed in a future release. */ timeoutMS?: number; } @@ -138,8 +136,8 @@ export class ClientSession /** @internal */ public txnNumberIncrement: number; /** - * @experimental * Specifies the time an operation in a given `ClientSession` will run until it throws a timeout error + * @deprecated `timeoutMS` has no effect and will be removed in a future release. */ timeoutMS?: number; @@ -186,7 +184,6 @@ export class ClientSession this.sessionPool = sessionPool; this.hasEnded = false; this.clientOptions = clientOptions; - this.timeoutMS = options.defaultTimeoutMS ?? client.s.options?.timeoutMS; this.explicit = !!options.explicit; this._serverSession = this.explicit ? this.sessionPool.acquire() : null; @@ -424,9 +421,12 @@ export class ClientSession /** * Commits the currently active transaction in this session. * - * @param options - Optional options, can be used to override `defaultTimeoutMS`. + * @param _options - `timeoutMS` is deprecated and has no effect. */ - async commitTransaction(options?: { timeoutMS?: number }): Promise { + async commitTransaction(_options?: { + /** @deprecated `timeoutMS` has no effect and will be removed in a future release. */ + timeoutMS?: number; + }): Promise { if (this.transaction.state === TxnState.NO_TRANSACTION) { throw new MongoTransactionError('No transaction started'); } @@ -453,16 +453,9 @@ export class ClientSession maxTimeMS?: number; } = { commitTransaction: 1 }; - const timeoutMS = - typeof options?.timeoutMS === 'number' - ? options.timeoutMS - : typeof this.timeoutMS === 'number' - ? this.timeoutMS - : null; - const wc = this.transaction.options.writeConcern ?? this.clientOptions?.writeConcern; if (wc != null) { - if (timeoutMS == null && this.timeoutContext == null) { + if (this.timeoutContext == null) { WriteConcern.apply(command, { wtimeoutMS: 10000, w: 'majority', ...wc }); } else { const wcKeys = Object.keys(wc); @@ -477,7 +470,7 @@ export class ClientSession } if (this.transaction.state === TxnState.TRANSACTION_COMMITTED || this.commitAttempted) { - if (timeoutMS == null && this.timeoutContext == null) { + if (this.timeoutContext == null) { WriteConcern.apply(command, { wtimeoutMS: 10000, ...wc, w: 'majority' }); } else { WriteConcern.apply(command, { w: 'majority', ...wc, wtimeoutMS: undefined }); @@ -499,15 +492,7 @@ export class ClientSession }); operation.maxAttempts = this.clientOptions.maxAdaptiveRetries + 1; - const timeoutContext = - this.timeoutContext ?? - (typeof timeoutMS === 'number' - ? TimeoutContext.create({ - serverSelectionTimeoutMS: this.clientOptions.serverSelectionTimeoutMS, - socketTimeoutMS: this.clientOptions.socketTimeoutMS, - timeoutMS - }) - : null); + const timeoutContext = this.timeoutContext ?? null; try { await executeOperation(this.client, operation, timeoutContext); @@ -602,26 +587,10 @@ export class ClientSession recoveryToken?: Document; } = { abortTransaction: 1 }; - const timeoutMS = - typeof options?.timeoutMS === 'number' - ? options.timeoutMS - : this.timeoutContext?.csotEnabled() - ? this.timeoutContext.timeoutMS // refresh timeoutMS for abort operation - : typeof this.timeoutMS === 'number' - ? this.timeoutMS - : null; - - const timeoutContext = - timeoutMS != null - ? TimeoutContext.create({ - timeoutMS, - serverSelectionTimeoutMS: this.clientOptions.serverSelectionTimeoutMS, - socketTimeoutMS: this.clientOptions.socketTimeoutMS - }) - : null; + const timeoutContext = null; const wc = this.transaction.options.writeConcern ?? this.clientOptions?.writeConcern; - if (wc != null && timeoutMS == null) { + if (wc != null) { WriteConcern.apply(command, { wtimeoutMS: 10000, w: 'majority', ...wc }); } @@ -714,27 +683,13 @@ export class ClientSession async withTransaction( fn: WithTransactionCallback, options?: TransactionOptions & { - /** - * Configures a timeoutMS expiry for the entire withTransactionCallback. - * - * @remarks - * - The remaining timeout will not be applied to callback operations that do not use the ClientSession. - * - Overriding timeoutMS for operations executed using the explicit session inside the provided callback will result in a client-side error. - */ + /** @deprecated `timeoutMS` has no effect and will be removed in a future release. */ timeoutMS?: number; } ): Promise { const MAX_TIMEOUT = 120_000; - const timeoutMS = options?.timeoutMS ?? this.timeoutMS ?? null; - this.timeoutContext = - timeoutMS != null - ? TimeoutContext.create({ - timeoutMS, - serverSelectionTimeoutMS: this.clientOptions.serverSelectionTimeoutMS, - socketTimeoutMS: this.clientOptions.socketTimeoutMS - }) - : null; + // timeoutMS is a no-op, so a CSOT timeout context is never created // 1. Define the following: // 1.1 Record the current monotonic time, which will be used to enforce the 120-second / CSOT timeout before later retry attempts. diff --git a/src/timeout.ts b/src/timeout.ts index 26e04943f6a..4c3f2dbc5c7 100644 --- a/src/timeout.ts +++ b/src/timeout.ts @@ -150,23 +150,12 @@ function isLegacyTimeoutContextOptions(v: unknown): v is LegacyTimeoutContextOpt ); } -function isCSOTTimeoutContextOptions(v: unknown): v is CSOTTimeoutContextOptions { - return ( - v != null && - typeof v === 'object' && - 'serverSelectionTimeoutMS' in v && - typeof v.serverSelectionTimeoutMS === 'number' && - 'timeoutMS' in v && - typeof v.timeoutMS === 'number' - ); -} - /** @internal */ export abstract class TimeoutContext { static create(options: TimeoutContextOptions): TimeoutContext { if (options.session?.timeoutContext != null) return options.session?.timeoutContext; - if (isCSOTTimeoutContextOptions(options)) return new CSOTTimeoutContext(options); - else if (isLegacyTimeoutContextOptions(options)) return new LegacyTimeoutContext(options); + // timeoutMS is a no-op, so a CSOT context is never created + if (isLegacyTimeoutContextOptions(options)) return new LegacyTimeoutContext(options); else throw new MongoRuntimeError('Unrecognized options'); } diff --git a/src/utils.ts b/src/utils.ts index d9a96115fc8..05517c04e6e 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -493,12 +493,19 @@ export function resolveTimeoutOptions>( ): T & Pick< MongoClient['s']['options'], - 'timeoutMS' | 'serverSelectionTimeoutMS' | 'waitQueueTimeoutMS' | 'socketTimeoutMS' - > { - const { socketTimeoutMS, serverSelectionTimeoutMS, waitQueueTimeoutMS, timeoutMS } = - client.s.options; - return { socketTimeoutMS, serverSelectionTimeoutMS, waitQueueTimeoutMS, timeoutMS, ...options }; + 'serverSelectionTimeoutMS' | 'waitQueueTimeoutMS' | 'socketTimeoutMS' + > & { timeoutMS?: undefined } { + const { socketTimeoutMS, serverSelectionTimeoutMS, waitQueueTimeoutMS } = client.s.options; + // timeoutMS is a no-op, so it is never forwarded + return { + socketTimeoutMS, + serverSelectionTimeoutMS, + waitQueueTimeoutMS, + ...options, + timeoutMS: undefined + }; } + /** * Merge inherited properties from parent into options, prioritizing values from options, * then values from parent. @@ -514,7 +521,6 @@ export function resolveOptions( ): T { const result: T = Object.assign({}, options, resolveBSONOptions(options, parent)); - const timeoutMS = options?.timeoutMS ?? parent?.timeoutMS; // Users cannot pass a readConcern/writeConcern to operations in a transaction const session = options?.session; @@ -524,35 +530,20 @@ export function resolveOptions( result.readConcern = readConcern; } - let writeConcern = WriteConcern.fromOptions(options) ?? parent?.writeConcern; + const writeConcern = WriteConcern.fromOptions(options) ?? parent?.writeConcern; if (writeConcern) { - if (timeoutMS != null) { - writeConcern = WriteConcern.fromOptions({ - writeConcern: { - ...writeConcern, - wtimeout: undefined, - wtimeoutMS: undefined - } - }); - } result.writeConcern = writeConcern; } } - result.timeoutMS = timeoutMS; + // timeoutMS is a no-op, so it is never propagated to the operation + result.timeoutMS = undefined; const readPreference = ReadPreference.fromOptions(options) ?? parent?.readPreference; if (readPreference) { result.readPreference = readPreference; } - const isConvenientTransaction = session?.explicit && session?.timeoutContext != null; - if (isConvenientTransaction && options?.timeoutMS != null) { - throw new MongoInvalidArgumentError( - 'An operation cannot be given a timeoutMS setting when inside a withTransaction call that has a timeoutMS setting' - ); - } - return result; } diff --git a/test/integration/client-side-encryption/client_side_encryption.prose.28.kms_connect_callback.test.ts b/test/integration/client-side-encryption/client_side_encryption.prose.28.kms_connect_callback.test.ts index 20f88fd5960..8a0a21783db 100644 --- a/test/integration/client-side-encryption/client_side_encryption.prose.28.kms_connect_callback.test.ts +++ b/test/integration/client-side-encryption/client_side_encryption.prose.28.kms_connect_callback.test.ts @@ -320,6 +320,9 @@ describe('28. KMS Connect Callback', function () { it('Case 5: callback receives timeout', metadata, async function () { // This case MUST only be run by drivers that have implemented CSOT. + this.skipReason = 'NODE-7958: timeoutMS is a no-op'; + this.skip(); + let receivedTimeout: number | undefined; // Create a `ClientEncryption` object with `keyVaultNamespace` `keyvault.datakeys` and a diff --git a/test/integration/client-side-encryption/driver.test.ts b/test/integration/client-side-encryption/driver.test.ts index d56af2df090..35836c6094c 100644 --- a/test/integration/client-side-encryption/driver.test.ts +++ b/test/integration/client-side-encryption/driver.test.ts @@ -392,6 +392,11 @@ describe('Client Side Encryption Functional', function () { ); describe('CSOT on ClientEncryption', { requires: { clientSideEncryption: true } }, function () { + before(function () { + this.skipReason = 'NODE-7958: timeoutMS is a no-op'; + this.skip(); + }); + const metadata: MongoDBMetadataUI = { requires: { clientSideEncryption: true, mongodb: '>=4.4' } }; @@ -662,7 +667,8 @@ describe('Range Explicit Encryption with JS native types', function () { }); describe('CSOT', function () { - describe('Auto encryption', function () { + // NODE-7958: timeoutMS is a no-op + describe.skip('Auto encryption', function () { let setupClient; let keyVaultClient: MongoClient; let dataKey; @@ -797,7 +803,8 @@ describe('CSOT', function () { ); }); - describe('State machine', function () { + // NODE-7958: timeoutMS is a no-op + describe.skip('State machine', function () { const stateMachine = new StateMachine({} as any); const timeoutContext = () => ({ @@ -1091,7 +1098,8 @@ describe('CSOT', function () { }); }); - describe('Explicit Encryption', function () { + // NODE-7958: timeoutMS is a no-op + describe.skip('Explicit Encryption', function () { describe('#createEncryptedCollection', function () { let client: MongoClient; let clientEncryption: ClientEncryption; diff --git a/test/integration/client-side-operations-timeout/client_side_operations_timeout.prose.test.ts b/test/integration/client-side-operations-timeout/client_side_operations_timeout.prose.test.ts index 436ad0e73d9..247ef302f2a 100644 --- a/test/integration/client-side-operations-timeout/client_side_operations_timeout.prose.test.ts +++ b/test/integration/client-side-operations-timeout/client_side_operations_timeout.prose.test.ts @@ -45,10 +45,8 @@ describe('CSOT spec prose tests', function () { }); before(function () { - if (semver.satisfies(this.configuration.version, '>=9.0')) { - this.skipReason = 'TODO(NODE-7418): CSOT tests fail on server >=9.0, tracked in NODE-7418'; - this.skip(); - } + this.skipReason = 'NODE-7958: timeoutMS is a no-op'; + this.skip(); }); describe('1. Multi-batch writes', { requires: { topology: 'single', mongodb: '>=4.4' } }, () => { diff --git a/test/integration/client-side-operations-timeout/client_side_operations_timeout.spec.test.ts b/test/integration/client-side-operations-timeout/client_side_operations_timeout.spec.test.ts index d1deca64da9..661996a4594 100644 --- a/test/integration/client-side-operations-timeout/client_side_operations_timeout.spec.test.ts +++ b/test/integration/client-side-operations-timeout/client_side_operations_timeout.spec.test.ts @@ -4,6 +4,10 @@ import * as semver from 'semver'; import { loadSpecTests } from '../../spec'; import { runUnifiedSuite } from '../../tools/unified-spec-runner/runner'; +// NODE-7958: timeoutMS is a no-op, so every spec file that exercises it is skipped. These specs +// only use the legacy timeouts, so they keep running. +const nonCSOTSpecs = ['legacy-timeouts', 'retryability-legacy-timeouts']; + const skippedSpecs = { 'retryability-legacy-timeouts': 'Skipped in NODE-6984: timing-sensitive on loaded hosts.' }; @@ -30,6 +34,9 @@ describe('CSOT spec tests', function () { const specs = loadSpecTests('client-side-operations-timeout'); for (const spec of specs) { for (const test of spec.tests) { + if (!nonCSOTSpecs.includes(spec.name)) { + test.skipReason = 'NODE-7958: timeoutMS is a no-op'; + } if (skippedSpecs[spec.name] != null) { test.skipReason = skippedSpecs[spec.name]; } @@ -58,7 +65,8 @@ describe('CSOT spec tests', function () { }); }); -describe('CSOT modified spec tests', function () { +// NODE-7958: timeoutMS is a no-op +describe.skip('CSOT modified spec tests', function () { const specs = loadSpecTests( join('..', 'integration', 'client-side-operations-timeout', 'unified-csot-node-specs') ); diff --git a/test/integration/client-side-operations-timeout/client_side_operations_timeout.unit.test.ts b/test/integration/client-side-operations-timeout/client_side_operations_timeout.unit.test.ts index 10ecbe36298..d615b54448f 100644 --- a/test/integration/client-side-operations-timeout/client_side_operations_timeout.unit.test.ts +++ b/test/integration/client-side-operations-timeout/client_side_operations_timeout.unit.test.ts @@ -28,6 +28,11 @@ import { createTimerSandbox } from '../../unit/timer_sandbox'; describe('CSOT spec unit tests', function () { let client: MongoClient; + before(function () { + this.skipReason = 'NODE-7958: timeoutMS is a no-op'; + this.skip(); + }); + afterEach(async function () { sinon.restore(); await client?.close(); diff --git a/test/integration/client-side-operations-timeout/node_csot.test.ts b/test/integration/client-side-operations-timeout/node_csot.test.ts index 7ffae722640..ee2df278df3 100644 --- a/test/integration/client-side-operations-timeout/node_csot.test.ts +++ b/test/integration/client-side-operations-timeout/node_csot.test.ts @@ -37,10 +37,8 @@ const metadata = { requires: { mongodb: '>=4.4' } }; describe('CSOT driver tests', metadata, () => { before(function () { - if (semver.satisfies(this.configuration.version, '>=9.0')) { - this.skipReason = 'TODO(NODE-7418): CSOT tests fail on server >=9.0, tracked in NODE-7418'; - this.skip(); - } + this.skipReason = 'NODE-7958: timeoutMS is a no-op'; + this.skip(); }); // NOTE: minPoolSize here is set to ensure that connections are available when testing timeout diff --git a/test/integration/crud/client_bulk_write.test.ts b/test/integration/crud/client_bulk_write.test.ts index 4a65f800837..3c38c0bfcd5 100644 --- a/test/integration/crud/client_bulk_write.test.ts +++ b/test/integration/crud/client_bulk_write.test.ts @@ -1,5 +1,4 @@ import { expect } from 'chai'; -import * as semver from 'semver'; import { setTimeout } from 'timers/promises'; import { @@ -92,10 +91,8 @@ describe('Client Bulk Write', function () { describe('CSOT enabled', function () { before(function () { - if (semver.satisfies(this.configuration.version, '>=9.0')) { - this.skipReason = 'TODO(NODE-7418): CSOT tests fail on server >=9.0, tracked in NODE-7418'; - this.skip(); - } + this.skipReason = 'NODE-7958: timeoutMS is a no-op'; + this.skip(); }); describe('when timeoutMS is set on the client', function () { diff --git a/test/integration/crud/explain.test.ts b/test/integration/crud/explain.test.ts index 8236cbf00ee..fff9014f0d0 100644 --- a/test/integration/crud/explain.test.ts +++ b/test/integration/crud/explain.test.ts @@ -300,7 +300,8 @@ describe('CRUD API explain option', function () { } }); - describe('explain with timeoutMS', function () { + // NODE-7958: timeoutMS is a no-op + describe.skip('explain with timeoutMS', function () { let client: MongoClient; type ExplainStartedEvent = CommandStartedEvent & { command: { explain: Document & { maxTimeMS?: number }; maxTimeMS?: number }; diff --git a/test/integration/crud/find_cursor_methods.test.ts b/test/integration/crud/find_cursor_methods.test.ts index fd2b66f679d..fa236849f52 100644 --- a/test/integration/crud/find_cursor_methods.test.ts +++ b/test/integration/crud/find_cursor_methods.test.ts @@ -260,8 +260,8 @@ describe('Find Cursor', function () { { timeoutContext: new CursorTimeoutContext( TimeoutContext.create({ - timeoutMS: 1000, - serverSelectionTimeoutMS: 1000 + serverSelectionTimeoutMS: 1000, + waitQueueTimeoutMS: 0 }), Symbol() ) diff --git a/test/integration/node-specific/abstract_cursor.test.ts b/test/integration/node-specific/abstract_cursor.test.ts index 3a39cfe03e5..17e1af643a3 100644 --- a/test/integration/node-specific/abstract_cursor.test.ts +++ b/test/integration/node-specific/abstract_cursor.test.ts @@ -436,7 +436,8 @@ describe('class AbstractCursor', function () { ); }); - describe('externally provided timeout contexts', function () { + // NODE-7958: timeoutMS and timeoutMode are no-ops + describe.skip('externally provided timeout contexts', function () { let client: MongoClient; let collection: Collection; let context: CursorTimeoutContext; diff --git a/test/integration/node-specific/auto_connect.test.ts b/test/integration/node-specific/auto_connect.test.ts index f0850049632..05c397dea50 100644 --- a/test/integration/node-specific/auto_connect.test.ts +++ b/test/integration/node-specific/auto_connect.test.ts @@ -827,6 +827,11 @@ describe('When executing an operation for the first time', () => { describe('when CSOT is enabled', function () { let client: MongoClient; + before(function () { + this.skipReason = 'NODE-7958: timeoutMS is a no-op'; + this.skip(); + }); + beforeEach(async function () { client = this.configuration.newClient({ timeoutMS: 500 }); }); diff --git a/test/integration/node-specific/timeout_ms_noop.test.ts b/test/integration/node-specific/timeout_ms_noop.test.ts new file mode 100644 index 00000000000..c10f9e9b6be --- /dev/null +++ b/test/integration/node-specific/timeout_ms_noop.test.ts @@ -0,0 +1,63 @@ +import { expect } from 'chai'; + +import { type CommandStartedEvent, type MongoClient } from '../../mongodb'; +import { filterForCommands } from '../shared'; + +// NODE-7958: timeoutMS is deprecated and has no effect +describe('timeoutMS is a no-op', function () { + let client: MongoClient; + const commands: CommandStartedEvent[] = []; + + afterEach(async function () { + commands.length = 0; + await client?.close(); + }); + + context('when timeoutMS is set on the client', function () { + beforeEach(async function () { + client = this.configuration.newClient({}, { timeoutMS: 1, monitorCommands: true }); + client.on('commandStarted', filterForCommands('insert', commands)); + }); + + it('does not time out operations and does not send maxTimeMS', async function () { + const result = await client.db('timeout_ms_noop').collection('test').insertOne({ a: 1 }); + + expect(result).to.have.property('acknowledged', true); + expect(commands).to.have.lengthOf(1); + expect(commands[0].command).to.not.have.property('maxTimeMS'); + }); + + it('is not set on the client options', function () { + expect(client.options).to.not.have.property('timeoutMS'); + }); + }); + + context('when timeoutMS is set on an operation', function () { + beforeEach(async function () { + client = this.configuration.newClient({}, { monitorCommands: true }); + client.on('commandStarted', filterForCommands('find', commands)); + }); + + it('does not time out the operation and does not send maxTimeMS', async function () { + const docs = await client + .db('timeout_ms_noop') + .collection('test') + .find({}, { timeoutMS: 1 }) + .toArray(); + + expect(docs).to.be.an('array'); + expect(commands).to.have.lengthOf(1); + expect(commands[0].command).to.not.have.property('maxTimeMS'); + }); + + it('does not throw when timeoutMode is also set', async function () { + const docs = await client + .db('timeout_ms_noop') + .collection('test') + .find({}, { timeoutMode: 'iteration' }) + .toArray(); + + expect(docs).to.be.an('array'); + }); + }); +}); diff --git a/test/integration/read-write-concern/write_concern.test.ts b/test/integration/read-write-concern/write_concern.test.ts index 44dfe658c2e..acafa1065b9 100644 --- a/test/integration/read-write-concern/write_concern.test.ts +++ b/test/integration/read-write-concern/write_concern.test.ts @@ -315,7 +315,8 @@ describe('Write Concern', function () { }); }); - describe('NODE-6763: write concern is still added with timeoutMS is set', function () { + // NODE-7958: timeoutMS is a no-op + describe.skip('NODE-6763: write concern is still added with timeoutMS is set', function () { let client: MongoClient; let collection: Collection; const commands: CommandStartedEvent[] = []; diff --git a/test/unit/assorted/uri_options.spec.test.ts b/test/unit/assorted/uri_options.spec.test.ts index 349eedb33fb..2c704f93b3b 100644 --- a/test/unit/assorted/uri_options.spec.test.ts +++ b/test/unit/assorted/uri_options.spec.test.ts @@ -23,9 +23,18 @@ describe('URI option spec tests', function () { // NOTE(NODE-3989): Skipped because we now only accept true/false for boolean options. The spec expects us to // warn on other accepted but deprecated values, but as of NODE-3989, we now throw on these // values - 'Invalid loadBalanced value' + 'Invalid loadBalanced value', + + // NODE-7958: timeoutMS is a no-op, so it is accepted but never stored on the parsed options + 'timeoutMS=0', + 'Non-numeric timeoutMS causes a warning', + 'Too low timeoutMS causes a warning' ]; + // NODE-7958: the URI still contains timeoutMS, so these tests confirm it is accepted without + // throwing, but its expected value is dropped because it is never stored on the parsed options + const testsWithIgnoredTimeoutMS = ['Valid connection and timeout options are parsed correctly']; + const testsThatDoNotThrowOnWarn = [ // TODO(NODE-3923): compression option validation 'Too high zlibCompressionLevel causes a warning', @@ -54,8 +63,9 @@ describe('URI option spec tests', function () { return this.skip(); } + const { timeoutMS: _timeoutMS, ...options } = test.options ?? {}; executeUriValidationTest( - test, + testsWithIgnoredTimeoutMS.includes(test.description) ? { ...test, options } : test, testsThatDoNotThrowOnWarn.some(t => t === test.description) ); }); diff --git a/test/unit/client-side-encryption/client_encryption.test.ts b/test/unit/client-side-encryption/client_encryption.test.ts index 05f6cdbc5d2..68207ba55ac 100644 --- a/test/unit/client-side-encryption/client_encryption.test.ts +++ b/test/unit/client-side-encryption/client_encryption.test.ts @@ -40,38 +40,16 @@ describe('ClientEncryption', function () { 'Mng0NCt4ZHVUYUJCa1kxNkVyNUR1QURhZ2h2UzR2d2RrZzh0cFBwM3R6NmdWMDFBMUN3YkQ5aXRRMkhGRGdQV09wOGVNYUMxT2k3NjZKelhaQmRCZGJkTXVyZG9uSjFk', 'base64' ); + // NODE-7958: timeoutMS is a no-op, so it is never used by ClientEncryption context('when timeoutMS is provided in ClientEncryptionOptions and client', function () { - it('sets clientEncryption._timeoutMS to ClientEncryptionOptions.timeoutMS value', function () { + it('leaves clientEncryption._timeoutMS undefined', function () { const client = new MongoClient('mongodb://a/', { timeoutMS: 100 }); const clientEncryption = new ClientEncryption(client, { keyVaultNamespace: 'keyvault.datakeys', kmsProviders: { local: { key: LOCAL_MASTERKEY } }, timeoutMS: 500 }); - expect(clientEncryption._timeoutMS).to.equal(500); - }); - }); - - context('when timeoutMS is only provided in ClientEncryptionOptions', function () { - it('sets clientEncryption._timeoutMS to ClientEncryptionOptions.timeoutMS value', function () { - const client = new MongoClient('mongodb://a/'); - const clientEncryption = new ClientEncryption(client, { - keyVaultNamespace: 'keyvault.datakeys', - kmsProviders: { local: { key: LOCAL_MASTERKEY } }, - timeoutMS: 500 - }); - expect(clientEncryption._timeoutMS).to.equal(500); - }); - }); - - context('when timeoutMS is only provided in client', function () { - it('sets clientEncryption._timeoutMS to client.timeoutMS value', function () { - const client = new MongoClient('mongodb://a/', { timeoutMS: 100 }); - const clientEncryption = new ClientEncryption(client, { - keyVaultNamespace: 'keyvault.datakeys', - kmsProviders: { local: { key: LOCAL_MASTERKEY } } - }); - expect(clientEncryption._timeoutMS).to.equal(100); + expect(clientEncryption._timeoutMS).to.be.undefined; }); }); }); diff --git a/test/unit/cursor/aggregation_cursor.test.ts b/test/unit/cursor/aggregation_cursor.test.ts index e0d7a1223c1..4741665fb46 100644 --- a/test/unit/cursor/aggregation_cursor.test.ts +++ b/test/unit/cursor/aggregation_cursor.test.ts @@ -131,7 +131,8 @@ describe('class AggregationCursor', () => { }); context('when addStage, bespoke stage methods, or array is used to construct pipeline', () => { - context('when CSOT is enabled', () => { + // NODE-7958: timeoutMS and timeoutMode are no-ops, so these guards can never trigger + context.skip('when CSOT is enabled', () => { let aggregationCursor: AggregationCursor; before(function () { aggregationCursor = client @@ -196,7 +197,8 @@ describe('class AggregationCursor', () => { }); describe('constructor()', () => { - context('when CSOT is enabled', () => { + // NODE-7958: timeoutMS and timeoutMode are no-ops, so these guards can never trigger + context.skip('when CSOT is enabled', () => { context('when timeoutMode=ITERATION and a $out stage is provided', function () { it('throws a MongoAPIError', function () { expect( diff --git a/test/unit/mongo_client.test.ts b/test/unit/mongo_client.test.ts index 73650950899..113fd641b20 100644 --- a/test/unit/mongo_client.test.ts +++ b/test/unit/mongo_client.test.ts @@ -1176,25 +1176,21 @@ describe('MongoClient', function () { }); context('timeoutMS', function () { - context('when timeoutMS is negative', function () { - it('throws MongoParseError', function () { - expect(() => new MongoClient('mongodb://host', { timeoutMS: -1 })).to.throw( - MongoParseError - ); - }); - }); - - context('when timeoutMS is positive', function () { - it('sets the value on the MongoClient', function () { - const client = new MongoClient('mongodb://host', { timeoutMS: 1 }); - expect(client.options.timeoutMS).to.equal(1); + // NODE-7958: timeoutMS is a no-op, it is accepted but never stored on the client + for (const timeoutMS of [-1, 0, 1]) { + context(`when timeoutMS is ${timeoutMS}`, function () { + it('does not throw and is not set on the options', function () { + const client = new MongoClient('mongodb://host', { timeoutMS }); + expect(client.options).to.not.have.property('timeoutMS'); + expect(client.timeoutMS).to.be.undefined; + }); }); - }); + } - context('when timeoutMS is zero', function () { - it('sets the value on the MongoClient', function () { - const client = new MongoClient('mongodb://host', { timeoutMS: 0 }); - expect(client.options.timeoutMS).to.equal(0); + context('when timeoutMS is in the connection string', function () { + it('does not throw and is not set on the options', function () { + const client = new MongoClient('mongodb://host/?timeoutMS=100'); + expect(client.options).to.not.have.property('timeoutMS'); }); }); }); diff --git a/test/unit/sessions.test.ts b/test/unit/sessions.test.ts index b14edbb4ac5..50a923ef7fd 100644 --- a/test/unit/sessions.test.ts +++ b/test/unit/sessions.test.ts @@ -283,43 +283,20 @@ describe('Sessions - unit', function () { await mock.cleanup(); }); - context('when client has defined timeoutMS', function () { - beforeEach(async () => { + // NODE-7958: timeoutMS is a no-op, so the session never has a timeoutMS + context('when defaultTimeoutMS is defined', function () { + it('leaves timeoutMS as undefined', function () { client = new MongoClient(`mongodb://${server.hostAddress()}`, { timeoutMS: 100 }); - }); - - context('when defaultTimeoutMS is defined', function () { - it(`overrides client's timeoutMS value`, function () { - session = new ClientSession(client, serverSessionPool, { defaultTimeoutMS: 200 }); - expect(session).to.have.property('timeoutMS', 200); - }); - }); - - context('when defaultTimeoutMS is not defined', function () { - it(`inherits client's timeoutMS value`, function () { - session = new ClientSession(client, serverSessionPool, {}); - expect(session).to.have.property('timeoutMS', 100); - }); + session = new ClientSession(client, serverSessionPool, { defaultTimeoutMS: 200 }); + expect(session.timeoutMS).to.be.undefined; }); }); - context('when client has not defined timeoutMS', function () { - beforeEach(async () => { - client = new MongoClient(`mongodb://${server.hostAddress()}`, {}); - }); - - context('when defaultTimeoutMS is defined', function () { - it(`sets timeoutMS to defaultTimeoutMS`, function () { - session = new ClientSession(client, serverSessionPool, { defaultTimeoutMS: 200 }); - expect(session).to.have.property('timeoutMS', 200); - }); - }); - - context('when defaultTimeoutMS is not defined', function () { - it(`leaves timeoutMS as undefined`, function () { - session = new ClientSession(client, serverSessionPool, {}); - expect(session.timeoutMS).to.be.undefined; - }); + context('when defaultTimeoutMS is not defined', function () { + it('leaves timeoutMS as undefined', function () { + client = new MongoClient(`mongodb://${server.hostAddress()}`, { timeoutMS: 100 }); + session = new ClientSession(client, serverSessionPool, {}); + expect(session.timeoutMS).to.be.undefined; }); }); }); diff --git a/test/unit/timeout.test.ts b/test/unit/timeout.test.ts index 1dd7e83feb5..30c68154946 100644 --- a/test/unit/timeout.test.ts +++ b/test/unit/timeout.test.ts @@ -78,15 +78,16 @@ describe('Timeout', function () { describe('TimeoutContext', function () { describe('TimeoutContext.create', function () { + // NODE-7958: timeoutMS is a no-op, so a CSOTTimeoutContext is never created context('when timeoutMS is a number', function () { - it('returns a CSOTTimeoutContext instance', function () { + it('returns a LegacyTimeoutContext instance', function () { const ctx = TimeoutContext.create({ timeoutMS: 0, serverSelectionTimeoutMS: 0, waitQueueTimeoutMS: 0 }); - expect(ctx).to.be.instanceOf(CSOTTimeoutContext); + expect(ctx).to.be.instanceOf(LegacyTimeoutContext); }); }); diff --git a/test/unit/utils.test.ts b/test/unit/utils.test.ts index cb98f69dc3f..1f7fc30f1c4 100644 --- a/test/unit/utils.test.ts +++ b/test/unit/utils.test.ts @@ -19,12 +19,16 @@ import { isUint8Array, LEGACY_HELLO_COMMAND, List, + MongoClient, MongoDBCollectionNamespace, MongoDBNamespace, MongoInvalidArgumentError, MongoRuntimeError, + resolveOptions, + resolveTimeoutOptions, runNodelessTests, - shuffle + shuffle, + TimeoutContext } from '../mongodb'; import { ensureTypeByName, sleep } from '../tools/utils'; @@ -1325,4 +1329,59 @@ describe('driver utils', function () { }); }); }); + + // NODE-7958: timeoutMS and timeoutMode are no-ops + describe('timeoutMS no-op', function () { + describe('resolveOptions()', function () { + it('drops timeoutMS passed in the options', function () { + const result = resolveOptions(undefined, { timeoutMS: 100 }); + expect(result.timeoutMS).to.be.undefined; + }); + + it('does not inherit timeoutMS from the parent', function () { + const result = resolveOptions({ + s: { namespace: new MongoDBNamespace('db') }, + timeoutMS: 100 + }); + expect(result.timeoutMS).to.be.undefined; + }); + + it('does not throw when timeoutMS is given inside an explicit session', function () { + const session = { + explicit: true, + timeoutContext: {}, + inTransaction: () => true, + transaction: { options: {} } + }; + expect(() => resolveOptions(undefined, { timeoutMS: 100, session } as any)).to.not.throw(); + }); + + it('keeps wtimeoutMS on the write concern', function () { + const result = resolveOptions(undefined, { + timeoutMS: 100, + writeConcern: { w: 1, wtimeoutMS: 50 } + }); + expect(result.writeConcern).to.have.property('wtimeoutMS', 50); + }); + }); + + describe('resolveTimeoutOptions()', function () { + it('never forwards timeoutMS', function () { + const client = new MongoClient('mongodb://a/', { timeoutMS: 100 }); + expect(resolveTimeoutOptions(client, {})).to.have.property('timeoutMS', undefined); + expect(resolveTimeoutOptions(client, { timeoutMS: 200 } as any)).to.have.property( + 'timeoutMS', + undefined + ); + }); + + it('creates a context that is not CSOT', function () { + const client = new MongoClient('mongodb://a/'); + const context = TimeoutContext.create( + resolveTimeoutOptions(client, { timeoutMS: 200 } as any) + ); + expect(context.csotEnabled()).to.be.false; + }); + }); + }); }); From e5ab6011ee21c030b55bd567c5e4fc28e43e626c Mon Sep 17 00:00:00 2001 From: Sergey Zelenov Date: Thu, 29 Oct 2026 17:46:42 +0100 Subject: [PATCH 02/11] Deprecate timeoutMS and update comments Mark `timeoutMS` as deprecated and note its future removal. Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/mongo_client.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/mongo_client.ts b/src/mongo_client.ts index b879c861081..586d20d1ae1 100644 --- a/src/mongo_client.ts +++ b/src/mongo_client.ts @@ -1115,6 +1115,8 @@ export interface MongoOptions * TODO: NODE-5671 - remove internal flag */ mongodbLogPath?: 'stderr' | 'stdout' | MongoDBLogWritable; + /** @deprecated `timeoutMS` has no effect and will be removed in a future release. */ + timeoutMS?: number; /** @internal */ __skipPingOnConnect?: boolean; From 11c8158d0d5c4202c3e6cb68aefdd9d102e9b55c Mon Sep 17 00:00:00 2001 From: Sergey Zelenov Date: Thu, 29 Oct 2026 19:27:48 +0100 Subject: [PATCH 03/11] shorten the description for no longer relevant option, simplify `executeOperation` --- .../client_encryption.ts | 1 + src/cursor/abstract_cursor.ts | 53 +------------------ src/cursor/run_command_cursor.ts | 31 +---------- src/sessions.ts | 23 ++++---- 4 files changed, 18 insertions(+), 90 deletions(-) diff --git a/src/client-side-encryption/client_encryption.ts b/src/client-side-encryption/client_encryption.ts index 28ed20f3280..9cec14405fb 100644 --- a/src/client-side-encryption/client_encryption.ts +++ b/src/client-side-encryption/client_encryption.ts @@ -987,6 +987,7 @@ export interface ClientEncryptionOptions { keyExpirationMS?: number; /** + * The timeout setting to be used for all the operations on ClientEncryption. * @deprecated `timeoutMS` has no effect and will be removed in a future release. */ timeoutMS?: number; diff --git a/src/cursor/abstract_cursor.ts b/src/cursor/abstract_cursor.ts index 5d302608d22..68e21ebef58 100644 --- a/src/cursor/abstract_cursor.ts +++ b/src/cursor/abstract_cursor.ts @@ -67,31 +67,7 @@ function removeActiveCursor(this: AbstractCursor) { /** * @public - * Specifies how `timeoutMS` is applied to the cursor. Can be either `'cursorLifeTime'` or `'iteration'` - * When set to `'iteration'`, the deadline specified by `timeoutMS` applies to each call of - * `cursor.next()`. - * When set to `'cursorLifetime'`, the deadline applies to the life of the entire cursor. - * - * Depending on the type of cursor being used, this option has different default values. - * For non-tailable cursors, this value defaults to `'cursorLifetime'` - * For tailable cursors, this value defaults to `'iteration'` since tailable cursors, by - * definition can have an arbitrarily long lifetime. - * - * @example - * ```ts - * const cursor = collection.find({}, {timeoutMS: 100, timeoutMode: 'iteration'}); - * for await (const doc of cursor) { - * // process doc - * // This will throw a timeout error if any of the iterator's `next()` calls takes more than 100ms, but - * // will continue to iterate successfully otherwise, regardless of the number of batches. - * } - * ``` - * - * @example - * ```ts - * const cursor = collection.find({}, { timeoutMS: 1000, timeoutMode: 'cursorLifetime' }); - * const docs = await cursor.toArray(); // This entire line will throw a timeout error if all batches are not fetched and returned within 1000ms. - * ``` + * Specifies how `timeoutMS` is applied to the cursor. * @deprecated `timeoutMode` has no effect and will be removed in a future release. */ export const CursorTimeoutMode = Object.freeze({ @@ -149,32 +125,7 @@ export interface AbstractCursorOptions extends BSONSerializeOptions { */ timeoutMS?: number; /** - * @public - * Specifies how `timeoutMS` is applied to the cursor. Can be either `'cursorLifeTime'` or `'iteration'` - * When set to `'iteration'`, the deadline specified by `timeoutMS` applies to each call of - * `cursor.next()`. - * When set to `'cursorLifetime'`, the deadline applies to the life of the entire cursor. - * - * Depending on the type of cursor being used, this option has different default values. - * For non-tailable cursors, this value defaults to `'cursorLifetime'` - * For tailable cursors, this value defaults to `'iteration'` since tailable cursors, by - * definition can have an arbitrarily long lifetime. - * - * @example - * ```ts - * const cursor = collection.find({}, {timeoutMS: 100, timeoutMode: 'iteration'}); - * for await (const doc of cursor) { - * // process doc - * // This will throw a timeout error if any of the iterator's `next()` calls takes more than 100ms, but - * // will continue to iterate successfully otherwise, regardless of the number of batches. - * } - * ``` - * - * @example - * ```ts - * const cursor = collection.find({}, { timeoutMS: 1000, timeoutMode: 'cursorLifetime' }); - * const docs = await cursor.toArray(); // This entire line will throw a timeout error if all batches are not fetched and returned within 1000ms. - * ``` + * Specifies how `timeoutMS` is applied to the cursor. * @deprecated `timeoutMode` has no effect and will be removed in a future release. */ timeoutMode?: CursorTimeoutMode; diff --git a/src/cursor/run_command_cursor.ts b/src/cursor/run_command_cursor.ts index f7d3b56cfb6..f0686c0a18d 100644 --- a/src/cursor/run_command_cursor.ts +++ b/src/cursor/run_command_cursor.ts @@ -20,39 +20,12 @@ export type RunCursorCommandOptions = { readPreference?: ReadPreferenceLike; session?: ClientSession; /** - * Specifies the time an operation will run until it throws a timeout error. Note that if - * `maxTimeMS` is provided in the command in addition to setting `timeoutMS` in the options, then - * the original value of `maxTimeMS` will be overwritten. + * Specifies the time an operation will run until it throws a timeout error. * @deprecated `timeoutMS` has no effect and will be removed in a future release. */ timeoutMS?: number; /** - * @public - * Specifies how `timeoutMS` is applied to the cursor. Can be either `'cursorLifeTime'` or `'iteration'` - * When set to `'iteration'`, the deadline specified by `timeoutMS` applies to each call of - * `cursor.next()`. - * When set to `'cursorLifetime'`, the deadline applies to the life of the entire cursor. - * - * Depending on the type of cursor being used, this option has different default values. - * For non-tailable cursors, this value defaults to `'cursorLifetime'` - * For tailable cursors, this value defaults to `'iteration'` since tailable cursors, by - * definition can have an arbitrarily long lifetime. - * - * @example - * ```ts - * const cursor = collection.find({}, {timeoutMS: 100, timeoutMode: 'iteration'}); - * for await (const doc of cursor) { - * // process doc - * // This will throw a timeout error if any of the iterator's `next()` calls takes more than 100ms, but - * // will continue to iterate successfully otherwise, regardless of the number of batches. - * } - * ``` - * - * @example - * ```ts - * const cursor = collection.find({}, { timeoutMS: 1000, timeoutMode: 'cursorLifetime' }); - * const docs = await cursor.toArray(); // This entire line will throw a timeout error if all batches are not fetched and returned within 1000ms. - * ``` + * Specifies how `timeoutMS` is applied to the cursor. * @deprecated `timeoutMode` has no effect and will be removed in a future release. */ timeoutMode?: CursorTimeoutMode; diff --git a/src/sessions.ts b/src/sessions.ts index e9ba121d032..0ad14c39391 100644 --- a/src/sessions.ts +++ b/src/sessions.ts @@ -60,6 +60,7 @@ export interface ClientSessionOptions { defaultTransactionOptions?: TransactionOptions; /** * @public + * An overriding timeoutMS value to use for a client-side timeout. * @deprecated `timeoutMS` has no effect and will be removed in a future release. */ defaultTimeoutMS?: number; @@ -90,7 +91,10 @@ export interface EndSessionOptions { force?: boolean; forceClear?: boolean; - /** @deprecated `timeoutMS` has no effect and will be removed in a future release. */ + /** + * Specifies the time an operation will run until it throws a timeout error + * @deprecated `timeoutMS` has no effect and will be removed in a future release. + */ timeoutMS?: number; } @@ -492,10 +496,8 @@ export class ClientSession }); operation.maxAttempts = this.clientOptions.maxAdaptiveRetries + 1; - const timeoutContext = this.timeoutContext ?? null; - try { - await executeOperation(this.client, operation, timeoutContext); + await executeOperation(this.client, operation); this.commitAttempted = undefined; return; } catch (firstCommitError) { @@ -519,7 +521,7 @@ export class ClientSession bypassPinningCheck: true }); op.maxAttempts = remainingAttempts; - await executeOperation(this.client, op, timeoutContext); + await executeOperation(this.client, op); return; } catch (retryCommitError) { // If the retry failed, we process that error instead of the original @@ -587,8 +589,6 @@ export class ClientSession recoveryToken?: Document; } = { abortTransaction: 1 }; - const timeoutContext = null; - const wc = this.transaction.options.writeConcern ?? this.clientOptions?.writeConcern; if (wc != null) { WriteConcern.apply(command, { wtimeoutMS: 10000, w: 'majority', ...wc }); @@ -605,7 +605,7 @@ export class ClientSession }); try { - await executeOperation(this.client, operation, timeoutContext); + await executeOperation(this.client, operation); this.unpin(); return; } catch (firstAbortError) { @@ -618,7 +618,7 @@ export class ClientSession if (firstAbortError instanceof MongoError && isRetryableWriteError(firstAbortError)) { try { - await executeOperation(this.client, operation, timeoutContext); + await executeOperation(this.client, operation); return; } catch (secondAbortError) { if (secondAbortError.name === 'MongoRuntimeError') throw secondAbortError; @@ -683,7 +683,10 @@ export class ClientSession async withTransaction( fn: WithTransactionCallback, options?: TransactionOptions & { - /** @deprecated `timeoutMS` has no effect and will be removed in a future release. */ + /** + * Configures a timeoutMS expiry for the entire withTransactionCallback. + * @deprecated `timeoutMS` has no effect and will be removed in a future release. + */ timeoutMS?: number; } ): Promise { From a29738a37c55f5f9c2757fa2893784b3f1146caf Mon Sep 17 00:00:00 2001 From: Sergey Zelenov Date: Thu, 29 Oct 2026 20:25:53 +0100 Subject: [PATCH 04/11] clear timeoutMS and timeoutMode on cursor and change stream options --- src/change_stream.ts | 3 +++ src/cursor/abstract_cursor.ts | 4 ++++ src/gridfs/download.ts | 2 -- src/gridfs/upload.ts | 3 --- test/unit/change_stream.test.ts | 23 ++++++++++++++++++++- test/unit/cursor/aggregation_cursor.test.ts | 18 ++++++++++++++++ 6 files changed, 47 insertions(+), 6 deletions(-) diff --git a/src/change_stream.ts b/src/change_stream.ts index a459dda4dca..a473556a764 100644 --- a/src/change_stream.ts +++ b/src/change_stream.ts @@ -647,6 +647,9 @@ export class ChangeStream< this.pipeline = pipeline; this.options = { ...options }; + // timeoutMS and timeoutMode are no-ops, so they must never reach the cursor options + this.options.timeoutMS = undefined; + this.options.timeoutMode = undefined; delete this.options.writeConcern; if (parent instanceof Collection) { diff --git a/src/cursor/abstract_cursor.ts b/src/cursor/abstract_cursor.ts index 68e21ebef58..c4936b79c70 100644 --- a/src/cursor/abstract_cursor.ts +++ b/src/cursor/abstract_cursor.ts @@ -212,6 +212,10 @@ export abstract class AbstractCursor< super(); this.on('error', noop); + // timeoutMS and timeoutMode are no-ops, so they must never reach the operations built from these options + options.timeoutMS = undefined; + options.timeoutMode = undefined; + if (!client.s.isMongoClient) { throw new MongoRuntimeError('Cursor must be constructed with MongoClient'); } diff --git a/src/gridfs/download.ts b/src/gridfs/download.ts index 9bd0ef226ae..0ef1eafbc34 100644 --- a/src/gridfs/download.ts +++ b/src/gridfs/download.ts @@ -2,7 +2,6 @@ import { Readable } from 'stream'; import { ByteUtils, type Document, type ObjectId } from '../bson'; import type { Collection } from '../collection'; -import { CursorTimeoutMode } from '../cursor/abstract_cursor'; import type { FindCursor } from '../cursor/find_cursor'; import { MongoGridFSChunkError, @@ -367,7 +366,6 @@ function init(stream: GridFSBucketReadStream): void { stream.s.cursor = stream.s.chunks .find(filter, { - timeoutMode: stream.s.options.timeoutMS != null ? CursorTimeoutMode.LIFETIME : undefined, timeoutMS: remainingTimeMS }) .sort({ n: 1 }); diff --git a/src/gridfs/upload.ts b/src/gridfs/upload.ts index c6056cf3ef8..bdbfec44e1b 100644 --- a/src/gridfs/upload.ts +++ b/src/gridfs/upload.ts @@ -2,7 +2,6 @@ import { Writable } from 'stream'; import { ByteUtils, type Document, ObjectId } from '../bson'; import type { Collection } from '../collection'; -import { CursorTimeoutMode } from '../cursor/abstract_cursor'; import { MongoAPIError, MONGODB_ERROR_CODES, @@ -241,7 +240,6 @@ async function checkChunksIndex(stream: GridFSBucketWriteStream): Promise try { indexes = await stream.chunks .listIndexes({ - timeoutMode: remainingTimeMS != null ? CursorTimeoutMode.LIFETIME : undefined, timeoutMS: remainingTimeMS }) .toArray(); @@ -341,7 +339,6 @@ async function checkIndexes(stream: GridFSBucketWriteStream): Promise { `Upload timed out after ${stream.timeoutContext?.timeoutMS}ms` ); const listIndexesOptions = { - timeoutMode: remainingTimeMS != null ? CursorTimeoutMode.LIFETIME : undefined, timeoutMS: remainingTimeMS }; try { diff --git a/test/unit/change_stream.test.ts b/test/unit/change_stream.test.ts index 2eac004f3da..dfaea5c8e90 100644 --- a/test/unit/change_stream.test.ts +++ b/test/unit/change_stream.test.ts @@ -2,7 +2,13 @@ import { Long, Timestamp } from 'bson'; import { expect } from 'chai'; import * as sinon from 'sinon'; -import { ChangeStreamCursor, filterOutOptions, MongoClient, MongoDBNamespace } from '../mongodb'; +import { + ChangeStream, + ChangeStreamCursor, + filterOutOptions, + MongoClient, + MongoDBNamespace +} from '../mongodb'; describe('ChangeStreamCursor', function () { afterEach(function () { @@ -262,3 +268,18 @@ describe('ChangeStreamCursor', function () { }); }); }); + +// NODE-7958: timeoutMS and timeoutMode are no-ops +describe('ChangeStream', function () { + it('does not keep timeoutMS and timeoutMode on its options', function () { + const client = new MongoClient('mongodb://localhost:27017'); + const changeStream = new ChangeStream(client, [], { + timeoutMS: 100, + timeoutMode: 'iteration' + } as any); + + expect(changeStream.options).to.have.property('timeoutMS', undefined); + expect(changeStream.options).to.have.property('timeoutMode', undefined); + changeStream.close().catch(() => null); + }); +}); diff --git a/test/unit/cursor/aggregation_cursor.test.ts b/test/unit/cursor/aggregation_cursor.test.ts index 4741665fb46..3bb9b80458c 100644 --- a/test/unit/cursor/aggregation_cursor.test.ts +++ b/test/unit/cursor/aggregation_cursor.test.ts @@ -28,6 +28,24 @@ describe('class AggregationCursor', () => { }); }); + // NODE-7958: timeoutMS and timeoutMode are no-ops + context('when timeoutMS and timeoutMode are provided', () => { + it('does not keep them on the cursor options or the aggregate options', () => { + const options = { timeoutMS: 100, timeoutMode: CursorTimeoutMode.ITERATION }; + const aggregationCursor = new AggregationCursor( + client, + ns('db.coll'), + [{ $out: 'test' }], + options + ); + + expect(aggregationCursor).to.not.have.nested.property('cursorOptions.timeoutMS'); + expect(aggregationCursor).to.not.have.nested.property('cursorOptions.timeoutMode'); + expect(aggregationCursor).to.have.nested.property('aggregateOptions.timeoutMS', undefined); + expect(aggregationCursor).to.have.nested.property('aggregateOptions.timeoutMode', undefined); + }); + }); + context('clone()', () => { it('returns a new cursor', () => { const cloned = cursor.clone(); From bd9ef14c0285a06189694f0fd8586f1eaa06fcaf Mon Sep 17 00:00:00 2001 From: Johnathan Martell <286711055+johnmtll@users.noreply.github.com> Date: Thu, 29 Oct 2026 18:46:59 -0400 Subject: [PATCH 05/11] fix: support frozen opts --- src/collection.ts | 2 +- src/cursor/list_collections_cursor.ts | 2 +- src/cursor/list_indexes_cursor.ts | 2 +- src/db.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/collection.ts b/src/collection.ts index 855e6a8fc82..6264eae9638 100644 --- a/src/collection.ts +++ b/src/collection.ts @@ -1263,7 +1263,7 @@ export class Collection { ? null : indexNameOrOptions; - return new ListSearchIndexesCursor(this as TODO_NODE_3286, indexName, options); + return new ListSearchIndexesCursor(this as TODO_NODE_3286, indexName, { ...options }); } /** diff --git a/src/cursor/list_collections_cursor.ts b/src/cursor/list_collections_cursor.ts index bbe0a2536b9..f351384a627 100644 --- a/src/cursor/list_collections_cursor.ts +++ b/src/cursor/list_collections_cursor.ts @@ -21,7 +21,7 @@ export class ListCollectionsCursor< options?: ListCollectionsOptions & Abortable; constructor(db: Db, filter: Document, options?: ListCollectionsOptions & Abortable) { - super(db.client, db.s.namespace, options); + super(db.client, db.s.namespace, { ...options }); this.parent = db; this.filter = filter; this.options = options; diff --git a/src/cursor/list_indexes_cursor.ts b/src/cursor/list_indexes_cursor.ts index 0f768f3b699..61743b82481 100644 --- a/src/cursor/list_indexes_cursor.ts +++ b/src/cursor/list_indexes_cursor.ts @@ -10,7 +10,7 @@ export class ListIndexesCursor extends AbstractCursor { options?: ListIndexesOptions; constructor(collection: Collection, options?: ListIndexesOptions) { - super(collection.client, collection.s.namespace, options); + super(collection.client, collection.s.namespace, { ...options }); this.parent = collection; this.options = options; } diff --git a/src/db.ts b/src/db.ts index 50b8b14a19c..c1234035aef 100644 --- a/src/db.ts +++ b/src/db.ts @@ -570,6 +570,6 @@ export class Db { * @param options - Configurations for running the command, bson options will apply to getMores */ runCursorCommand(command: Document, options?: RunCursorCommandOptions): RunCommandCursor { - return new RunCommandCursor(this, command, options); + return new RunCommandCursor(this, command, { ...options }); } } From 1094b66f82d708a87bde289365d15ef32db02f30 Mon Sep 17 00:00:00 2001 From: Johnathan Martell <286711055+johnmtll@users.noreply.github.com> Date: Thu, 29 Oct 2026 19:00:35 -0400 Subject: [PATCH 06/11] Revert "fix: support frozen opts" This reverts commit bd9ef14c0285a06189694f0fd8586f1eaa06fcaf. --- src/collection.ts | 2 +- src/cursor/list_collections_cursor.ts | 2 +- src/cursor/list_indexes_cursor.ts | 2 +- src/db.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/collection.ts b/src/collection.ts index 6264eae9638..855e6a8fc82 100644 --- a/src/collection.ts +++ b/src/collection.ts @@ -1263,7 +1263,7 @@ export class Collection { ? null : indexNameOrOptions; - return new ListSearchIndexesCursor(this as TODO_NODE_3286, indexName, { ...options }); + return new ListSearchIndexesCursor(this as TODO_NODE_3286, indexName, options); } /** diff --git a/src/cursor/list_collections_cursor.ts b/src/cursor/list_collections_cursor.ts index f351384a627..bbe0a2536b9 100644 --- a/src/cursor/list_collections_cursor.ts +++ b/src/cursor/list_collections_cursor.ts @@ -21,7 +21,7 @@ export class ListCollectionsCursor< options?: ListCollectionsOptions & Abortable; constructor(db: Db, filter: Document, options?: ListCollectionsOptions & Abortable) { - super(db.client, db.s.namespace, { ...options }); + super(db.client, db.s.namespace, options); this.parent = db; this.filter = filter; this.options = options; diff --git a/src/cursor/list_indexes_cursor.ts b/src/cursor/list_indexes_cursor.ts index 61743b82481..0f768f3b699 100644 --- a/src/cursor/list_indexes_cursor.ts +++ b/src/cursor/list_indexes_cursor.ts @@ -10,7 +10,7 @@ export class ListIndexesCursor extends AbstractCursor { options?: ListIndexesOptions; constructor(collection: Collection, options?: ListIndexesOptions) { - super(collection.client, collection.s.namespace, { ...options }); + super(collection.client, collection.s.namespace, options); this.parent = collection; this.options = options; } diff --git a/src/db.ts b/src/db.ts index c1234035aef..50b8b14a19c 100644 --- a/src/db.ts +++ b/src/db.ts @@ -570,6 +570,6 @@ export class Db { * @param options - Configurations for running the command, bson options will apply to getMores */ runCursorCommand(command: Document, options?: RunCursorCommandOptions): RunCommandCursor { - return new RunCommandCursor(this, command, { ...options }); + return new RunCommandCursor(this, command, options); } } From c147bd32c73528e51e2c34c94eb228e47571c691 Mon Sep 17 00:00:00 2001 From: Johnathan Martell <286711055+johnmtll@users.noreply.github.com> Date: Thu, 29 Oct 2026 19:04:17 -0400 Subject: [PATCH 07/11] fix: copy all input opts to AbstractCursor --- src/cursor/abstract_cursor.ts | 6 +++--- test/unit/cursor/abstract_cursor.test.ts | 5 +++++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/cursor/abstract_cursor.ts b/src/cursor/abstract_cursor.ts index c4936b79c70..f172602f8b2 100644 --- a/src/cursor/abstract_cursor.ts +++ b/src/cursor/abstract_cursor.ts @@ -212,9 +212,9 @@ export abstract class AbstractCursor< super(); this.on('error', noop); - // timeoutMS and timeoutMode are no-ops, so they must never reach the operations built from these options - options.timeoutMS = undefined; - options.timeoutMode = undefined; + // timeoutMS and timeoutMode are no-ops, so they must never reach the operations built from these options. + // Copy first so that frozen or sealed user-provided options are supported + options = { ...options, timeoutMS: undefined, timeoutMode: undefined }; if (!client.s.isMongoClient) { throw new MongoRuntimeError('Cursor must be constructed with MongoClient'); diff --git a/test/unit/cursor/abstract_cursor.test.ts b/test/unit/cursor/abstract_cursor.test.ts index c28a54fcdcd..0046635f997 100644 --- a/test/unit/cursor/abstract_cursor.test.ts +++ b/test/unit/cursor/abstract_cursor.test.ts @@ -43,5 +43,10 @@ describe('class AbstractCursor', () => { expect(cursor).to.have.property('session', session); await session.endSession(); }); + + it('does not throw when given frozen options', () => { + const options = Object.freeze({ timeoutMS: 100 }); + expect(() => new ConcreteCursor(client, options)).to.not.throw(); + }); }); }); From 6fda83566235cd75a751020a1bd48958dba36b9c Mon Sep 17 00:00:00 2001 From: Johnathan Martell <286711055+johnmtll@users.noreply.github.com> Date: Thu, 29 Oct 2026 19:54:57 -0400 Subject: [PATCH 08/11] Reapply "fix: support frozen opts" This reverts commit 1094b66f82d708a87bde289365d15ef32db02f30. --- src/collection.ts | 2 +- src/cursor/list_collections_cursor.ts | 2 +- src/cursor/list_indexes_cursor.ts | 2 +- src/db.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/collection.ts b/src/collection.ts index 855e6a8fc82..6264eae9638 100644 --- a/src/collection.ts +++ b/src/collection.ts @@ -1263,7 +1263,7 @@ export class Collection { ? null : indexNameOrOptions; - return new ListSearchIndexesCursor(this as TODO_NODE_3286, indexName, options); + return new ListSearchIndexesCursor(this as TODO_NODE_3286, indexName, { ...options }); } /** diff --git a/src/cursor/list_collections_cursor.ts b/src/cursor/list_collections_cursor.ts index bbe0a2536b9..f351384a627 100644 --- a/src/cursor/list_collections_cursor.ts +++ b/src/cursor/list_collections_cursor.ts @@ -21,7 +21,7 @@ export class ListCollectionsCursor< options?: ListCollectionsOptions & Abortable; constructor(db: Db, filter: Document, options?: ListCollectionsOptions & Abortable) { - super(db.client, db.s.namespace, options); + super(db.client, db.s.namespace, { ...options }); this.parent = db; this.filter = filter; this.options = options; diff --git a/src/cursor/list_indexes_cursor.ts b/src/cursor/list_indexes_cursor.ts index 0f768f3b699..61743b82481 100644 --- a/src/cursor/list_indexes_cursor.ts +++ b/src/cursor/list_indexes_cursor.ts @@ -10,7 +10,7 @@ export class ListIndexesCursor extends AbstractCursor { options?: ListIndexesOptions; constructor(collection: Collection, options?: ListIndexesOptions) { - super(collection.client, collection.s.namespace, options); + super(collection.client, collection.s.namespace, { ...options }); this.parent = collection; this.options = options; } diff --git a/src/db.ts b/src/db.ts index 50b8b14a19c..c1234035aef 100644 --- a/src/db.ts +++ b/src/db.ts @@ -570,6 +570,6 @@ export class Db { * @param options - Configurations for running the command, bson options will apply to getMores */ runCursorCommand(command: Document, options?: RunCursorCommandOptions): RunCommandCursor { - return new RunCommandCursor(this, command, options); + return new RunCommandCursor(this, command, { ...options }); } } From 9fc8a66b21773eaf2c078456c27b3e3f074645f8 Mon Sep 17 00:00:00 2001 From: Johnathan Martell <286711055+johnmtll@users.noreply.github.com> Date: Thu, 29 Oct 2026 19:55:03 -0400 Subject: [PATCH 09/11] Revert "fix: copy all input opts to AbstractCursor" This reverts commit c147bd32c73528e51e2c34c94eb228e47571c691. --- src/cursor/abstract_cursor.ts | 6 +++--- test/unit/cursor/abstract_cursor.test.ts | 5 ----- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/src/cursor/abstract_cursor.ts b/src/cursor/abstract_cursor.ts index f172602f8b2..c4936b79c70 100644 --- a/src/cursor/abstract_cursor.ts +++ b/src/cursor/abstract_cursor.ts @@ -212,9 +212,9 @@ export abstract class AbstractCursor< super(); this.on('error', noop); - // timeoutMS and timeoutMode are no-ops, so they must never reach the operations built from these options. - // Copy first so that frozen or sealed user-provided options are supported - options = { ...options, timeoutMS: undefined, timeoutMode: undefined }; + // timeoutMS and timeoutMode are no-ops, so they must never reach the operations built from these options + options.timeoutMS = undefined; + options.timeoutMode = undefined; if (!client.s.isMongoClient) { throw new MongoRuntimeError('Cursor must be constructed with MongoClient'); diff --git a/test/unit/cursor/abstract_cursor.test.ts b/test/unit/cursor/abstract_cursor.test.ts index 0046635f997..c28a54fcdcd 100644 --- a/test/unit/cursor/abstract_cursor.test.ts +++ b/test/unit/cursor/abstract_cursor.test.ts @@ -43,10 +43,5 @@ describe('class AbstractCursor', () => { expect(cursor).to.have.property('session', session); await session.endSession(); }); - - it('does not throw when given frozen options', () => { - const options = Object.freeze({ timeoutMS: 100 }); - expect(() => new ConcreteCursor(client, options)).to.not.throw(); - }); }); }); From e4929eb35cc7898246695b83cc87ec73071b0e6a Mon Sep 17 00:00:00 2001 From: Johnathan Martell <286711055+johnmtll@users.noreply.github.com> Date: Thu, 29 Oct 2026 20:02:07 -0400 Subject: [PATCH 10/11] test: support frozen options on public api surfaces which touch timeoutMS mods --- test/unit/collection.test.ts | 9 +++++++++ test/unit/cursor/list_collections_cursor.test.ts | 14 ++++++++++++++ test/unit/cursor/list_indexes_cursor.test.ts | 16 ++++++++++++++++ test/unit/db.test.ts | 9 +++++++++ 4 files changed, 48 insertions(+) create mode 100644 test/unit/cursor/list_collections_cursor.test.ts create mode 100644 test/unit/cursor/list_indexes_cursor.test.ts diff --git a/test/unit/collection.test.ts b/test/unit/collection.test.ts index 748895b7fac..6704d5e3ed4 100644 --- a/test/unit/collection.test.ts +++ b/test/unit/collection.test.ts @@ -15,6 +15,15 @@ describe('Collection', function () { await cleanup(); }); + context('#listSearchIndexes', () => { + it('does not throw when given frozen options', () => { + const client = new MongoClient('mongodb://localhost:27017'); + const options = Object.freeze({ timeoutMS: 100 }); + + expect(() => client.db().collection('test').listSearchIndexes(options)).to.not.throw(); + }); + }); + context('#aggregate', () => { // general test for aggregate function async function testAggregate(config) { diff --git a/test/unit/cursor/list_collections_cursor.test.ts b/test/unit/cursor/list_collections_cursor.test.ts new file mode 100644 index 00000000000..9346be1422f --- /dev/null +++ b/test/unit/cursor/list_collections_cursor.test.ts @@ -0,0 +1,14 @@ +import { expect } from 'chai'; + +import { ListCollectionsCursor, MongoClient } from '../../mongodb'; + +describe('class ListCollectionsCursor', () => { + context('constructor()', () => { + it('does not throw when given frozen options', () => { + const client = new MongoClient('mongodb://localhost:27017'); + const options = Object.freeze({ timeoutMS: 100 }); + + expect(() => new ListCollectionsCursor(client.db(), {}, options)).to.not.throw(); + }); + }); +}); diff --git a/test/unit/cursor/list_indexes_cursor.test.ts b/test/unit/cursor/list_indexes_cursor.test.ts new file mode 100644 index 00000000000..f8954ceb279 --- /dev/null +++ b/test/unit/cursor/list_indexes_cursor.test.ts @@ -0,0 +1,16 @@ +import { expect } from 'chai'; + +import { ListIndexesCursor, MongoClient } from '../../mongodb'; + +describe('class ListIndexesCursor', () => { + context('constructor()', () => { + it('does not throw when given frozen options', () => { + const client = new MongoClient('mongodb://localhost:27017'); + const options = Object.freeze({ timeoutMS: 100 }); + + expect( + () => new ListIndexesCursor(client.db().collection('test'), options) + ).to.not.throw(); + }); + }); +}); diff --git a/test/unit/db.test.ts b/test/unit/db.test.ts index d9eaebfdbe4..c1172cecb1a 100644 --- a/test/unit/db.test.ts +++ b/test/unit/db.test.ts @@ -3,6 +3,15 @@ import { expect } from 'chai'; import { Db, type DbOptions, MongoClient, ReadPreference } from '../mongodb'; describe('class Db', function () { + describe('runCursorCommand()', function () { + it('does not throw when given frozen options', function () { + const client = new MongoClient('mongodb://localhost:27017'); + const options = Object.freeze({ timeoutMS: 100 }); + + expect(() => client.db().runCursorCommand({ a: 1 }, options)).to.not.throw(); + }); + }); + describe('secondaryOk', function () { const client = new MongoClient('mongodb://localhost:27017'); const secondary_ok = 'secondaryOk'; From b74f7a313a7fa0dbb1c52e9f0b6fb21955d1dca8 Mon Sep 17 00:00:00 2001 From: Johnathan Martell <286711055+johnmtll@users.noreply.github.com> Date: Thu, 29 Oct 2026 20:22:43 -0400 Subject: [PATCH 11/11] chore: lint --- test/unit/cursor/list_indexes_cursor.test.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/test/unit/cursor/list_indexes_cursor.test.ts b/test/unit/cursor/list_indexes_cursor.test.ts index f8954ceb279..9c4a5b21e40 100644 --- a/test/unit/cursor/list_indexes_cursor.test.ts +++ b/test/unit/cursor/list_indexes_cursor.test.ts @@ -8,9 +8,7 @@ describe('class ListIndexesCursor', () => { const client = new MongoClient('mongodb://localhost:27017'); const options = Object.freeze({ timeoutMS: 100 }); - expect( - () => new ListIndexesCursor(client.db().collection('test'), options) - ).to.not.throw(); + expect(() => new ListIndexesCursor(client.db().collection('test'), options)).to.not.throw(); }); }); });