From 4dbd896a02fc99ff622601b8dadc7f18f8ca0cbc Mon Sep 17 00:00:00 2001 From: bailey Date: Wed, 28 Jan 2026 15:23:53 -0700 Subject: [PATCH 1/6] initial POC --- .eslintrc.json | 5 +- src/cmap/auth/gssapi.ts | 11 +-- src/cmap/connection.ts | 21 +++--- src/cmap/handshake/client_metadata.ts | 6 +- src/connection_string.ts | 11 +++ src/index.ts | 1 + src/mongo_client.ts | 72 ++++++++++--------- test/unit/assorted/optional_require.test.ts | 4 +- test/unit/cmap/connect.test.ts | 17 +++-- .../cmap/handshake/client_metadata.test.ts | 60 +++++++++------- test/unit/sdam/topology.test.ts | 8 ++- 11 files changed, 133 insertions(+), 83 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index d009780f372..95c6e99a813 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -276,7 +276,8 @@ "patterns": [ "**/../lib/**", "mongodb-mock-server", - "node:*" + "node:*", + "os" ], "paths": [ { @@ -327,4 +328,4 @@ } } ] -} +} \ No newline at end of file diff --git a/src/cmap/auth/gssapi.ts b/src/cmap/auth/gssapi.ts index d18cb6b360e..0154057919f 100644 --- a/src/cmap/auth/gssapi.ts +++ b/src/cmap/auth/gssapi.ts @@ -1,5 +1,4 @@ import * as dns from 'dns'; -import * as os from 'os'; import { getKerberos, type Kerberos, type KerberosClient } from '../../deps'; import { MongoInvalidArgumentError, MongoMissingCredentialsError } from '../../error'; @@ -69,9 +68,13 @@ export class GSSAPI extends AuthProvider { } } -async function makeKerberosClient(authContext: AuthContext): Promise { - const { hostAddress } = authContext.options; - const { credentials } = authContext; +async function makeKerberosClient({ + options: { + hostAddress, + runtime: { os } + }, + credentials +}: AuthContext): Promise { if (!hostAddress || typeof hostAddress.host !== 'string' || !credentials) { throw new MongoInvalidArgumentError( 'Connection must have host and port and credentials defined.' diff --git a/src/cmap/connection.ts b/src/cmap/connection.ts index 9652e3a5e4f..c53c86d7ed7 100644 --- a/src/cmap/connection.ts +++ b/src/cmap/connection.ts @@ -35,6 +35,7 @@ import { type MongoClientAuthProviders } from '../mongo_client_auth_providers'; import { MongoLoggableComponent, type MongoLogger, SeverityLevel } from '../mongo_logger'; import { type Abortable, type CancellationToken, TypedEventEmitter } from '../mongo_types'; import { ReadPreference, type ReadPreferenceLike } from '../read_preference'; +import { type Runtime } from '../runtime_adapters'; import { ServerType } from '../sdam/common'; import { applySession, type ClientSession, updateSessionFromResponse } from '../sessions'; import { type TimeoutContext, TimeoutError } from '../timeout'; @@ -118,8 +119,8 @@ export interface ProxyOptions { /** @public */ export interface ConnectionOptions extends SupportedNodeConnectionOptions, - StreamDescriptionOptions, - ProxyOptions { + StreamDescriptionOptions, + ProxyOptions { // Internal creation info id: number | ''; generation: number; @@ -143,6 +144,8 @@ export interface ConnectionOptions metadata: Promise; /** @internal */ mongoLogger?: MongoLogger | undefined; + /** @internal */ + runtime: Runtime; } /** @public */ @@ -526,10 +529,10 @@ export class Connection extends TypedEventEmitter { options.documentsReturnedIn == null || !options.raw ? options : { - ...options, - raw: false, - fieldsAsRaw: { [options.documentsReturnedIn]: true } - }; + ...options, + raw: false, + fieldsAsRaw: { [options.documentsReturnedIn]: true } + }; /** MongoDBResponse instance or subclass */ let document: MongoDBResponse | undefined = undefined; @@ -692,9 +695,9 @@ export class Connection extends TypedEventEmitter { options.agreedCompressor === 'none' || !OpCompressedRequest.canCompress(command) ? command : new OpCompressedRequest(command, { - agreedCompressor: options.agreedCompressor ?? 'none', - zlibCompressionLevel: options.zlibCompressionLevel ?? 0 - }); + agreedCompressor: options.agreedCompressor ?? 'none', + zlibCompressionLevel: options.zlibCompressionLevel ?? 0 + }); const buffer = Buffer.concat(await finalCommand.toBin()); diff --git a/src/cmap/handshake/client_metadata.ts b/src/cmap/handshake/client_metadata.ts index 48cb6a47350..3b79e1df48f 100644 --- a/src/cmap/handshake/client_metadata.ts +++ b/src/cmap/handshake/client_metadata.ts @@ -1,4 +1,3 @@ -import * as os from 'os'; import * as process from 'process'; import { BSON, type Document, Int32, NumberUtils } from '../../bson'; @@ -96,7 +95,8 @@ export class LimitedSizeDocument { } } -type MakeClientMetadataOptions = Pick; +type MakeClientMetadataOptions = Pick; + /** * From the specs: * Implementors SHOULD cumulatively update fields in the following order until the document is under the size limit: @@ -107,7 +107,7 @@ type MakeClientMetadataOptions = Pick; */ export async function makeClientMetadata( driverInfoList: DriverInfo[], - { appName = '' }: MakeClientMetadataOptions + { appName = '', runtime: { os } }: MakeClientMetadataOptions ): Promise { const metadataDocument = new LimitedSizeDocument(512); diff --git a/src/connection_string.ts b/src/connection_string.ts index df6dfc607a0..ce63e99ad90 100644 --- a/src/connection_string.ts +++ b/src/connection_string.ts @@ -20,6 +20,7 @@ import { import { MongoLoggableComponent, MongoLogger, SeverityLevel } from './mongo_logger'; import { ReadConcern, type ReadConcernLevel } from './read_concern'; import { ReadPreference, type ReadPreferenceMode } from './read_preference'; +import { type Runtime } from './runtime_adapters'; import { ServerMonitoringMode } from './sdam/monitor'; import type { TagSet } from './sdam/server_description'; import { @@ -538,6 +539,13 @@ export function parseOptions( } ); + const runtime: Runtime = { + // eslint-disable-next-line @typescript-eslint/no-require-imports + os: options.runtimeAdapters?.os ?? require('os') + }; + + mongoOptions.runtime = runtime; + return mongoOptions; } @@ -1061,6 +1069,9 @@ export const OPTIONS = { default: true, type: 'boolean' }, + runtimeAdapters: { + type: 'record' + }, serializeFunctions: { type: 'boolean' }, diff --git a/src/index.ts b/src/index.ts index 8f5c4cfa60e..74803dfa2a0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -562,6 +562,7 @@ export type { ReadPreferenceLikeOptions, ReadPreferenceOptions } from './read_preference'; +export type { OsAdapter, Runtime, RuntimeAdapters } from './runtime_adapters'; export type { ClusterTime } from './sdam/common'; export type { Monitor, diff --git a/src/mongo_client.ts b/src/mongo_client.ts index 970f0f88061..1ac02e93c6e 100644 --- a/src/mongo_client.ts +++ b/src/mongo_client.ts @@ -46,6 +46,7 @@ import { EndSessionsOperation } from './operations/end_sessions'; import { executeOperation } from './operations/execute_operation'; import type { ReadConcern, ReadConcernLevel, ReadConcernLike } from './read_concern'; import { ReadPreference, type ReadPreferenceMode } from './read_preference'; +import { type Runtime, type RuntimeAdapters } from './runtime_adapters'; import type { ServerMonitoringMode } from './sdam/monitor'; import type { TagSet } from './sdam/server_description'; import { DeprioritizedServers, readPreferenceServerSelector } from './sdam/server_selection'; @@ -318,6 +319,8 @@ export interface MongoClientOptions extends BSONSerializeOptions, SupportedNodeC connectionType?: typeof Connection; /** @internal */ __skipPingOnConnect?: boolean; + /** @experimental */ + runtimeAdapters?: RuntimeAdapters; } /** @public */ @@ -1032,39 +1035,39 @@ export class MongoClient extends TypedEventEmitter implements */ export interface MongoOptions extends Required< - Pick< - MongoClientOptions, - | 'autoEncryption' - | 'connectTimeoutMS' - | 'directConnection' - | 'driverInfo' - | 'forceServerObjectId' - | 'minHeartbeatFrequencyMS' - | 'heartbeatFrequencyMS' - | 'localThresholdMS' - | 'maxConnecting' - | 'maxIdleTimeMS' - | 'maxPoolSize' - | 'minPoolSize' - | 'monitorCommands' - | 'noDelay' - | 'pkFactory' - | 'raw' - | 'replicaSet' - | 'retryReads' - | 'retryWrites' - | 'serverSelectionTimeoutMS' - | 'socketTimeoutMS' - | 'srvMaxHosts' - | 'srvServiceName' - | 'tlsAllowInvalidCertificates' - | 'tlsAllowInvalidHostnames' - | 'tlsInsecure' - | 'waitQueueTimeoutMS' - | 'zlibCompressionLevel' - > - >, - SupportedNodeConnectionOptions { + Pick< + MongoClientOptions, + | 'autoEncryption' + | 'connectTimeoutMS' + | 'directConnection' + | 'driverInfo' + | 'forceServerObjectId' + | 'minHeartbeatFrequencyMS' + | 'heartbeatFrequencyMS' + | 'localThresholdMS' + | 'maxConnecting' + | 'maxIdleTimeMS' + | 'maxPoolSize' + | 'minPoolSize' + | 'monitorCommands' + | 'noDelay' + | 'pkFactory' + | 'raw' + | 'replicaSet' + | 'retryReads' + | 'retryWrites' + | 'serverSelectionTimeoutMS' + | 'socketTimeoutMS' + | 'srvMaxHosts' + | 'srvServiceName' + | 'tlsAllowInvalidCertificates' + | 'tlsAllowInvalidHostnames' + | 'tlsInsecure' + | 'waitQueueTimeoutMS' + | 'zlibCompressionLevel' + > + >, + SupportedNodeConnectionOptions { appName?: string; hosts: HostAddress[]; srvHost?: string; @@ -1152,4 +1155,7 @@ export interface MongoOptions timeoutMS?: number; /** @internal */ __skipPingOnConnect?: boolean; + + /** @internal */ + runtime: Runtime; } diff --git a/test/unit/assorted/optional_require.test.ts b/test/unit/assorted/optional_require.test.ts index 5dc579ee304..f6772baf2d5 100644 --- a/test/unit/assorted/optional_require.test.ts +++ b/test/unit/assorted/optional_require.test.ts @@ -41,7 +41,9 @@ describe('optionalRequire', function () { const gssapi = new GSSAPI(); const error = await gssapi - .auth(new AuthContext(null, true, { hostAddress: new HostAddress('a'), credentials: true })) + .auth(new AuthContext(null, true, { + hostAddress: new HostAddress('a'), credentials: true, runtime: { os: require('os') } + })) .then( () => null, e => e diff --git a/test/unit/cmap/connect.test.ts b/test/unit/cmap/connect.test.ts index a97cb7194a6..145d9f0ff3f 100644 --- a/test/unit/cmap/connect.test.ts +++ b/test/unit/cmap/connect.test.ts @@ -210,7 +210,9 @@ describe('Connect Tests', function () { connection: {}, options: { ...CONNECT_DEFAULTS, - metadata: makeClientMetadata([], {}) + metadata: makeClientMetadata([], { + runtime: { os: require('os') } + }) } }; }); @@ -239,7 +241,9 @@ describe('Connect Tests', function () { name: 's'.repeat(128) } ], - { appName: longAppName } + { + appName: longAppName, runtime: { os: require('os') } + } ); const longAuthContext = { connection: {}, @@ -267,7 +271,9 @@ describe('Connect Tests', function () { connection: {}, options: { ...CONNECT_DEFAULTS, - metadata: makeClientMetadata([], {}) + metadata: makeClientMetadata([], { + runtime: { os: require('os') } + }) } }; }); @@ -296,7 +302,10 @@ describe('Connect Tests', function () { name: 's'.repeat(128) } ], - { appName: longAppName } + { + appName: longAppName, + runtime: { os: require('os') } + } ); const longAuthContext = { connection: {}, diff --git a/test/unit/cmap/handshake/client_metadata.test.ts b/test/unit/cmap/handshake/client_metadata.test.ts index 61288384481..b4b19b17204 100644 --- a/test/unit/cmap/handshake/client_metadata.test.ts +++ b/test/unit/cmap/handshake/client_metadata.test.ts @@ -12,6 +12,11 @@ import { makeClientMetadata } from '../../../../src/cmap/handshake/client_metadata'; import { MongoInvalidArgumentError } from '../../../../src/error'; +import { Runtime } from '../../../../src'; + +const runtime: Runtime = { + os: require('os') +}; describe('client metadata module', () => { afterEach(() => sinon.restore()); @@ -141,7 +146,7 @@ describe('client metadata module', () => { describe('makeClientMetadata()', () => { context('when no FAAS environment is detected', () => { it('does not append FAAS metadata', async () => { - const metadata = await makeClientMetadata([], {}); + const metadata = await makeClientMetadata([], { runtime }); expect(metadata).not.to.have.property( 'env', 'faas metadata applied in a non-faas environment' @@ -164,14 +169,14 @@ describe('client metadata module', () => { context('when driverInfo.platform is provided', () => { it('throws an error if driverInfo.platform is too large', async () => { - const error = await makeClientMetadata([{ platform: 'a'.repeat(512) }], {}).catch(e => e); + const error = await makeClientMetadata([{ platform: 'a'.repeat(512) }], { runtime }).catch(e => e); expect(error) .to.be.instanceOf(MongoInvalidArgumentError) .to.match(/platform/); }); it('appends driverInfo.platform to the platform field', async () => { - const metadata = await makeClientMetadata([{ platform: 'myPlatform' }], {}); + const metadata = await makeClientMetadata([{ platform: 'myPlatform' }], { runtime }); expect(metadata).to.deep.equal({ driver: { name: 'nodejs', @@ -190,12 +195,12 @@ describe('client metadata module', () => { context('when driverInfo.name is provided', () => { it('throws an error if driverInfo.name is too large', async () => { - const error = await makeClientMetadata([{ name: 'a'.repeat(512) }], {}).catch(e => e); + const error = await makeClientMetadata([{ name: 'a'.repeat(512) }], { runtime }).catch(e => e); expect(error).to.be.instanceOf(MongoInvalidArgumentError).to.match(/name/); }); it('appends driverInfo.name to the driver.name field', async () => { - const metadata = await makeClientMetadata([{ name: 'myName' }], {}); + const metadata = await makeClientMetadata([{ name: 'myName' }], { runtime }); expect(metadata).to.deep.equal({ driver: { name: 'nodejs|myName', @@ -214,14 +219,14 @@ describe('client metadata module', () => { context('when driverInfo.version is provided', () => { it('throws an error if driverInfo.version is too large', async () => { - const error = await makeClientMetadata([{ version: 'a'.repeat(512) }], {}).catch(e => e); + const error = await makeClientMetadata([{ version: 'a'.repeat(512) }], { runtime }).catch(e => e); expect(error) .to.be.instanceOf(MongoInvalidArgumentError) .to.match(/version/); }); it('appends driverInfo.version to the version field', async () => { - const metadata = await makeClientMetadata([{ version: 'myVersion' }], {}); + const metadata = await makeClientMetadata([{ version: 'myVersion' }], { runtime }); expect(metadata).to.deep.equal({ driver: { name: 'nodejs', @@ -240,7 +245,7 @@ describe('client metadata module', () => { context('when no custom driverInto is provided', () => { it('does not append the driver info to the metadata', async () => { - const metadata = await makeClientMetadata([], {}); + const metadata = await makeClientMetadata([], { runtime }); expect(metadata).to.deep.equal({ driver: { name: 'nodejs', @@ -257,7 +262,7 @@ describe('client metadata module', () => { }); it('does not set the application field', async () => { - const metadata = await makeClientMetadata([], {}); + const metadata = await makeClientMetadata([], { runtime }); expect(metadata).not.to.have.property('application'); }); }); @@ -267,6 +272,7 @@ describe('client metadata module', () => { it('truncates the application name to <=128 bytes', async () => { const longString = 'a'.repeat(300); const metadata = await makeClientMetadata([], { + runtime, appName: longString }); expect(metadata.application?.name).to.be.a('string'); @@ -283,6 +289,7 @@ describe('client metadata module', () => { it('truncates the application name to 129 bytes', async () => { const longString = '€'.repeat(300); const metadata = await makeClientMetadata([], { + runtime, appName: longString }); @@ -298,6 +305,7 @@ describe('client metadata module', () => { context('when the app name is under 128 bytes', () => { it('sets the application name to the value', async () => { const metadata = await makeClientMetadata([], { + runtime, appName: 'myApplication' }); expect(metadata.application?.name).to.equal('myApplication'); @@ -313,37 +321,37 @@ describe('client metadata module', () => { it('sets platform to Deno', async () => { globalThis.Deno = { version: { deno: '1.2.3' } }; - const metadata = await makeClientMetadata([], {}); + const metadata = await makeClientMetadata([], { runtime }); expect(metadata.platform).to.equal('Deno v1.2.3, LE'); }); it('sets platform to Deno with driverInfo.platform', async () => { globalThis.Deno = { version: { deno: '1.2.3' } }; - const metadata = await makeClientMetadata([{ platform: 'myPlatform' }], {}); + const metadata = await makeClientMetadata([{ platform: 'myPlatform' }], { runtime }); expect(metadata.platform).to.equal('Deno v1.2.3, LE|myPlatform'); }); it('ignores version if Deno.version.deno is not a string', async () => { globalThis.Deno = { version: { deno: 1 } }; - const metadata = await makeClientMetadata([], {}); + const metadata = await makeClientMetadata([], { runtime }); expect(metadata.platform).to.equal('Deno v0.0.0-unknown, LE'); }); it('ignores version if Deno.version does not have a deno property', async () => { globalThis.Deno = { version: { somethingElse: '1.2.3' } }; - const metadata = await makeClientMetadata([], {}); + const metadata = await makeClientMetadata([], { runtime }); expect(metadata.platform).to.equal('Deno v0.0.0-unknown, LE'); }); it('ignores version if Deno.version is null', async () => { globalThis.Deno = { version: null }; - const metadata = await makeClientMetadata([], {}); + const metadata = await makeClientMetadata([], { runtime }); expect(metadata.platform).to.equal('Deno v0.0.0-unknown, LE'); }); it('ignores version if Deno is nullish', async () => { globalThis.Deno = null; - const metadata = await makeClientMetadata([], {}); + const metadata = await makeClientMetadata([], { runtime }); expect(metadata.platform).to.equal('Deno v0.0.0-unknown, LE'); }); }); @@ -357,7 +365,7 @@ describe('client metadata module', () => { globalThis.Bun = class { static version = '1.2.3'; }; - const metadata = await makeClientMetadata([], {}); + const metadata = await makeClientMetadata([], { runtime }); expect(metadata.platform).to.equal('Bun v1.2.3, LE'); }); @@ -365,7 +373,7 @@ describe('client metadata module', () => { globalThis.Bun = class { static version = '1.2.3'; }; - const metadata = await makeClientMetadata([{ platform: 'myPlatform' }], {}); + const metadata = await makeClientMetadata([{ platform: 'myPlatform' }], { runtime }); expect(metadata.platform).to.equal('Bun v1.2.3, LE|myPlatform'); }); @@ -373,7 +381,7 @@ describe('client metadata module', () => { globalThis.Bun = class { static version = 1; }; - const metadata = await makeClientMetadata([], {}); + const metadata = await makeClientMetadata([], { runtime }); expect(metadata.platform).to.equal('Bun v0.0.0-unknown, LE'); }); @@ -381,13 +389,13 @@ describe('client metadata module', () => { globalThis.Bun = class { static version = 1; }; - const metadata = await makeClientMetadata([{ platform: 'myPlatform' }], {}); + const metadata = await makeClientMetadata([{ platform: 'myPlatform' }], { runtime }); expect(metadata.platform).to.equal('Bun v0.0.0-unknown, LE|myPlatform'); }); it('ignores version if Bun is nullish', async () => { globalThis.Bun = null; - const metadata = await makeClientMetadata([{ platform: 'myPlatform' }], {}); + const metadata = await makeClientMetadata([{ platform: 'myPlatform' }], { runtime }); expect(metadata.platform).to.equal('Bun v0.0.0-unknown, LE|myPlatform'); }); }); @@ -508,7 +516,7 @@ describe('client metadata module', () => { }); it(`returns ${inspect(outcome)} under env property`, async () => { - const { env } = await makeClientMetadata([], {}); + const { env } = await makeClientMetadata([], { runtime }); expect(env).to.deep.equal(outcome); }); @@ -532,7 +540,7 @@ describe('client metadata module', () => { }); it('does not attach it to the metadata', async () => { - expect(await makeClientMetadata([], {})).not.to.have.nested.property('aws.memory_mb'); + expect(await makeClientMetadata([], { runtime })).not.to.have.nested.property('aws.memory_mb'); }); }); }); @@ -547,7 +555,7 @@ describe('client metadata module', () => { }); it('only includes env.name', async () => { - const metadata = await makeClientMetadata([], {}); + const metadata = await makeClientMetadata([], { runtime }); expect(metadata).to.not.have.nested.property('env.region'); expect(metadata).to.have.nested.property('env.name', 'aws.lambda'); expect(metadata.env).to.have.all.keys('name'); @@ -565,7 +573,7 @@ describe('client metadata module', () => { }); it('only includes env.name', async () => { - const metadata = await makeClientMetadata([], {}); + const metadata = await makeClientMetadata([], { runtime }); expect(metadata).to.have.property('env'); expect(metadata).to.have.nested.property('env.region', 'abc'); expect(metadata.os).to.have.all.keys('type'); @@ -582,7 +590,7 @@ describe('client metadata module', () => { }); it('omits os information', async () => { - const metadata = await makeClientMetadata([], {}); + const metadata = await makeClientMetadata([], { runtime }); expect(metadata).to.not.have.property('os'); }); }); @@ -598,7 +606,7 @@ describe('client metadata module', () => { }); it('omits the faas env', async () => { - const metadata = await makeClientMetadata([{ name: 'a'.repeat(350) }], {}); + const metadata = await makeClientMetadata([{ name: 'a'.repeat(350) }], { runtime }); expect(metadata).to.not.have.property('env'); }); }); diff --git a/test/unit/sdam/topology.test.ts b/test/unit/sdam/topology.test.ts index 1444e1a40c4..1dab4f6a938 100644 --- a/test/unit/sdam/topology.test.ts +++ b/test/unit/sdam/topology.test.ts @@ -29,6 +29,11 @@ import { TimeoutContext } from '../../../src/timeout'; import { isHello, ns } from '../../../src/utils'; import * as mock from '../../tools/mongodb-mock/index'; import { topologyWithPlaceholderClient } from '../../tools/utils'; +import { Runtime } from '../../../src'; + +const runtime: Runtime = { + os: require('os') +}; describe('Topology (unit)', function () { let client, topology; @@ -56,6 +61,7 @@ describe('Topology (unit)', function () { it('should correctly pass appname', async function () { const topology: Topology = topologyWithPlaceholderClient([`localhost:27017`], { metadata: makeClientMetadata([], { + runtime, appName: 'My application name' }) }); @@ -120,7 +126,7 @@ describe('Topology (unit)', function () { }); const server = await topology.selectServer('primary', { timeoutContext: ctx, - operationName: 'none' + operationName: 'none', }); const err = await server .command(new RunCursorCommandOperation(ns('admin.$cmd'), { ping: 1 }, {}), ctx) From b790827671322da3d67e0ae7c23841c93b8a32dd Mon Sep 17 00:00:00 2001 From: bailey Date: Wed, 28 Jan 2026 15:28:13 -0700 Subject: [PATCH 2/6] lint --- src/cmap/connection.ts | 18 ++--- src/mongo_client.ts | 66 +++++++++---------- src/runtime_adapters.ts | 25 +++++++ test/tools/utils.ts | 6 ++ test/unit/assorted/optional_require.test.ts | 11 +++- test/unit/cmap/connect.test.ts | 10 +-- .../cmap/handshake/client_metadata.test.ts | 21 +++--- test/unit/sdam/topology.test.ts | 9 +-- 8 files changed, 101 insertions(+), 65 deletions(-) create mode 100644 src/runtime_adapters.ts diff --git a/src/cmap/connection.ts b/src/cmap/connection.ts index c53c86d7ed7..dfffb15daeb 100644 --- a/src/cmap/connection.ts +++ b/src/cmap/connection.ts @@ -119,8 +119,8 @@ export interface ProxyOptions { /** @public */ export interface ConnectionOptions extends SupportedNodeConnectionOptions, - StreamDescriptionOptions, - ProxyOptions { + StreamDescriptionOptions, + ProxyOptions { // Internal creation info id: number | ''; generation: number; @@ -529,10 +529,10 @@ export class Connection extends TypedEventEmitter { options.documentsReturnedIn == null || !options.raw ? options : { - ...options, - raw: false, - fieldsAsRaw: { [options.documentsReturnedIn]: true } - }; + ...options, + raw: false, + fieldsAsRaw: { [options.documentsReturnedIn]: true } + }; /** MongoDBResponse instance or subclass */ let document: MongoDBResponse | undefined = undefined; @@ -695,9 +695,9 @@ export class Connection extends TypedEventEmitter { options.agreedCompressor === 'none' || !OpCompressedRequest.canCompress(command) ? command : new OpCompressedRequest(command, { - agreedCompressor: options.agreedCompressor ?? 'none', - zlibCompressionLevel: options.zlibCompressionLevel ?? 0 - }); + agreedCompressor: options.agreedCompressor ?? 'none', + zlibCompressionLevel: options.zlibCompressionLevel ?? 0 + }); const buffer = Buffer.concat(await finalCommand.toBin()); diff --git a/src/mongo_client.ts b/src/mongo_client.ts index 1ac02e93c6e..75a2bc9fd2a 100644 --- a/src/mongo_client.ts +++ b/src/mongo_client.ts @@ -1035,39 +1035,39 @@ export class MongoClient extends TypedEventEmitter implements */ export interface MongoOptions extends Required< - Pick< - MongoClientOptions, - | 'autoEncryption' - | 'connectTimeoutMS' - | 'directConnection' - | 'driverInfo' - | 'forceServerObjectId' - | 'minHeartbeatFrequencyMS' - | 'heartbeatFrequencyMS' - | 'localThresholdMS' - | 'maxConnecting' - | 'maxIdleTimeMS' - | 'maxPoolSize' - | 'minPoolSize' - | 'monitorCommands' - | 'noDelay' - | 'pkFactory' - | 'raw' - | 'replicaSet' - | 'retryReads' - | 'retryWrites' - | 'serverSelectionTimeoutMS' - | 'socketTimeoutMS' - | 'srvMaxHosts' - | 'srvServiceName' - | 'tlsAllowInvalidCertificates' - | 'tlsAllowInvalidHostnames' - | 'tlsInsecure' - | 'waitQueueTimeoutMS' - | 'zlibCompressionLevel' - > - >, - SupportedNodeConnectionOptions { + Pick< + MongoClientOptions, + | 'autoEncryption' + | 'connectTimeoutMS' + | 'directConnection' + | 'driverInfo' + | 'forceServerObjectId' + | 'minHeartbeatFrequencyMS' + | 'heartbeatFrequencyMS' + | 'localThresholdMS' + | 'maxConnecting' + | 'maxIdleTimeMS' + | 'maxPoolSize' + | 'minPoolSize' + | 'monitorCommands' + | 'noDelay' + | 'pkFactory' + | 'raw' + | 'replicaSet' + | 'retryReads' + | 'retryWrites' + | 'serverSelectionTimeoutMS' + | 'socketTimeoutMS' + | 'srvMaxHosts' + | 'srvServiceName' + | 'tlsAllowInvalidCertificates' + | 'tlsAllowInvalidHostnames' + | 'tlsInsecure' + | 'waitQueueTimeoutMS' + | 'zlibCompressionLevel' + > + >, + SupportedNodeConnectionOptions { appName?: string; hosts: HostAddress[]; srvHost?: string; diff --git a/src/runtime_adapters.ts b/src/runtime_adapters.ts new file mode 100644 index 00000000000..3ad5e8751cc --- /dev/null +++ b/src/runtime_adapters.ts @@ -0,0 +1,25 @@ +/** + * @public + * @experimental + */ +export type OsAdapter = Pick; + +/** + * @public + * @experimental + * + * This type represents the interface that the driver needs from the runtime in order to function. + */ +export interface RuntimeAdapters { + os?: OsAdapter; +} + +/** + * @internal + * + * Represents a complete, parsed set of runtime adapters. After options parsing, all adapters + * are always present (either using the user's provided adapter, or defaulting to Nodejs' module). + */ +export interface Runtime { + os: OsAdapter; +} diff --git a/test/tools/utils.ts b/test/tools/utils.ts index 5a23fdd9706..97def9d1798 100644 --- a/test/tools/utils.ts +++ b/test/tools/utils.ts @@ -7,6 +7,7 @@ import * as path from 'node:path'; import { EJSON } from 'bson'; import * as BSON from 'bson'; import { expect } from 'chai'; +import * as os from 'os'; import * as process from 'process'; import { Readable } from 'stream'; import { setTimeout } from 'timers'; @@ -18,6 +19,7 @@ import { type HostAddress, MongoClient, type MongoClientOptions, + type Runtime, type ServerApiVersion, type TopologyOptions } from '../../src'; @@ -604,3 +606,7 @@ export function configureMongocryptdSpawnHooks( port }; } + +export const runtime: Runtime = { + os +}; diff --git a/test/unit/assorted/optional_require.test.ts b/test/unit/assorted/optional_require.test.ts index f6772baf2d5..463f7f95ffb 100644 --- a/test/unit/assorted/optional_require.test.ts +++ b/test/unit/assorted/optional_require.test.ts @@ -7,6 +7,7 @@ import { GSSAPI } from '../../../src/cmap/auth/gssapi'; import { compress } from '../../../src/cmap/wire_protocol/compression'; import { MongoMissingDependencyError } from '../../../src/error'; import { HostAddress } from '../../../src/utils'; +import { runtime } from '../../tools/utils'; function moduleExistsSync(moduleName) { return existsSync(resolve(__dirname, `../../../node_modules/${moduleName}`)); @@ -41,9 +42,13 @@ describe('optionalRequire', function () { const gssapi = new GSSAPI(); const error = await gssapi - .auth(new AuthContext(null, true, { - hostAddress: new HostAddress('a'), credentials: true, runtime: { os: require('os') } - })) + .auth( + new AuthContext(null, true, { + hostAddress: new HostAddress('a'), + credentials: true, + runtime + }) + ) .then( () => null, e => e diff --git a/test/unit/cmap/connect.test.ts b/test/unit/cmap/connect.test.ts index 145d9f0ff3f..cd205de976c 100644 --- a/test/unit/cmap/connect.test.ts +++ b/test/unit/cmap/connect.test.ts @@ -15,6 +15,7 @@ import { CancellationToken } from '../../../src/mongo_types'; import { HostAddress, isHello } from '../../../src/utils'; import { genClusterTime } from '../../tools/common'; import * as mock from '../../tools/mongodb-mock/index'; +import { runtime } from '../../tools/utils'; const CONNECT_DEFAULTS = { id: 1, @@ -211,7 +212,7 @@ describe('Connect Tests', function () { options: { ...CONNECT_DEFAULTS, metadata: makeClientMetadata([], { - runtime: { os: require('os') } + runtime }) } }; @@ -242,7 +243,8 @@ describe('Connect Tests', function () { } ], { - appName: longAppName, runtime: { os: require('os') } + appName: longAppName, + runtime } ); const longAuthContext = { @@ -272,7 +274,7 @@ describe('Connect Tests', function () { options: { ...CONNECT_DEFAULTS, metadata: makeClientMetadata([], { - runtime: { os: require('os') } + runtime }) } }; @@ -304,7 +306,7 @@ describe('Connect Tests', function () { ], { appName: longAppName, - runtime: { os: require('os') } + runtime } ); const longAuthContext = { diff --git a/test/unit/cmap/handshake/client_metadata.test.ts b/test/unit/cmap/handshake/client_metadata.test.ts index b4b19b17204..87fd3efcfbb 100644 --- a/test/unit/cmap/handshake/client_metadata.test.ts +++ b/test/unit/cmap/handshake/client_metadata.test.ts @@ -12,11 +12,6 @@ import { makeClientMetadata } from '../../../../src/cmap/handshake/client_metadata'; import { MongoInvalidArgumentError } from '../../../../src/error'; -import { Runtime } from '../../../../src'; - -const runtime: Runtime = { - os: require('os') -}; describe('client metadata module', () => { afterEach(() => sinon.restore()); @@ -169,7 +164,9 @@ describe('client metadata module', () => { context('when driverInfo.platform is provided', () => { it('throws an error if driverInfo.platform is too large', async () => { - const error = await makeClientMetadata([{ platform: 'a'.repeat(512) }], { runtime }).catch(e => e); + const error = await makeClientMetadata([{ platform: 'a'.repeat(512) }], { runtime }).catch( + e => e + ); expect(error) .to.be.instanceOf(MongoInvalidArgumentError) .to.match(/platform/); @@ -195,7 +192,9 @@ describe('client metadata module', () => { context('when driverInfo.name is provided', () => { it('throws an error if driverInfo.name is too large', async () => { - const error = await makeClientMetadata([{ name: 'a'.repeat(512) }], { runtime }).catch(e => e); + const error = await makeClientMetadata([{ name: 'a'.repeat(512) }], { runtime }).catch( + e => e + ); expect(error).to.be.instanceOf(MongoInvalidArgumentError).to.match(/name/); }); @@ -219,7 +218,9 @@ describe('client metadata module', () => { context('when driverInfo.version is provided', () => { it('throws an error if driverInfo.version is too large', async () => { - const error = await makeClientMetadata([{ version: 'a'.repeat(512) }], { runtime }).catch(e => e); + const error = await makeClientMetadata([{ version: 'a'.repeat(512) }], { runtime }).catch( + e => e + ); expect(error) .to.be.instanceOf(MongoInvalidArgumentError) .to.match(/version/); @@ -540,7 +541,9 @@ describe('client metadata module', () => { }); it('does not attach it to the metadata', async () => { - expect(await makeClientMetadata([], { runtime })).not.to.have.nested.property('aws.memory_mb'); + expect(await makeClientMetadata([], { runtime })).not.to.have.nested.property( + 'aws.memory_mb' + ); }); }); }); diff --git a/test/unit/sdam/topology.test.ts b/test/unit/sdam/topology.test.ts index 1dab4f6a938..8db64288bd9 100644 --- a/test/unit/sdam/topology.test.ts +++ b/test/unit/sdam/topology.test.ts @@ -28,12 +28,7 @@ import { TopologyDescription } from '../../../src/sdam/topology_description'; import { TimeoutContext } from '../../../src/timeout'; import { isHello, ns } from '../../../src/utils'; import * as mock from '../../tools/mongodb-mock/index'; -import { topologyWithPlaceholderClient } from '../../tools/utils'; -import { Runtime } from '../../../src'; - -const runtime: Runtime = { - os: require('os') -}; +import { runtime, topologyWithPlaceholderClient } from '../../tools/utils'; describe('Topology (unit)', function () { let client, topology; @@ -126,7 +121,7 @@ describe('Topology (unit)', function () { }); const server = await topology.selectServer('primary', { timeoutContext: ctx, - operationName: 'none', + operationName: 'none' }); const err = await server .command(new RunCursorCommandOperation(ns('admin.$cmd'), { ping: 1 }, {}), ctx) From 1d1c89ec58a6a4b5fa2ef4c866d82503a108bdd1 Mon Sep 17 00:00:00 2001 From: bailey Date: Wed, 28 Jan 2026 15:33:38 -0700 Subject: [PATCH 3/6] add adapter unit test --- test/unit/runtime_adapters.test.ts | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 test/unit/runtime_adapters.test.ts diff --git a/test/unit/runtime_adapters.test.ts b/test/unit/runtime_adapters.test.ts new file mode 100644 index 00000000000..d7ee11a70f7 --- /dev/null +++ b/test/unit/runtime_adapters.test.ts @@ -0,0 +1,30 @@ +import { expect } from "chai" +import { MongoClient, OsAdapter } from "../../src" +import * as os from 'os'; + +describe('Runtime Adapters tests', function () { + describe('`os`', function () { + describe('when no os adapter is provided', function () { + it(`defaults to Node's os module`, function () { + const client = new MongoClient('mongodb://localhost:27017'); + + expect(client.options.runtime.os).to.equal(os); + }) + }) + + describe('when an os adapter is provided', function () { + it(`uses the user provided adapter`, function () { + const osAdapter: OsAdapter = { + ...os + }; + const client = new MongoClient('mongodb://localhost:27017', { + runtimeAdapters: { + os: osAdapter + } + }); + + expect(client.options.runtime.os).to.equal(osAdapter); + }) + }) + }) +}) \ No newline at end of file From 58d8976ce7f3d0b7580859c9ded15871e77c6fd1 Mon Sep 17 00:00:00 2001 From: bailey Date: Wed, 28 Jan 2026 15:46:30 -0700 Subject: [PATCH 4/6] unit tests and integration tests --- .../connection.test.ts | 15 ++++-- .../cmap/handshake/client_metadata.test.ts | 1 + test/unit/runtime_adapters.test.ts | 49 ++++++++++--------- 3 files changed, 37 insertions(+), 28 deletions(-) diff --git a/test/integration/connection-monitoring-and-pooling/connection.test.ts b/test/integration/connection-monitoring-and-pooling/connection.test.ts index a20f36c7b6e..b6aaf759691 100644 --- a/test/integration/connection-monitoring-and-pooling/connection.test.ts +++ b/test/integration/connection-monitoring-and-pooling/connection.test.ts @@ -22,7 +22,7 @@ import { LEGACY_HELLO_COMMAND } from '../../../src/constants'; import { Topology } from '../../../src/sdam/topology'; import { HostAddress, ns } from '../../../src/utils'; import * as mock from '../../tools/mongodb-mock/index'; -import { processTick, sleep } from '../../tools/utils'; +import { processTick, runtime, sleep } from '../../tools/utils'; import { assert as test, setupDatabase } from '../shared'; const commonConnectOptions = { @@ -49,7 +49,10 @@ describe('Connection', function () { ...commonConnectOptions, connectionType: Connection, ...this.configuration.options, - metadata: makeClientMetadata([], {}) + metadata: makeClientMetadata([], { + runtime + }), + runtime }; let conn; @@ -71,7 +74,8 @@ describe('Connection', function () { connectionType: Connection, ...this.configuration.options, monitorCommands: true, - metadata: makeClientMetadata([], {}) + runtime, + metadata: makeClientMetadata([], { runtime }) }; let conn; @@ -102,7 +106,10 @@ describe('Connection', function () { connectionType: Connection, ...this.configuration.options, monitorCommands: true, - metadata: makeClientMetadata([], {}) + runtime, + metadata: makeClientMetadata([], { + runtime + }) }; let conn; diff --git a/test/unit/cmap/handshake/client_metadata.test.ts b/test/unit/cmap/handshake/client_metadata.test.ts index 87fd3efcfbb..a7c5e864d7e 100644 --- a/test/unit/cmap/handshake/client_metadata.test.ts +++ b/test/unit/cmap/handshake/client_metadata.test.ts @@ -12,6 +12,7 @@ import { makeClientMetadata } from '../../../../src/cmap/handshake/client_metadata'; import { MongoInvalidArgumentError } from '../../../../src/error'; +import { runtime } from '../../../tools/utils'; describe('client metadata module', () => { afterEach(() => sinon.restore()); diff --git a/test/unit/runtime_adapters.test.ts b/test/unit/runtime_adapters.test.ts index d7ee11a70f7..3980f9f1d71 100644 --- a/test/unit/runtime_adapters.test.ts +++ b/test/unit/runtime_adapters.test.ts @@ -1,30 +1,31 @@ -import { expect } from "chai" -import { MongoClient, OsAdapter } from "../../src" +import { expect } from 'chai'; import * as os from 'os'; +import { MongoClient, type OsAdapter } from '../../src'; + describe('Runtime Adapters tests', function () { - describe('`os`', function () { - describe('when no os adapter is provided', function () { - it(`defaults to Node's os module`, function () { - const client = new MongoClient('mongodb://localhost:27017'); + describe('`os`', function () { + describe('when no os adapter is provided', function () { + it(`defaults to Node's os module`, function () { + const client = new MongoClient('mongodb://localhost:27017'); - expect(client.options.runtime.os).to.equal(os); - }) - }) + expect(client.options.runtime.os).to.equal(os); + }); + }); - describe('when an os adapter is provided', function () { - it(`uses the user provided adapter`, function () { - const osAdapter: OsAdapter = { - ...os - }; - const client = new MongoClient('mongodb://localhost:27017', { - runtimeAdapters: { - os: osAdapter - } - }); + describe('when an os adapter is provided', function () { + it(`uses the user provided adapter`, function () { + const osAdapter: OsAdapter = { + ...os + }; + const client = new MongoClient('mongodb://localhost:27017', { + runtimeAdapters: { + os: osAdapter + } + }); - expect(client.options.runtime.os).to.equal(osAdapter); - }) - }) - }) -}) \ No newline at end of file + expect(client.options.runtime.os).to.equal(osAdapter); + }); + }); + }); +}); From 9e0e0ff1ca9fbdad1ccfe49d6dfcabea10738787 Mon Sep 17 00:00:00 2001 From: bailey Date: Mon, 2 Feb 2026 11:21:42 -0700 Subject: [PATCH 5/6] cleanup implementation, fix tests --- src/connection_string.ts | 9 ++------- src/mongo_client.ts | 6 +++++- src/runtime_adapters.ts | 29 ++++++++++++++++++++++++++--- test/tools/utils.ts | 9 +++++---- 4 files changed, 38 insertions(+), 15 deletions(-) diff --git a/src/connection_string.ts b/src/connection_string.ts index ce63e99ad90..06315a96869 100644 --- a/src/connection_string.ts +++ b/src/connection_string.ts @@ -20,7 +20,7 @@ import { import { MongoLoggableComponent, MongoLogger, SeverityLevel } from './mongo_logger'; import { ReadConcern, type ReadConcernLevel } from './read_concern'; import { ReadPreference, type ReadPreferenceMode } from './read_preference'; -import { type Runtime } from './runtime_adapters'; +import { resolveRuntimeAdapters } from './runtime_adapters'; import { ServerMonitoringMode } from './sdam/monitor'; import type { TagSet } from './sdam/server_description'; import { @@ -539,12 +539,7 @@ export function parseOptions( } ); - const runtime: Runtime = { - // eslint-disable-next-line @typescript-eslint/no-require-imports - os: options.runtimeAdapters?.os ?? require('os') - }; - - mongoOptions.runtime = runtime; + mongoOptions.runtime = resolveRuntimeAdapters(options); return mongoOptions; } diff --git a/src/mongo_client.ts b/src/mongo_client.ts index 75a2bc9fd2a..87d969fee93 100644 --- a/src/mongo_client.ts +++ b/src/mongo_client.ts @@ -319,7 +319,11 @@ export interface MongoClientOptions extends BSONSerializeOptions, SupportedNodeC connectionType?: typeof Connection; /** @internal */ __skipPingOnConnect?: boolean; - /** @experimental */ + /** + * @experimental + * + * If provided, any adapters provided will be used in place of the corresponding Node.js module. + */ runtimeAdapters?: RuntimeAdapters; } diff --git a/src/runtime_adapters.ts b/src/runtime_adapters.ts index 3ad5e8751cc..43f48f3934a 100644 --- a/src/runtime_adapters.ts +++ b/src/runtime_adapters.ts @@ -1,14 +1,24 @@ +/* eslint-disable no-restricted-imports */ +// We squash the restricted import errors here because we are using type-only imports, which +// do not impact the driver's actual runtime dependencies. + +import type * as os from 'os'; + +import { type MongoClientOptions } from './mongo_client'; + /** * @public * @experimental + * + * Represents the set of dependencies that the driver uses from the [Node.js OS module](https://nodejs.org/api/os.html). */ -export type OsAdapter = Pick; +export type OsAdapter = Pick; /** * @public * @experimental * - * This type represents the interface that the driver needs from the runtime in order to function. + * This type represents the set of dependencies that the driver needs from the Javascript runtime in order to function. */ export interface RuntimeAdapters { os?: OsAdapter; @@ -18,8 +28,21 @@ export interface RuntimeAdapters { * @internal * * Represents a complete, parsed set of runtime adapters. After options parsing, all adapters - * are always present (either using the user's provided adapter, or defaulting to Nodejs' module). + * are always present (either using the user's provided adapter, or defaulting to the Node.js module). */ export interface Runtime { os: OsAdapter; } + +/** + * @internal + * + * Given a MongoClientOptions, this function resolves the set of runtime options, providing Nodejs implementations if + * not provided by in `options`, and returns a `Runtime`. + */ +export function resolveRuntimeAdapters(options: MongoClientOptions): Runtime { + return { + // eslint-disable-next-line @typescript-eslint/no-require-imports + os: options.runtimeAdapters?.os ?? require('os') + }; +} diff --git a/test/tools/utils.ts b/test/tools/utils.ts index 97def9d1798..d3948d773cd 100644 --- a/test/tools/utils.ts +++ b/test/tools/utils.ts @@ -7,7 +7,6 @@ import * as path from 'node:path'; import { EJSON } from 'bson'; import * as BSON from 'bson'; import { expect } from 'chai'; -import * as os from 'os'; import * as process from 'process'; import { Readable } from 'stream'; import { setTimeout } from 'timers'; @@ -24,6 +23,7 @@ import { type TopologyOptions } from '../../src'; import { OP_MSG } from '../../src/cmap/wire_protocol/constants'; +import { resolveRuntimeAdapters } from '../../src/runtime_adapters'; import { Topology } from '../../src/sdam/topology'; import { processTimeMS } from '../../src/utils'; import { type TestConfiguration } from './runner/config'; @@ -607,6 +607,7 @@ export function configureMongocryptdSpawnHooks( }; } -export const runtime: Runtime = { - os -}; +/** + * A `Runtime` that resolves to entirely Nodejs modules, useful when tests must provide a default `runtime` object to an API. + */ +export const runtime: Runtime = resolveRuntimeAdapters({}); From 0738ef88f3a7d519d049f7c88cec7093b975d9e9 Mon Sep 17 00:00:00 2001 From: bailey Date: Mon, 2 Feb 2026 13:30:18 -0700 Subject: [PATCH 6/6] comments --- src/runtime_adapters.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/runtime_adapters.ts b/src/runtime_adapters.ts index 43f48f3934a..bb998d4bd09 100644 --- a/src/runtime_adapters.ts +++ b/src/runtime_adapters.ts @@ -1,6 +1,8 @@ -/* eslint-disable no-restricted-imports */ +/* eslint-disable no-restricted-imports, @typescript-eslint/no-require-imports */ + // We squash the restricted import errors here because we are using type-only imports, which // do not impact the driver's actual runtime dependencies. +// We also allow restricted imports in this file, because we expect this file to be the only place actually importing restricted Node APIs. import type * as os from 'os'; @@ -42,7 +44,6 @@ export interface Runtime { */ export function resolveRuntimeAdapters(options: MongoClientOptions): Runtime { return { - // eslint-disable-next-line @typescript-eslint/no-require-imports os: options.runtimeAdapters?.os ?? require('os') }; }