From 8be6c4f7555c7b319dca26a360cb299d36bd47ff Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Wed, 24 Jul 2024 15:41:53 -0600 Subject: [PATCH 01/17] foo bar baz --- .eslintignore | 2 + .eslintrc.json | 3 +- .evergreen/config.in.yml | 27 ++++ .evergreen/config.yml | 129 +++++++++++++++++- .evergreen/generate_evergreen_tasks.js | 114 +++++++++++----- .evergreen/install-dependencies.sh | 4 + .../run-resource-management-smoke-tests.sh | 15 ++ .evergreen/run-resource-management.sh | 5 + .evergreen/run-typescript.sh | 8 +- api-extractor.json | 3 +- etc/clean_definition_files.cjs | 2 +- package.json | 7 +- src/beta.ts | 1 + src/change_stream.ts | 18 ++- src/cursor/abstract_cursor.ts | 18 ++- src/index.ts | 1 + src/mongo_client.ts | 11 +- src/resource_management.ts | 57 ++++++++ src/sessions.ts | 13 +- test/explicit-resource-management/.gitignore | 2 + .../.mocharc.json | 12 ++ .../explicit-resource-management/main.test.ts | 123 +++++++++++++++++ .../explicit-resource-management/package.json | 24 ++++ .../tsconfig.json | 47 +++++++ test/manual/resource_management.test.ts | 89 ++++++++++++ test/mongodb.ts | 1 + test/unit/index.test.ts | 1 + 27 files changed, 679 insertions(+), 58 deletions(-) create mode 100644 .evergreen/run-resource-management-smoke-tests.sh create mode 100644 .evergreen/run-resource-management.sh create mode 100644 src/beta.ts create mode 100644 src/resource_management.ts create mode 100644 test/explicit-resource-management/.gitignore create mode 100644 test/explicit-resource-management/.mocharc.json create mode 100644 test/explicit-resource-management/main.test.ts create mode 100644 test/explicit-resource-management/package.json create mode 100644 test/explicit-resource-management/tsconfig.json create mode 100644 test/manual/resource_management.test.ts diff --git a/.eslintignore b/.eslintignore index 39a231fa1fc..4689356e005 100644 --- a/.eslintignore +++ b/.eslintignore @@ -2,3 +2,5 @@ lib test/disabled !etc/docs + +test/explicit-resource-management diff --git a/.eslintrc.json b/.eslintrc.json index 8f19ffd9099..e959e07814e 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -271,7 +271,8 @@ { // Settings for generated definition files "files": [ - "mongodb.d.ts" + "**/*.d.ts", + "lib/*.d.ts" ], "parser": "@typescript-eslint/parser", "rules": { diff --git a/.evergreen/config.in.yml b/.evergreen/config.in.yml index b4d1c00ac7b..f65562ed74f 100644 --- a/.evergreen/config.in.yml +++ b/.evergreen/config.in.yml @@ -348,10 +348,36 @@ functions: PROJECT_DIRECTORY: ${PROJECT_DIRECTORY} TS_VERSION: ${TS_VERSION} TS_CHECK: CHECK_TYPES + TYPES_VERSION: ${TYPES_VERSION} binary: bash args: - "${PROJECT_DIRECTORY}/.evergreen/run-typescript.sh" + "check resource management": + - command: subprocess.exec + type: test + params: + working_dir: "src" + timeout_secs: 60 + env: + PROJECT_DIRECTORY: ${PROJECT_DIRECTORY} + binary: bash + args: + - "${PROJECT_DIRECTORY}/.evergreen/run-resource-management.sh" + + "check resource management smoke tests": + - command: subprocess.exec + type: test + params: + working_dir: "src" + timeout_secs: 60 + env: + PROJECT_DIRECTORY: ${PROJECT_DIRECTORY} + MONGODB_URI: ${MONGODB_URI} + binary: bash + args: + - "${PROJECT_DIRECTORY}/.evergreen/run-resource-management-smoke-tests.sh" + "compile driver": - command: subprocess.exec type: test @@ -362,6 +388,7 @@ functions: PROJECT_DIRECTORY: ${PROJECT_DIRECTORY} TS_VERSION: ${TS_VERSION} TS_CHECK: COMPILE_DRIVER + TYPES_VERSION: ${TYPES_VERSION} binary: bash args: - "${PROJECT_DIRECTORY}/.evergreen/run-typescript.sh" diff --git a/.evergreen/config.yml b/.evergreen/config.yml index 184828452b9..376040942ee 100644 --- a/.evergreen/config.yml +++ b/.evergreen/config.yml @@ -312,9 +312,33 @@ functions: PROJECT_DIRECTORY: ${PROJECT_DIRECTORY} TS_VERSION: ${TS_VERSION} TS_CHECK: CHECK_TYPES + TYPES_VERSION: ${TYPES_VERSION} binary: bash args: - ${PROJECT_DIRECTORY}/.evergreen/run-typescript.sh + check resource management: + - command: subprocess.exec + type: test + params: + working_dir: src + timeout_secs: 60 + env: + PROJECT_DIRECTORY: ${PROJECT_DIRECTORY} + binary: bash + args: + - ${PROJECT_DIRECTORY}/.evergreen/run-resource-management.sh + check resource management smoke tests: + - command: subprocess.exec + type: test + params: + working_dir: src + timeout_secs: 60 + env: + PROJECT_DIRECTORY: ${PROJECT_DIRECTORY} + MONGODB_URI: ${MONGODB_URI} + binary: bash + args: + - ${PROJECT_DIRECTORY}/.evergreen/run-resource-management-smoke-tests.sh compile driver: - command: subprocess.exec type: test @@ -325,6 +349,7 @@ functions: PROJECT_DIRECTORY: ${PROJECT_DIRECTORY} TS_VERSION: ${TS_VERSION} TS_CHECK: COMPILE_DRIVER + TYPES_VERSION: ${TYPES_VERSION} binary: bash args: - ${PROJECT_DIRECTORY}/.evergreen/run-typescript.sh @@ -3459,7 +3484,45 @@ tasks: - {key: NPM_VERSION, value: '9'} - func: install dependencies - func: run lint checks - - name: check-types-typescript-next + - name: run-resource-management-no-async-dispose + tags: + - resource-management + commands: + - command: expansions.update + type: setup + params: + updates: + - {key: NODE_LTS_VERSION, value: v16.20.2} + - {key: NPM_VERSION, value: '9'} + - func: install dependencies + - func: check resource management + - name: run-resource-management-async-dispose + tags: + - resource-management + commands: + - command: expansions.update + type: setup + params: + updates: + - {key: NODE_LTS_VERSION, value: latest} + - {key: NPM_VERSION, value: '9'} + - func: install dependencies + - func: check resource management + - name: test-explicit-resource-management-smoke-tests + tags: + - resource-management + commands: + - command: expansions.update + type: setup + params: + updates: + - {key: VERSION, value: latest} + - {key: TOPOLOGY, value: replica_set} + - {key: NODE_LTS_VERSION, value: latest} + - func: install dependencies + - func: bootstrap mongo-orchestration + - func: check resource management smoke tests + - name: check-types-typescript-next-node-types-20.14.10 tags: - check-types-typescript-next - typescript-compilation @@ -3471,11 +3534,12 @@ tasks: - {key: NODE_LTS_VERSION, value: '16'} - {key: NPM_VERSION, value: '9'} - {key: TS_VERSION, value: next} + - {key: TYPES_VERSION, value: 20.14.10} - func: install dependencies - func: check types - - name: compile-driver-typescript-current + - name: check-types-typescript-current-node-types-20.14.10 tags: - - compile-driver-typescript-current + - check-types-typescript-current - typescript-compilation commands: - command: expansions.update @@ -3485,9 +3549,25 @@ tasks: - {key: NODE_LTS_VERSION, value: '16'} - {key: NPM_VERSION, value: '9'} - {key: TS_VERSION, value: current} + - {key: TYPES_VERSION, value: 20.14.10} - func: install dependencies - - func: compile driver - - name: check-types-typescript-current + - func: check types + - name: check-types-typescript-next-node-types-16.x + tags: + - check-types-typescript-next + - typescript-compilation + commands: + - command: expansions.update + type: setup + params: + updates: + - {key: NODE_LTS_VERSION, value: '16'} + - {key: NPM_VERSION, value: '9'} + - {key: TS_VERSION, value: next} + - {key: TYPES_VERSION, value: 16.x} + - func: install dependencies + - func: check types + - name: check-types-typescript-current-node-types-16.x tags: - check-types-typescript-current - typescript-compilation @@ -3499,9 +3579,10 @@ tasks: - {key: NODE_LTS_VERSION, value: '16'} - {key: NPM_VERSION, value: '9'} - {key: TS_VERSION, value: current} + - {key: TYPES_VERSION, value: 16.x} - func: install dependencies - func: check types - - name: check-types-typescript-4.4 + - name: check-types-typescript-4.4-node-types-18.11.9 tags: - check-types-typescript-4.4 - typescript-compilation @@ -3513,8 +3594,39 @@ tasks: - {key: NODE_LTS_VERSION, value: '16'} - {key: NPM_VERSION, value: '9'} - {key: TS_VERSION, value: '4.4'} + - {key: TYPES_VERSION, value: 18.11.9} - func: install dependencies - func: check types + - name: compile-driver-typescript-current-node-types-20.14.10 + tags: + - compile-driver-typescript-current + - typescript-compilation + commands: + - command: expansions.update + type: setup + params: + updates: + - {key: NODE_LTS_VERSION, value: '16'} + - {key: NPM_VERSION, value: '9'} + - {key: TS_VERSION, value: current} + - {key: TYPES_VERSION, value: 20.14.10} + - func: install dependencies + - func: compile driver + - name: compile-driver-typescript-current-node-types-16.x + tags: + - compile-driver-typescript-current + - typescript-compilation + commands: + - command: expansions.update + type: setup + params: + updates: + - {key: NODE_LTS_VERSION, value: '16'} + - {key: NPM_VERSION, value: '9'} + - {key: TS_VERSION, value: current} + - {key: TYPES_VERSION, value: 16.x} + - func: install dependencies + - func: compile driver - name: download-and-merge-coverage tags: [] commands: @@ -5171,3 +5283,8 @@ buildvariants: run_on: rhel80-large tasks: - test_atlas_task_group_search_indexes + - name: resource management tests + display_name: resource management tests + run_on: rhel80-large + tasks: + - .resource-management diff --git a/.evergreen/generate_evergreen_tasks.js b/.evergreen/generate_evergreen_tasks.js index 5d2d2b366c5..02855fef1a7 100644 --- a/.evergreen/generate_evergreen_tasks.js +++ b/.evergreen/generate_evergreen_tasks.js @@ -492,56 +492,94 @@ SINGLETON_TASKS.push( { func: 'run lint checks' } ] }, + { + name: 'run-resource-management-no-async-dispose', + tags: ['resource-management'], + commands: [ + updateExpansions({ + NODE_LTS_VERSION: "v16.20.2", + NPM_VERSION: 9 + }), + { func: 'install dependencies' }, + { func: 'check resource management' } + ] + }, + { + name: 'run-resource-management-async-dispose', + tags: ['resource-management'], + commands: [ + updateExpansions({ + NODE_LTS_VERSION: 'latest', + NPM_VERSION: 9 + }), + { func: 'install dependencies' }, + { func: 'check resource management' } + ] + }, + { + name: 'test-explicit-resource-management-smoke-tests', + tags: ['resource-management'], + commands: [ + updateExpansions({ + VERSION: 'latest', + TOPOLOGY: 'replica_set', + NODE_LTS_VERSION: 'latest' + }), + { func: 'install dependencies' }, + { func: 'bootstrap mongo-orchestration' }, + { func: 'check resource management smoke tests' } + ] + }, ...Array.from(makeTypescriptTasks()) ] ); function* makeTypescriptTasks() { - for (const TS_VERSION of ['next', 'current', '4.4']) { - // We don't compile on next, because compilation errors are likely. We do expect - // that the drivers types continue to work with next though. - if (TS_VERSION !== '4.4' && TS_VERSION !== 'next') { - yield { - name: `compile-driver-typescript-${TS_VERSION}`, - tags: [`compile-driver-typescript-${TS_VERSION}`, 'typescript-compilation'], - commands: [ - updateExpansions({ - NODE_LTS_VERSION: LOWEST_LTS, - NPM_VERSION: 9, - TS_VERSION - }), - { func: 'install dependencies' }, - { func: 'compile driver' } - ] - }; + function makeCompileTask(TS_VERSION, TYPES_VERSION) { + return { + name: `compile-driver-typescript-${TS_VERSION}-node-types-${TYPES_VERSION}`, + tags: [`compile-driver-typescript-${TS_VERSION}`, 'typescript-compilation'], + commands: [ + updateExpansions({ + NODE_LTS_VERSION: LOWEST_LTS, + NPM_VERSION: 9, + TS_VERSION, + TYPES_VERSION + }), + { func: 'install dependencies' }, + { func: 'compile driver' } + ] } - - yield { - name: `check-types-typescript-${TS_VERSION}`, + } + function makeCheckTypesTask(TS_VERSION, TYPES_VERSION) { + return { + name: `check-types-typescript-${TS_VERSION}-node-types-${TYPES_VERSION}`, tags: [`check-types-typescript-${TS_VERSION}`, 'typescript-compilation'], commands: [ updateExpansions({ NODE_LTS_VERSION: LOWEST_LTS, NPM_VERSION: 9, - TS_VERSION + TS_VERSION, + TYPES_VERSION }), { func: 'install dependencies' }, { func: 'check types' } ] - }; + } } - return { - name: 'run-typescript-next', - tags: ['run-typescript-next', 'typescript-compilation'], - commands: [ - updateExpansions({ - NODE_LTS_VERSION: LOWEST_LTS, - NPM_VERSION: 9 - }), - { func: 'install dependencies' }, - { func: 'run typescript next' } - ] - }; + + const typesVersion = require('../package.json').devDependencies['@types/node'].slice(1) + yield makeCheckTypesTask('next', typesVersion); + yield makeCheckTypesTask('current', typesVersion); + + yield makeCheckTypesTask('next', '16.x'); + yield makeCheckTypesTask('current', '16.x'); + + // typescript 4.4 only compiles our types with this particular version + yield makeCheckTypesTask('4.4', '18.11.9'); + + yield makeCompileTask('current', typesVersion); + yield makeCompileTask('current', '16.x'); } BUILD_VARIANTS.push({ @@ -731,6 +769,13 @@ BUILD_VARIANTS.push({ tasks: ['test_atlas_task_group_search_indexes'] }); +BUILD_VARIANTS.push({ + name: 'resource management tests', + display_name: 'resource management tests', + run_on: DEFAULT_OS, + tasks: ['.resource-management'] +}); + // TODO(NODE-4575): unskip zstd and snappy on node 16 for (const variant of BUILD_VARIANTS.filter( variant => variant.expansions && [16, 18, 20].includes(variant.expansions.NODE_LTS_VERSION) @@ -755,6 +800,7 @@ fileData.tasks = (fileData.tasks || []) .concat(AUTH_DISABLED_TASKS) .concat(AWS_LAMBDA_HANDLER_TASKS) .concat(MONGOCRYPTD_CSFLE_TASKS); + fileData.buildvariants = (fileData.buildvariants || []).concat(BUILD_VARIANTS); fs.writeFileSync( diff --git a/.evergreen/install-dependencies.sh b/.evergreen/install-dependencies.sh index 54724f16076..515722b22b4 100644 --- a/.evergreen/install-dependencies.sh +++ b/.evergreen/install-dependencies.sh @@ -1,6 +1,10 @@ #!/usr/bin/env bash set -o errexit # Exit the script with error if any of the commands fail +# allowed values: +## a nodejs major version (i.e., 16) +## 'latest' +## a full nodejs version, in the format v..patch NODE_LTS_VERSION=${NODE_LTS_VERSION:-16} # npm version can be defined in the environment for cases where we need to install # a version lower than latest to support EOL Node versions. diff --git a/.evergreen/run-resource-management-smoke-tests.sh b/.evergreen/run-resource-management-smoke-tests.sh new file mode 100644 index 00000000000..808527d0088 --- /dev/null +++ b/.evergreen/run-resource-management-smoke-tests.sh @@ -0,0 +1,15 @@ +#! /bin/bash + +source "${PROJECT_DIRECTORY}/.evergreen/init-node-and-npm-env.sh" + +echo "Building driver..." +npm pack +echo "Building driver...finished." + +echo "Node version: $(node -v)" +cd test/explicit-resource-management + +pwd +npm i +npm t +mv xunit.xml ../.. diff --git a/.evergreen/run-resource-management.sh b/.evergreen/run-resource-management.sh new file mode 100644 index 00000000000..ff7737b1c5c --- /dev/null +++ b/.evergreen/run-resource-management.sh @@ -0,0 +1,5 @@ +#! /bin/bash + +source "${PROJECT_DIRECTORY}/.evergreen/init-node-and-npm-env.sh" + +npm run check:resource-management diff --git a/.evergreen/run-typescript.sh b/.evergreen/run-typescript.sh index 87d47a559b6..9bd212eb705 100644 --- a/.evergreen/run-typescript.sh +++ b/.evergreen/run-typescript.sh @@ -3,8 +3,6 @@ set -o errexit # Exit the script with error if any of the commands fail source "${PROJECT_DIRECTORY}/.evergreen/init-node-and-npm-env.sh" -set -o xtrace - case $TS_CHECK in COMPILE_DRIVER|CHECK_TYPES) # Ok ;; @@ -14,6 +12,7 @@ case $TS_CHECK in esac if [ -z "$TS_VERSION" ]; then echo "TS_VERSION must be set"; exit 1; fi +if [ -z "$TYPES_VERSION" ]; then echo "TYPES_VERSION must be set"; exit 1; fi if [ ! -f "mongodb.d.ts" ]; then echo "mongodb.d.ts should always exist because of the installation in prior steps but in case it doesn't, build it" @@ -31,10 +30,11 @@ function get_ts_version() { export TSC="./node_modules/typescript/bin/tsc" export TS_VERSION=$(get_ts_version) -# On old versions of TS we need to put the node types back to 18.11.19 -npm install --no-save --force typescript@"$TS_VERSION" "$(if [[ $TS_VERSION == '4.4' ]]; then echo "@types/node@18.11.19"; else echo ""; fi)" +npm install --no-save --force "typescript@$TS_VERSION" "@types/node@$TYPES_VERSION" echo "Typescript $($TSC -v)" +echo "Types: $(cat node_modules/@types/node/package.json | jq -r .version)" +echo "Nodejs: $(node -v)" # check resolution uses the default latest types echo "import * as mdb from '.'" > file.ts && node $TSC --noEmit --traceResolution file.ts | grep 'mongodb.d.ts' && rm file.ts diff --git a/api-extractor.json b/api-extractor.json index bda873c16bf..bbf8b9d173a 100644 --- a/api-extractor.json +++ b/api-extractor.json @@ -10,7 +10,8 @@ "dtsRollup": { "enabled": true, "untrimmedFilePath": "", - "publicTrimmedFilePath": "/.d.ts" + "publicTrimmedFilePath": "/.d.ts", + "betaTrimmedFilePath": "/lib/beta.d.ts" }, "tsdocMetadata": { "enabled": false diff --git a/etc/clean_definition_files.cjs b/etc/clean_definition_files.cjs index 1beb65d78ff..3e5f9b7569e 100755 --- a/etc/clean_definition_files.cjs +++ b/etc/clean_definition_files.cjs @@ -21,7 +21,7 @@ if (fs.existsSync(libPath)) { const definitionFiles = Array.from(walk(libPath)).filter(filePath => { return filePath.endsWith('.d.ts') || filePath.endsWith('.d.ts.map'); }); - for (const definitionFile of definitionFiles) { + for (const definitionFile of definitionFiles.filter(file => !file.endsWith('beta.d.ts'))) { fs.unlinkSync(definitionFile); } } diff --git a/package.json b/package.json index 989f2ce003a..9004f5e7891 100644 --- a/package.json +++ b/package.json @@ -73,7 +73,7 @@ "@types/express": "^4.17.21", "@types/kerberos": "^1.1.5", "@types/mocha": "^10.0.6", - "@types/node": "^20.12.7", + "@types/node": "^20.14.10", "@types/saslprep": "^1.0.3", "@types/semver": "^7.5.8", "@types/sinon": "^17.0.3", @@ -110,7 +110,7 @@ "source-map-support": "^0.5.21", "ts-node": "^10.9.2", "tsd": "^0.31.0", - "typescript": "5.0", + "typescript": "^5.5.3", "typescript-cached-transpile": "^0.0.6", "v8-heapsnapshot": "^1.3.1", "yargs": "^17.7.2" @@ -126,7 +126,7 @@ "scripts": { "build:evergreen": "node .evergreen/generate_evergreen_tasks.js", "build:ts": "node ./node_modules/typescript/bin/tsc", - "build:dts": "npm run build:ts && api-extractor run && node etc/clean_definition_files.cjs && eslint mongodb.d.ts --fix", + "build:dts": "npm run build:ts && api-extractor run && node etc/clean_definition_files.cjs && eslint --no-ignore --fix mongodb.d.ts lib/beta.d.ts", "build:docs": "./etc/docs/build.ts", "build:typedoc": "typedoc", "build:nightly": "node ./.github/scripts/nightly.mjs", @@ -145,6 +145,7 @@ "check:unit": "mocha test/unit", "check:ts": "node ./node_modules/typescript/bin/tsc -v && node ./node_modules/typescript/bin/tsc --noEmit", "check:atlas": "mocha --config test/manual/mocharc.json test/manual/atlas_connectivity.test.ts", + "check:resource-management": "mocha --config test/manual/mocharc.json test/manual/resource_management.test.ts", "check:drivers-atlas-testing": "mocha --config test/mocha_mongodb.json test/atlas/drivers_atlas_testing.test.ts", "check:adl": "mocha --config test/mocha_mongodb.json test/manual/atlas-data-lake-testing", "check:aws": "nyc mocha --config test/mocha_mongodb.json test/integration/auth/mongodb_aws.test.ts", diff --git a/src/beta.ts b/src/beta.ts new file mode 100644 index 00000000000..ea465c2a34a --- /dev/null +++ b/src/beta.ts @@ -0,0 +1 @@ +export * from './index'; diff --git a/src/change_stream.ts b/src/change_stream.ts index 4e46d6c013f..9551e5ff5a7 100644 --- a/src/change_stream.ts +++ b/src/change_stream.ts @@ -18,6 +18,7 @@ import { type InferIdType, TypedEventEmitter } from './mongo_types'; import type { AggregateOptions } from './operations/aggregate'; import type { CollationOptions, OperationParent } from './operations/command'; import type { ReadPreference } from './read_preference'; +import { type AsyncDisposable } from './resource_management'; import type { ServerSessionId } from './sessions'; import { filterOptions, getTopology, type MongoDBNamespace, squashError } from './utils'; @@ -544,9 +545,15 @@ export type ChangeStreamEvents< * @public */ export class ChangeStream< - TSchema extends Document = Document, - TChange extends Document = ChangeStreamDocument -> extends TypedEventEmitter> { + TSchema extends Document = Document, + TChange extends Document = ChangeStreamDocument + > + extends TypedEventEmitter> + implements AsyncDisposable +{ + /** @beta */ + declare [Symbol.asyncDispose]: () => Promise; + pipeline: Document[]; /** * @remarks WriteConcern can still be present on the options because @@ -986,3 +993,8 @@ export class ChangeStream< } } } + +Symbol.asyncDispose && + (ChangeStream.prototype[Symbol.asyncDispose] = async function () { + await this.close(); + }); diff --git a/src/cursor/abstract_cursor.ts b/src/cursor/abstract_cursor.ts index da08f1a1a66..cbe1c4abc0d 100644 --- a/src/cursor/abstract_cursor.ts +++ b/src/cursor/abstract_cursor.ts @@ -17,6 +17,7 @@ import { GetMoreOperation } from '../operations/get_more'; import { KillCursorsOperation } from '../operations/kill_cursors'; import { ReadConcern, type ReadConcernLike } from '../read_concern'; import { ReadPreference, type ReadPreferenceLike } from '../read_preference'; +import { type AsyncDisposable } from '../resource_management'; import type { Server } from '../sdam/server'; import { ClientSession, maybeClearPinnedConnection } from '../sessions'; import { type MongoDBNamespace, squashError } from '../utils'; @@ -124,9 +125,12 @@ export type AbstractCursorEvents = { /** @public */ export abstract class AbstractCursor< - TSchema = any, - CursorEvents extends AbstractCursorEvents = AbstractCursorEvents -> extends TypedEventEmitter { + TSchema = any, + CursorEvents extends AbstractCursorEvents = AbstractCursorEvents + > + extends TypedEventEmitter + implements AsyncDisposable +{ /** @internal */ private cursorId: Long | null; /** @internal */ @@ -275,6 +279,9 @@ export abstract class AbstractCursor< return !!this.cursorClient.topology?.loadBalanced; } + /** @beta */ + declare [Symbol.asyncDispose]: () => Promise; + /** Returns current buffered documents length */ bufferedCount(): number { return this.documents?.length ?? 0; @@ -916,3 +923,8 @@ class ReadableCursorStream extends Readable { ); } } + +Symbol.asyncDispose && + (AbstractCursor.prototype[Symbol.asyncDispose] = async function () { + await this.close(); + }); diff --git a/src/index.ts b/src/index.ts index 8bf6c686174..f4b299ea379 100644 --- a/src/index.ts +++ b/src/index.ts @@ -111,6 +111,7 @@ export { ReturnDocument } from './operations/find_and_modify'; export { ProfilingLevel } from './operations/set_profiling_level'; export { ReadConcernLevel } from './read_concern'; export { ReadPreferenceMode } from './read_preference'; +export { AsyncDisposable } from './resource_management'; export { ServerType, TopologyType } from './sdam/common'; // Helper classes diff --git a/src/mongo_client.ts b/src/mongo_client.ts index aee241076f9..d8abf57c47b 100644 --- a/src/mongo_client.ts +++ b/src/mongo_client.ts @@ -34,6 +34,7 @@ import { executeOperation } from './operations/execute_operation'; import { RunAdminCommandOperation } from './operations/run_command'; import type { ReadConcern, ReadConcernLevel, ReadConcernLike } from './read_concern'; import { ReadPreference, type ReadPreferenceMode } from './read_preference'; +import { type AsyncDisposable } from './resource_management'; import type { ServerMonitoringMode } from './sdam/monitor'; import type { TagSet } from './sdam/server_description'; import { readPreferenceServerSelector } from './sdam/server_selection'; @@ -344,7 +345,7 @@ const kOptions = Symbol('options'); * await client.insertOne({ name: 'spot', kind: 'dog' }); * ``` */ -export class MongoClient extends TypedEventEmitter { +export class MongoClient extends TypedEventEmitter implements AsyncDisposable { /** @internal */ s: MongoClientPrivate; /** @internal */ @@ -404,6 +405,9 @@ export class MongoClient extends TypedEventEmitter { this.checkForNonGenuineHosts(); } + /** @beta */ + declare [Symbol.asyncDispose]: () => Promise; + /** @internal */ private checkForNonGenuineHosts() { const documentDBHostnames = this[kOptions].hosts.filter((hostAddress: HostAddress) => @@ -758,6 +762,11 @@ export class MongoClient extends TypedEventEmitter { } } +Symbol.asyncDispose && + (MongoClient.prototype[Symbol.asyncDispose] = async function () { + await this.close(); + }); + /** * Parsed Mongo Client Options. * diff --git a/src/resource_management.ts b/src/resource_management.ts new file mode 100644 index 00000000000..15fe58bbe63 --- /dev/null +++ b/src/resource_management.ts @@ -0,0 +1,57 @@ +import { AbstractCursor, ChangeStream } from './beta'; +import { MongoClient } from './mongo_client'; +import { ClientSession } from './sessions'; + +/** + * @public + * @experimental + */ +export interface AsyncDisposable { + /** @beta */ + [Symbol.asyncDispose]: () => Promise; +} + +/** + * @public + * @beta + * + * Attaches `Symbol.asyncDispose` methods to the MongoClient, Cursors, sessions and change streams + * if Symbol.asyncDispose is defined. + * + * It's usually not necessary to call this method - the driver attempts to attach these methods + * itself when its loaded. However, sometimes the driver may be loaded before `Symbol.asyncDispose` + * is defined, in which case it is necessary to call this method directly. This can happen if the + * application is polyfilling `Symbol.asyncDispose`. + * + * Example: + * + * ```typescript + * import { load, MongoClient } from 'mongodb/beta'; + * + * Symbol.asyncDispose ??= Symbol('dispose'); + * load(); + * + * await using client = new MongoClient(...); + * ``` + */ +export function load() { + Symbol.asyncDispose && + (MongoClient.prototype[Symbol.asyncDispose] = async function () { + await this.close(); + }); + + Symbol.asyncDispose && + (ClientSession.prototype[Symbol.asyncDispose] = async function () { + await this.endSession({ force: true }); + }); + + Symbol.asyncDispose && + (AbstractCursor.prototype[Symbol.asyncDispose] = async function () { + await this.close(); + }); + + Symbol.asyncDispose && + (ChangeStream.prototype[Symbol.asyncDispose] = async function () { + await this.close(); + }); +} diff --git a/src/sessions.ts b/src/sessions.ts index 544bef897c4..e7bed45aae0 100644 --- a/src/sessions.ts +++ b/src/sessions.ts @@ -27,6 +27,7 @@ import { executeOperation } from './operations/execute_operation'; import { RunAdminCommandOperation } from './operations/run_command'; import { ReadConcernLevel } from './read_concern'; import { ReadPreference } from './read_preference'; +import { type AsyncDisposable } from './resource_management'; import { _advanceClusterTime, type ClusterTime, TopologyType } from './sdam/common'; import { isTransactionCommand, @@ -105,7 +106,10 @@ export interface EndSessionOptions { * NOTE: not meant to be instantiated directly. * @public */ -export class ClientSession extends TypedEventEmitter { +export class ClientSession + extends TypedEventEmitter + implements AsyncDisposable +{ /** @internal */ client: MongoClient; /** @internal */ @@ -286,6 +290,8 @@ export class ClientSession extends TypedEventEmitter { maybeClearPinnedConnection(this, { force: true, ...options }); } } + /** @beta */ + declare [Symbol.asyncDispose]: () => Promise; /** * Advances the operationTime for a ClientSession. @@ -484,6 +490,11 @@ export class ClientSession extends TypedEventEmitter { } } +Symbol.asyncDispose && + (ClientSession.prototype[Symbol.asyncDispose] = async function () { + await this.endSession({ force: true }); + }); + const MAX_WITH_TRANSACTION_TIMEOUT = 120000; const NON_DETERMINISTIC_WRITE_CONCERN_ERRORS = new Set([ 'CannotSatisfyWriteConcern', diff --git a/test/explicit-resource-management/.gitignore b/test/explicit-resource-management/.gitignore new file mode 100644 index 00000000000..b76821427a0 --- /dev/null +++ b/test/explicit-resource-management/.gitignore @@ -0,0 +1,2 @@ +*.js +package-lock.json diff --git a/test/explicit-resource-management/.mocharc.json b/test/explicit-resource-management/.mocharc.json new file mode 100644 index 00000000000..1d1c61fb5f7 --- /dev/null +++ b/test/explicit-resource-management/.mocharc.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://raw.githubusercontent.com/SchemaStore/schemastore/master/src/schemas/json/mocharc.json", + "extension": [ + "js" + ], + "reporter": "../tools/reporter/mongodb_reporter.js", + "recursive": true, + "timeout": 60000, + "failZero": true, + "sort": true, + "color": true +} diff --git a/test/explicit-resource-management/main.test.ts b/test/explicit-resource-management/main.test.ts new file mode 100644 index 00000000000..8a274253414 --- /dev/null +++ b/test/explicit-resource-management/main.test.ts @@ -0,0 +1,123 @@ + +import { expect } from 'chai'; +import { describe, it } from 'mocha'; +import { AbstractCursor, ChangeStream, ClientSession, GridFSBucket, MongoClient } from 'mongodb/lib/beta'; +import * as sinon from 'sinon'; +import { Readable } from 'stream'; +import { pipeline } from 'stream/promises'; +import { setTimeout } from 'timers/promises'; + +// @ts-expect-error Assigning readonly property. +Symbol.asyncDispose ??= Symbol('dispose'); + +async function setUpCollection(client: MongoClient) { + const collection = client.db('foo').collection<{ name: string }>('bar'); + const documents: Array<{ name: string }> = Array.from({ length: 5 }).map(i => ({ + name: String(i) + })); + await collection.insertMany(documents) + return collection; +} + +describe('explicit resource management smoke tests', function () { + const clientSpy = sinon.spy(MongoClient.prototype, Symbol.asyncDispose); + const cursorSpy = sinon.spy(AbstractCursor.prototype, Symbol.asyncDispose); + const endSessionSpy = sinon.spy(ClientSession.prototype, Symbol.asyncDispose); + const changeStreamSpy = sinon.spy(ChangeStream.prototype, Symbol.asyncDispose); + const readableSpy = sinon.spy(Readable.prototype, Symbol.asyncDispose); + + afterEach(function () { + clientSpy.resetHistory(); + cursorSpy.resetHistory(); + endSessionSpy.resetHistory(); + changeStreamSpy.resetHistory(); + readableSpy.resetHistory(); + }); + + describe('MongoClient', function () { + it('can be used with await-using syntax', async function () { + { + await using client = new MongoClient(process.env.MONGODB_URI!); + await client.connect(); + } + expect(clientSpy.called).to.be.true; + expect(clientSpy.callCount).to.equal(1); + }) + }) + + describe('Cursors', function () { + it('can be used with await-using syntax', async function () { + { + await using client = new MongoClient(process.env.MONGODB_URI!); + await client.connect(); + + const collection = await setUpCollection(client); + + await using cursor = collection.find(); + await cursor.next(); + await cursor.next(); + await cursor.next(); + } + expect(cursorSpy.callCount).to.equal(1); + }) + + describe('cursor streams', function() { + it('can be used with await-using syntax', async function() { + { + await using client = new MongoClient(process.env.MONGODB_URI!); + await client.connect(); + + const collection = await setUpCollection(client); + + await using readable = collection.find().stream(); + } + expect(readableSpy.callCount).to.equal(1); + }) + }) + }) + + describe('Sessions', function () { + it('can be used with await-using syntax', async function () { + { + await using client = new MongoClient(process.env.MONGODB_URI!); + await client.connect(); + + await using session = client.startSession(); + } + expect(endSessionSpy.callCount).to.equal(1); + }) + }) + + describe('ChangeStreams', function () { + it('can be used with await-using syntax', async function () { + { + await using client = new MongoClient(process.env.MONGODB_URI!); + await client.connect(); + + const collection = await setUpCollection(client); + await using cs = collection.watch(); + + setTimeout(1000).then(() => collection.insertOne({ name: 'bailey' })); + await cs.next(); + } + expect(changeStreamSpy.callCount).to.equal(1); + }) + }); + + describe('GridFSDownloadStream', function () { + it('can be used with await-using syntax', async function () { + { + await using client = new MongoClient(process.env.MONGODB_URI!); + await client.connect(); + + const bucket = new GridFSBucket(client.db('foo')); + const uploadStream = bucket.openUploadStream('foo.txt') + await pipeline(Readable.from("AAAAAAA".split('')), uploadStream); + + await using downloadStream = bucket.openDownloadStreamByName('foo.txt'); + + } + expect(readableSpy.callCount).to.equal(1); + }) + }); +}) diff --git a/test/explicit-resource-management/package.json b/test/explicit-resource-management/package.json new file mode 100644 index 00000000000..b5ac33ef64a --- /dev/null +++ b/test/explicit-resource-management/package.json @@ -0,0 +1,24 @@ +{ + "name": "explicit-resource-management", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "run": "npm run build && node out/main.js", + "build": "tsc", + "test": "npm run build && mocha --config .mocharc.json main.test.js" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "@types/chai": "^4.3.16", + "chai": "^4.4.1", + "mongodb": "../../mongodb-6.8.0.tgz", + "tslib": "^2.6.3" + }, + "devDependencies": { + "mocha": "^10.7.0", + "typescript": "5.5" + } +} diff --git a/test/explicit-resource-management/tsconfig.json b/test/explicit-resource-management/tsconfig.json new file mode 100644 index 00000000000..e11e12bbcd9 --- /dev/null +++ b/test/explicit-resource-management/tsconfig.json @@ -0,0 +1,47 @@ +{ + "include": [ + "main.test.ts" + ], + "exclude": [ + "main.test.js" + ], + "compilerOptions": { + "allowJs": true, + "checkJs": false, + "strict": true, + "alwaysStrict": true, + "target": "ES2021", + "module": "Node16", + "moduleResolution": "Node16", + "skipLibCheck": true, + "lib": [ + "es2021", + "ES2022.Error", + "ES2022.Object", + "ESNext.Disposable" + ], + // We don't make use of tslib helpers, all syntax used is supported by target engine + "importHelpers": true, + "noEmitHelpers": false, + // Never emit error filled code + "noEmitOnError": true, + "outDir": ".", + // We want the sourcemaps in a separate file + "inlineSourceMap": false, + "sourceMap": false, + // API-Extractor uses declaration maps to report problems in source, no need to distribute + "declaration": false, + "declarationMap": false, + // we include sources in the release + "inlineSources": false, + // Prevents web types from being suggested by vscode. + "types": [ + "node" + ], + "forceConsistentCasingInFileNames": true, + "noImplicitOverride": true, + "noImplicitReturns": true, + // TODO(NODE-3659): Enable useUnknownInCatchVariables and add type assertions or remove unnecessary catch blocks + "useUnknownInCatchVariables": false + } +} diff --git a/test/manual/resource_management.test.ts b/test/manual/resource_management.test.ts new file mode 100644 index 00000000000..f41a98cdb69 --- /dev/null +++ b/test/manual/resource_management.test.ts @@ -0,0 +1,89 @@ +import { expect } from 'chai'; +import * as sinon from 'sinon'; + +import { AbstractCursor, ChangeStream, ClientSession, MongoClient } from '../mongodb'; + +describe('Explicit Resource Management Tests', function () { + let client: MongoClient; + + afterEach(async function () { + await client?.close(); + }); + + describe('Symbol.asyncDispose defined', function () { + beforeEach(function () { + if (!('asyncDispose' in Symbol)) { + this.currentTest.skipReason = 'Test must run with asyncDispose available.'; + this.skip(); + } + }); + + describe('MongoClient', function () { + it('closes the the client', async function () { + client = new MongoClient('mongodb://localhost:27017'); + + const spy = sinon.spy(client, 'close'); + await client[Symbol.asyncDispose](); + expect(spy.called).to.be.true; + }); + }); + + describe('ClientSession', function () { + it('ends the session', async function () { + client = new MongoClient('mongodb://localhost:27017'); + const session = client.startSession(); + + const spy = sinon.spy(session, 'endSession'); + await session[Symbol.asyncDispose](); + expect(spy.called).to.be.true; + }); + }); + + describe('ChangeStreams', function () { + it('closes the change stream', async function () { + client = new MongoClient('mongodb://localhost:27017'); + const changeStream = client.watch(); + + const spy = sinon.spy(changeStream, 'close'); + await changeStream[Symbol.asyncDispose](); + expect(spy.called).to.be.true; + }); + }); + + describe('cursors', function () { + it('closes the cursor', async function () { + client = new MongoClient('mongodb://localhost:27017'); + const cursor = client.db('foo').collection('bar').find(); + + const spy = sinon.spy(cursor, 'close'); + await cursor[Symbol.asyncDispose](); + expect(spy.called).to.be.true; + }); + }); + }); + + describe('Symbol.asyncDispose not defined', function () { + beforeEach(function () { + if ('asyncDispose' in Symbol) { + this.currentTest.skipReason = 'Test must run without asyncDispose available.'; + this.skip(); + } + }); + + it('does not define symbol.asyncDispose on MongoClient', function () { + expect(MongoClient[Symbol.asyncDispose]).not.to.exist; + }); + + it('does not define symbol.asyncDispose on ClientSession', function () { + expect(ClientSession[Symbol.asyncDispose]).not.to.exist; + }); + + it('does not define symbol.asyncDispose on ChangeStream', function () { + expect(ChangeStream[Symbol.asyncDispose]).not.to.exist; + }); + + it('does not define symbol.asyncDispose on cursors', function () { + expect(AbstractCursor[Symbol.asyncDispose]).not.to.exist; + }); + }); +}); diff --git a/test/mongodb.ts b/test/mongodb.ts index 887c65d2774..65562f56c79 100644 --- a/test/mongodb.ts +++ b/test/mongodb.ts @@ -187,6 +187,7 @@ export * from '../src/operations/update'; export * from '../src/operations/validate_collection'; export * from '../src/read_concern'; export * from '../src/read_preference'; +export * from '../src/resource_management'; export * from '../src/sdam/common'; export * from '../src/sdam/events'; export * from '../src/sdam/monitor'; diff --git a/test/unit/index.test.ts b/test/unit/index.test.ts index 6509568c018..0a0bbdc98d8 100644 --- a/test/unit/index.test.ts +++ b/test/unit/index.test.ts @@ -16,6 +16,7 @@ const EXPECTED_EXPORTS = [ 'AbstractCursor', 'Admin', 'AggregationCursor', + 'AsyncDisposable', 'AuthMechanism', 'AutoEncryptionLoggerLevel', 'BatchType', From 0c53c1af89a60595864b55a0f8da91ec7729214f Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Wed, 24 Jul 2024 15:42:32 -0600 Subject: [PATCH 02/17] fix package lock --- package-lock.json | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0fa708e66c7..ed3af9feb9e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,7 +25,7 @@ "@types/express": "^4.17.21", "@types/kerberos": "^1.1.5", "@types/mocha": "^10.0.6", - "@types/node": "^20.12.7", + "@types/node": "^20.14.10", "@types/saslprep": "^1.0.3", "@types/semver": "^7.5.8", "@types/sinon": "^17.0.3", @@ -62,7 +62,7 @@ "source-map-support": "^0.5.21", "ts-node": "^10.9.2", "tsd": "^0.31.0", - "typescript": "5.0", + "typescript": "^5.5.3", "typescript-cached-transpile": "^0.0.6", "v8-heapsnapshot": "^1.3.1", "yargs": "^17.7.2" @@ -2966,9 +2966,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.12.7", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.7.tgz", - "integrity": "sha512-wq0cICSkRLVaf3UGLMGItu/PtdY7oaXaI/RVU+xliKVOtRna3PRY57ZDfztpDL0n11vfymMUnXv8QwYCO7L1wg==", + "version": "20.14.12", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.12.tgz", + "integrity": "sha512-r7wNXakLeSsGT0H1AU863vS2wa5wBOK4bWMjZz2wj+8nBx+m5PeIn0k8AloSLpRuiwdRQZwarZqHE4FNArPuJQ==", "dev": true, "dependencies": { "undici-types": "~5.26.4" @@ -10218,16 +10218,16 @@ } }, "node_modules/typescript": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz", - "integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==", + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", + "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", "dev": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { - "node": ">=12.20" + "node": ">=14.17" } }, "node_modules/typescript-cached-transpile": { From d995e728aa076e549bc062cef00fc0b76810e47e Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Thu, 25 Jul 2024 08:26:26 -0600 Subject: [PATCH 03/17] remove unnecessary compile task --- .evergreen/config.yml | 15 --------------- .evergreen/generate_evergreen_tasks.js | 1 - 2 files changed, 16 deletions(-) diff --git a/.evergreen/config.yml b/.evergreen/config.yml index 376040942ee..fef99ef6137 100644 --- a/.evergreen/config.yml +++ b/.evergreen/config.yml @@ -3612,21 +3612,6 @@ tasks: - {key: TYPES_VERSION, value: 20.14.10} - func: install dependencies - func: compile driver - - name: compile-driver-typescript-current-node-types-16.x - tags: - - compile-driver-typescript-current - - typescript-compilation - commands: - - command: expansions.update - type: setup - params: - updates: - - {key: NODE_LTS_VERSION, value: '16'} - - {key: NPM_VERSION, value: '9'} - - {key: TS_VERSION, value: current} - - {key: TYPES_VERSION, value: 16.x} - - func: install dependencies - - func: compile driver - name: download-and-merge-coverage tags: [] commands: diff --git a/.evergreen/generate_evergreen_tasks.js b/.evergreen/generate_evergreen_tasks.js index 02855fef1a7..3346679b235 100644 --- a/.evergreen/generate_evergreen_tasks.js +++ b/.evergreen/generate_evergreen_tasks.js @@ -579,7 +579,6 @@ function* makeTypescriptTasks() { yield makeCheckTypesTask('4.4', '18.11.9'); yield makeCompileTask('current', typesVersion); - yield makeCompileTask('current', '16.x'); } BUILD_VARIANTS.push({ From a64bd1927ee34d275785a00db9444616f2a5ed00 Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Thu, 25 Jul 2024 10:23:28 -0600 Subject: [PATCH 04/17] misc changes --- src/resource_management.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/resource_management.ts b/src/resource_management.ts index 15fe58bbe63..2ecafeaa9e8 100644 --- a/src/resource_management.ts +++ b/src/resource_management.ts @@ -34,7 +34,7 @@ export interface AsyncDisposable { * await using client = new MongoClient(...); * ``` */ -export function load() { +export function configureExplicitResourceManagement() { Symbol.asyncDispose && (MongoClient.prototype[Symbol.asyncDispose] = async function () { await this.close(); From 416e58cf3f644326de7d6690e1f4e5e2a611d058 Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Thu, 25 Jul 2024 11:17:02 -0600 Subject: [PATCH 05/17] bump tsdoc version and typescript to 5.5 --- etc/docs/build.ts | 2 +- package-lock.json | 2 +- package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/etc/docs/build.ts b/etc/docs/build.ts index b0018499cc1..65b744eac93 100755 --- a/etc/docs/build.ts +++ b/etc/docs/build.ts @@ -24,7 +24,7 @@ const RELEASES_JSON_FILE = './template/static/versions.json'; const copyGeneratedDocsToDocsFolder = () => exec(`cp -R temp/. ../../docs/.`); const removeTempDirectory = () => exec('rm -rf temp'); -const installDependencies = () => exec('npm i --no-save --legacy-peer-deps typedoc@0.24.8'); +const installDependencies = () => exec('npm i --no-save --legacy-peer-deps typedoc@0.26.5'); const buildDocs = ({ tag }: VersionSchema) => { const revision = tag === LATEST_TAG ? 'main' : `v${tag}.0`; return exec(`npm run build:typedoc -- --gitRevision ${revision}`); diff --git a/package-lock.json b/package-lock.json index ed3af9feb9e..643f351d51c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -62,7 +62,7 @@ "source-map-support": "^0.5.21", "ts-node": "^10.9.2", "tsd": "^0.31.0", - "typescript": "^5.5.3", + "typescript": "5.5", "typescript-cached-transpile": "^0.0.6", "v8-heapsnapshot": "^1.3.1", "yargs": "^17.7.2" diff --git a/package.json b/package.json index 9004f5e7891..2ed27b2ccfe 100644 --- a/package.json +++ b/package.json @@ -110,7 +110,7 @@ "source-map-support": "^0.5.21", "ts-node": "^10.9.2", "tsd": "^0.31.0", - "typescript": "^5.5.3", + "typescript": "5.5", "typescript-cached-transpile": "^0.0.6", "v8-heapsnapshot": "^1.3.1", "yargs": "^17.7.2" From a852a0e1732c83179ddf5a43cda3faffc6777a21 Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Thu, 25 Jul 2024 14:31:10 -0600 Subject: [PATCH 06/17] comments --- src/beta.ts | 5 ++- src/change_stream.ts | 9 +++- src/cursor/abstract_cursor.ts | 9 +++- src/mongo_client.ts | 9 +++- src/resource_management.ts | 42 ++++++++++++++----- src/sessions.ts | 9 +++- .../explicit-resource-management/main.test.ts | 3 -- 7 files changed, 63 insertions(+), 23 deletions(-) diff --git a/src/beta.ts b/src/beta.ts index ea465c2a34a..28311112d9b 100644 --- a/src/beta.ts +++ b/src/beta.ts @@ -1 +1,4 @@ -export * from './index'; +import * as contents from './index'; +import { configureExplicitResourceManagement } from './resource_management'; + +export = { ...contents, configureExplicitResourceManagement }; diff --git a/src/change_stream.ts b/src/change_stream.ts index 9551e5ff5a7..a5659ea4586 100644 --- a/src/change_stream.ts +++ b/src/change_stream.ts @@ -995,6 +995,11 @@ export class ChangeStream< } Symbol.asyncDispose && - (ChangeStream.prototype[Symbol.asyncDispose] = async function () { - await this.close(); + Object.defineProperty(ChangeStream.prototype, Symbol.asyncDispose, { + value: async function asyncDispose(this: { close(): Promise }) { + await this.close(); + }, + enumerable: false, + configurable: true, + writable: true }); diff --git a/src/cursor/abstract_cursor.ts b/src/cursor/abstract_cursor.ts index cbe1c4abc0d..e68dee76d3c 100644 --- a/src/cursor/abstract_cursor.ts +++ b/src/cursor/abstract_cursor.ts @@ -925,6 +925,11 @@ class ReadableCursorStream extends Readable { } Symbol.asyncDispose && - (AbstractCursor.prototype[Symbol.asyncDispose] = async function () { - await this.close(); + Object.defineProperty(AbstractCursor.prototype, Symbol.asyncDispose, { + value: async function asyncDispose(this: { close(): Promise }) { + await this.close(); + }, + enumerable: false, + configurable: true, + writable: true }); diff --git a/src/mongo_client.ts b/src/mongo_client.ts index d8abf57c47b..111bd0aad6f 100644 --- a/src/mongo_client.ts +++ b/src/mongo_client.ts @@ -763,8 +763,13 @@ export class MongoClient extends TypedEventEmitter implements } Symbol.asyncDispose && - (MongoClient.prototype[Symbol.asyncDispose] = async function () { - await this.close(); + Object.defineProperty(MongoClient.prototype, Symbol.asyncDispose, { + value: async function asyncDispose(this: { close(): Promise }) { + await this.close(); + }, + enumerable: false, + configurable: true, + writable: true }); /** diff --git a/src/resource_management.ts b/src/resource_management.ts index 2ecafeaa9e8..7959e1c95d0 100644 --- a/src/resource_management.ts +++ b/src/resource_management.ts @@ -1,4 +1,5 @@ -import { AbstractCursor, ChangeStream } from './beta'; +import { ChangeStream } from './change_stream'; +import { AbstractCursor } from './cursor/abstract_cursor'; import { MongoClient } from './mongo_client'; import { ClientSession } from './sessions'; @@ -12,7 +13,6 @@ export interface AsyncDisposable { } /** - * @public * @beta * * Attaches `Symbol.asyncDispose` methods to the MongoClient, Cursors, sessions and change streams @@ -26,7 +26,7 @@ export interface AsyncDisposable { * Example: * * ```typescript - * import { load, MongoClient } from 'mongodb/beta'; + * import { configureExplicitResourceManagement, MongoClient } from 'mongodb/lib/beta'; * * Symbol.asyncDispose ??= Symbol('dispose'); * load(); @@ -36,22 +36,42 @@ export interface AsyncDisposable { */ export function configureExplicitResourceManagement() { Symbol.asyncDispose && - (MongoClient.prototype[Symbol.asyncDispose] = async function () { - await this.close(); + Object.defineProperty(MongoClient.prototype, Symbol.asyncDispose, { + value: async function asyncDispose(this: { close(): Promise }) { + await this.close(); + }, + enumerable: false, + configurable: true, + writable: true }); Symbol.asyncDispose && - (ClientSession.prototype[Symbol.asyncDispose] = async function () { - await this.endSession({ force: true }); + Object.defineProperty(AbstractCursor.prototype, Symbol.asyncDispose, { + value: async function asyncDispose(this: { close(): Promise }) { + await this.close(); + }, + enumerable: false, + configurable: true, + writable: true }); Symbol.asyncDispose && - (AbstractCursor.prototype[Symbol.asyncDispose] = async function () { - await this.close(); + Object.defineProperty(ChangeStream.prototype, Symbol.asyncDispose, { + value: async function asyncDispose(this: { close(): Promise }) { + await this.close(); + }, + enumerable: false, + configurable: true, + writable: true }); Symbol.asyncDispose && - (ChangeStream.prototype[Symbol.asyncDispose] = async function () { - await this.close(); + Object.defineProperty(ClientSession.prototype, Symbol.asyncDispose, { + value: async function asyncDispose(this: { endSession(): Promise }) { + await this.endSession(); + }, + enumerable: false, + configurable: true, + writable: true }); } diff --git a/src/sessions.ts b/src/sessions.ts index e7bed45aae0..29871168b30 100644 --- a/src/sessions.ts +++ b/src/sessions.ts @@ -491,8 +491,13 @@ export class ClientSession } Symbol.asyncDispose && - (ClientSession.prototype[Symbol.asyncDispose] = async function () { - await this.endSession({ force: true }); + Object.defineProperty(ClientSession.prototype, Symbol.asyncDispose, { + value: async function asyncDispose(this: { endSession(): Promise }) { + await this.endSession(); + }, + enumerable: false, + configurable: true, + writable: true }); const MAX_WITH_TRANSACTION_TIMEOUT = 120000; diff --git a/test/explicit-resource-management/main.test.ts b/test/explicit-resource-management/main.test.ts index 8a274253414..69c3dd54eeb 100644 --- a/test/explicit-resource-management/main.test.ts +++ b/test/explicit-resource-management/main.test.ts @@ -7,9 +7,6 @@ import { Readable } from 'stream'; import { pipeline } from 'stream/promises'; import { setTimeout } from 'timers/promises'; -// @ts-expect-error Assigning readonly property. -Symbol.asyncDispose ??= Symbol('dispose'); - async function setUpCollection(client: MongoClient) { const collection = client.db('foo').collection<{ name: string }>('bar'); const documents: Array<{ name: string }> = Array.from({ length: 5 }).map(i => ({ From 9335280405d2017a853f32f5448426a5f5e0edaa Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Fri, 26 Jul 2024 11:57:33 -0600 Subject: [PATCH 07/17] address comments --- .../run-resource-management-smoke-tests.sh | 1 - src/beta.ts | 25 +++++- src/change_stream.ts | 16 ++-- src/cursor/abstract_cursor.ts | 16 ++-- src/index.ts | 3 +- src/mongo_client.ts | 16 ++-- src/resource_management.ts | 81 +++++++++---------- src/sessions.ts | 16 ++-- src/utils.ts | 1 + test/unit/index.test.ts | 2 +- 10 files changed, 87 insertions(+), 90 deletions(-) diff --git a/.evergreen/run-resource-management-smoke-tests.sh b/.evergreen/run-resource-management-smoke-tests.sh index 808527d0088..9d047969abc 100644 --- a/.evergreen/run-resource-management-smoke-tests.sh +++ b/.evergreen/run-resource-management-smoke-tests.sh @@ -9,7 +9,6 @@ echo "Building driver...finished." echo "Node version: $(node -v)" cd test/explicit-resource-management -pwd npm i npm t mv xunit.xml ../.. diff --git a/src/beta.ts b/src/beta.ts index 28311112d9b..6cb01ed8360 100644 --- a/src/beta.ts +++ b/src/beta.ts @@ -1,4 +1,23 @@ -import * as contents from './index'; -import { configureExplicitResourceManagement } from './resource_management'; +import { type Document } from 'bson'; -export = { ...contents, configureExplicitResourceManagement }; +export * from './index'; + +/** + * @internal + * + * Since we don't bundle tslib helpers, we need to polyfill this method. + * + * This is used in the generated JS. + */ +// eslint-disable-next-line @typescript-eslint/no-unused-vars +function __exportStar(mod: Document) { + for (const key of Object.keys(mod)) { + exports[key] = void 0; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return mod[key]; + } + }); + } +} diff --git a/src/change_stream.ts b/src/change_stream.ts index a5659ea4586..4ec753bd1d3 100644 --- a/src/change_stream.ts +++ b/src/change_stream.ts @@ -18,7 +18,7 @@ import { type InferIdType, TypedEventEmitter } from './mongo_types'; import type { AggregateOptions } from './operations/aggregate'; import type { CollationOptions, OperationParent } from './operations/command'; import type { ReadPreference } from './read_preference'; -import { type AsyncDisposable } from './resource_management'; +import { type AsyncDisposable, configureResourceManagement } from './resource_management'; import type { ServerSessionId } from './sessions'; import { filterOptions, getTopology, type MongoDBNamespace, squashError } from './utils'; @@ -553,6 +553,10 @@ export class ChangeStream< { /** @beta */ declare [Symbol.asyncDispose]: () => Promise; + /** @internal */ + async dispose() { + await this.close(); + } pipeline: Document[]; /** @@ -994,12 +998,4 @@ export class ChangeStream< } } -Symbol.asyncDispose && - Object.defineProperty(ChangeStream.prototype, Symbol.asyncDispose, { - value: async function asyncDispose(this: { close(): Promise }) { - await this.close(); - }, - enumerable: false, - configurable: true, - writable: true - }); +configureResourceManagement(ChangeStream.prototype); diff --git a/src/cursor/abstract_cursor.ts b/src/cursor/abstract_cursor.ts index e68dee76d3c..d96a917d5bb 100644 --- a/src/cursor/abstract_cursor.ts +++ b/src/cursor/abstract_cursor.ts @@ -17,7 +17,7 @@ import { GetMoreOperation } from '../operations/get_more'; import { KillCursorsOperation } from '../operations/kill_cursors'; import { ReadConcern, type ReadConcernLike } from '../read_concern'; import { ReadPreference, type ReadPreferenceLike } from '../read_preference'; -import { type AsyncDisposable } from '../resource_management'; +import { type AsyncDisposable, configureResourceManagement } from '../resource_management'; import type { Server } from '../sdam/server'; import { ClientSession, maybeClearPinnedConnection } from '../sessions'; import { type MongoDBNamespace, squashError } from '../utils'; @@ -281,6 +281,10 @@ export abstract class AbstractCursor< /** @beta */ declare [Symbol.asyncDispose]: () => Promise; + /** @internal */ + async dispose() { + await this.close(); + } /** Returns current buffered documents length */ bufferedCount(): number { @@ -924,12 +928,4 @@ class ReadableCursorStream extends Readable { } } -Symbol.asyncDispose && - Object.defineProperty(AbstractCursor.prototype, Symbol.asyncDispose, { - value: async function asyncDispose(this: { close(): Promise }) { - await this.close(); - }, - enumerable: false, - configurable: true, - writable: true - }); +configureResourceManagement(AbstractCursor.prototype); diff --git a/src/index.ts b/src/index.ts index f4b299ea379..08869f04c5d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -75,6 +75,7 @@ export { MongoUnexpectedServerResponseError, MongoWriteConcernError } from './error'; +export { configureExplicitResourceManagement } from './resource_management'; export { AbstractCursor, // Actual driver classes exported @@ -111,7 +112,6 @@ export { ReturnDocument } from './operations/find_and_modify'; export { ProfilingLevel } from './operations/set_profiling_level'; export { ReadConcernLevel } from './read_concern'; export { ReadPreferenceMode } from './read_preference'; -export { AsyncDisposable } from './resource_management'; export { ServerType, TopologyType } from './sdam/common'; // Helper classes @@ -521,6 +521,7 @@ export type { ReadPreferenceLikeOptions, ReadPreferenceOptions } from './read_preference'; +export type { AsyncDisposable } from './resource_management'; export type { ClusterTime, TimerQueue } from './sdam/common'; export type { Monitor, diff --git a/src/mongo_client.ts b/src/mongo_client.ts index 111bd0aad6f..4f70b769539 100644 --- a/src/mongo_client.ts +++ b/src/mongo_client.ts @@ -34,7 +34,7 @@ import { executeOperation } from './operations/execute_operation'; import { RunAdminCommandOperation } from './operations/run_command'; import type { ReadConcern, ReadConcernLevel, ReadConcernLike } from './read_concern'; import { ReadPreference, type ReadPreferenceMode } from './read_preference'; -import { type AsyncDisposable } from './resource_management'; +import { type AsyncDisposable, configureResourceManagement } from './resource_management'; import type { ServerMonitoringMode } from './sdam/monitor'; import type { TagSet } from './sdam/server_description'; import { readPreferenceServerSelector } from './sdam/server_selection'; @@ -407,6 +407,10 @@ export class MongoClient extends TypedEventEmitter implements /** @beta */ declare [Symbol.asyncDispose]: () => Promise; + /** @internal */ + async dispose() { + await this.close(); + } /** @internal */ private checkForNonGenuineHosts() { @@ -762,15 +766,7 @@ export class MongoClient extends TypedEventEmitter implements } } -Symbol.asyncDispose && - Object.defineProperty(MongoClient.prototype, Symbol.asyncDispose, { - value: async function asyncDispose(this: { close(): Promise }) { - await this.close(); - }, - enumerable: false, - configurable: true, - writable: true - }); +configureResourceManagement(MongoClient.prototype); /** * Parsed Mongo Client Options. diff --git a/src/resource_management.ts b/src/resource_management.ts index 7959e1c95d0..71a88478700 100644 --- a/src/resource_management.ts +++ b/src/resource_management.ts @@ -1,15 +1,29 @@ -import { ChangeStream } from './change_stream'; -import { AbstractCursor } from './cursor/abstract_cursor'; -import { MongoClient } from './mongo_client'; -import { ClientSession } from './sessions'; - /** * @public - * @experimental */ export interface AsyncDisposable { /** @beta */ - [Symbol.asyncDispose]: () => Promise; + [Symbol.asyncDispose](): Promise; + + /** + * @internal + * + * A method that wraps disposal semantics for a given resource in the class. + */ + dispose(): Promise; +} + +/** @internal */ +export function configureResourceManagement(target: AsyncDisposable) { + Symbol.asyncDispose && + Object.defineProperty(target, Symbol.asyncDispose, { + value: async function asyncDispose(this: AsyncDisposable) { + await this.dispose(); + }, + enumerable: false, + configurable: true, + writable: true + }); } /** @@ -35,43 +49,22 @@ export interface AsyncDisposable { * ``` */ export function configureExplicitResourceManagement() { - Symbol.asyncDispose && - Object.defineProperty(MongoClient.prototype, Symbol.asyncDispose, { - value: async function asyncDispose(this: { close(): Promise }) { - await this.close(); - }, - enumerable: false, - configurable: true, - writable: true - }); + // We must import lazily here, because there's a circular dependency between the resource management + // file and each resources' file. We could move `configureResourceManagement` to a separate + // function, but keeping all resource-management related code together seemed preferable and I chose + // lazy requiring of resources instead. - Symbol.asyncDispose && - Object.defineProperty(AbstractCursor.prototype, Symbol.asyncDispose, { - value: async function asyncDispose(this: { close(): Promise }) { - await this.close(); - }, - enumerable: false, - configurable: true, - writable: true - }); + // eslint-disable-next-line @typescript-eslint/no-var-requires + const { MongoClient } = require('./mongo_client'); + // eslint-disable-next-line @typescript-eslint/no-var-requires + const { ClientSession } = require('./sessions'); + // eslint-disable-next-line @typescript-eslint/no-var-requires + const { AbstractCursor } = require('./cursor/abstract_cursor'); + // eslint-disable-next-line @typescript-eslint/no-var-requires + const { ChangeStream } = require('./change_stream'); - Symbol.asyncDispose && - Object.defineProperty(ChangeStream.prototype, Symbol.asyncDispose, { - value: async function asyncDispose(this: { close(): Promise }) { - await this.close(); - }, - enumerable: false, - configurable: true, - writable: true - }); - - Symbol.asyncDispose && - Object.defineProperty(ClientSession.prototype, Symbol.asyncDispose, { - value: async function asyncDispose(this: { endSession(): Promise }) { - await this.endSession(); - }, - enumerable: false, - configurable: true, - writable: true - }); + configureResourceManagement(MongoClient.prototype); + configureResourceManagement(ClientSession.prototype); + configureResourceManagement(AbstractCursor.prototype); + configureResourceManagement(ChangeStream.prototype); } diff --git a/src/sessions.ts b/src/sessions.ts index 29871168b30..9cee29d3f97 100644 --- a/src/sessions.ts +++ b/src/sessions.ts @@ -27,7 +27,7 @@ import { executeOperation } from './operations/execute_operation'; import { RunAdminCommandOperation } from './operations/run_command'; import { ReadConcernLevel } from './read_concern'; import { ReadPreference } from './read_preference'; -import { type AsyncDisposable } from './resource_management'; +import { type AsyncDisposable, configureResourceManagement } from './resource_management'; import { _advanceClusterTime, type ClusterTime, TopologyType } from './sdam/common'; import { isTransactionCommand, @@ -292,6 +292,10 @@ export class ClientSession } /** @beta */ declare [Symbol.asyncDispose]: () => Promise; + /** @internal */ + async dispose() { + await this.endSession({ force: true }); + } /** * Advances the operationTime for a ClientSession. @@ -490,15 +494,7 @@ export class ClientSession } } -Symbol.asyncDispose && - Object.defineProperty(ClientSession.prototype, Symbol.asyncDispose, { - value: async function asyncDispose(this: { endSession(): Promise }) { - await this.endSession(); - }, - enumerable: false, - configurable: true, - writable: true - }); +configureResourceManagement(ClientSession.prototype); const MAX_WITH_TRANSACTION_TIMEOUT = 120000; const NON_DETERMINISTIC_WRITE_CONCERN_ERRORS = new Set([ diff --git a/src/utils.ts b/src/utils.ts index cebb81e0a0d..52f893c1798 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -31,6 +31,7 @@ import type { CommandOperationOptions, OperationParent } from './operations/comm import type { Hint, OperationOptions } from './operations/operation'; import { ReadConcern } from './read_concern'; import { ReadPreference } from './read_preference'; +import type { AsyncDisposable } from './resource_management'; import { ServerType } from './sdam/common'; import type { Server } from './sdam/server'; import type { Topology } from './sdam/topology'; diff --git a/test/unit/index.test.ts b/test/unit/index.test.ts index 0a0bbdc98d8..1478fa5ffc0 100644 --- a/test/unit/index.test.ts +++ b/test/unit/index.test.ts @@ -16,7 +16,6 @@ const EXPECTED_EXPORTS = [ 'AbstractCursor', 'Admin', 'AggregationCursor', - 'AsyncDisposable', 'AuthMechanism', 'AutoEncryptionLoggerLevel', 'BatchType', @@ -32,6 +31,7 @@ const EXPECTED_EXPORTS = [ 'ClientSession', 'Code', 'Collection', + 'configureExplicitResourceManagement', 'CommandFailedEvent', 'CommandStartedEvent', 'CommandSucceededEvent', From d9ed40c2594ab406f3e5d62f30ddb0ddc5f4d049 Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Fri, 26 Jul 2024 15:10:36 -0600 Subject: [PATCH 08/17] fix interface function name && fix lint --- src/change_stream.ts | 2 +- src/cursor/abstract_cursor.ts | 2 +- src/mongo_client.ts | 2 +- src/resource_management.ts | 4 ++-- src/sessions.ts | 2 +- src/utils.ts | 1 - 6 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/change_stream.ts b/src/change_stream.ts index 4ec753bd1d3..803ffc6f735 100644 --- a/src/change_stream.ts +++ b/src/change_stream.ts @@ -554,7 +554,7 @@ export class ChangeStream< /** @beta */ declare [Symbol.asyncDispose]: () => Promise; /** @internal */ - async dispose() { + async asyncDispose() { await this.close(); } diff --git a/src/cursor/abstract_cursor.ts b/src/cursor/abstract_cursor.ts index d96a917d5bb..907d2e53727 100644 --- a/src/cursor/abstract_cursor.ts +++ b/src/cursor/abstract_cursor.ts @@ -282,7 +282,7 @@ export abstract class AbstractCursor< /** @beta */ declare [Symbol.asyncDispose]: () => Promise; /** @internal */ - async dispose() { + async asyncDispose() { await this.close(); } diff --git a/src/mongo_client.ts b/src/mongo_client.ts index 4f70b769539..657b6a21f1f 100644 --- a/src/mongo_client.ts +++ b/src/mongo_client.ts @@ -408,7 +408,7 @@ export class MongoClient extends TypedEventEmitter implements /** @beta */ declare [Symbol.asyncDispose]: () => Promise; /** @internal */ - async dispose() { + async asyncDispose() { await this.close(); } diff --git a/src/resource_management.ts b/src/resource_management.ts index 71a88478700..a0bf5994c8d 100644 --- a/src/resource_management.ts +++ b/src/resource_management.ts @@ -10,7 +10,7 @@ export interface AsyncDisposable { * * A method that wraps disposal semantics for a given resource in the class. */ - dispose(): Promise; + asyncDispose(): Promise; } /** @internal */ @@ -18,7 +18,7 @@ export function configureResourceManagement(target: AsyncDisposable) { Symbol.asyncDispose && Object.defineProperty(target, Symbol.asyncDispose, { value: async function asyncDispose(this: AsyncDisposable) { - await this.dispose(); + await this.asyncDispose(); }, enumerable: false, configurable: true, diff --git a/src/sessions.ts b/src/sessions.ts index 9cee29d3f97..4ebbef6cca9 100644 --- a/src/sessions.ts +++ b/src/sessions.ts @@ -293,7 +293,7 @@ export class ClientSession /** @beta */ declare [Symbol.asyncDispose]: () => Promise; /** @internal */ - async dispose() { + async asyncDispose() { await this.endSession({ force: true }); } diff --git a/src/utils.ts b/src/utils.ts index 52f893c1798..cebb81e0a0d 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -31,7 +31,6 @@ import type { CommandOperationOptions, OperationParent } from './operations/comm import type { Hint, OperationOptions } from './operations/operation'; import { ReadConcern } from './read_concern'; import { ReadPreference } from './read_preference'; -import type { AsyncDisposable } from './resource_management'; import { ServerType } from './sdam/common'; import type { Server } from './sdam/server'; import type { Topology } from './sdam/topology'; From 8c75a588f47ba42e3b192d1fbd00bfec48ffdddc Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Mon, 29 Jul 2024 15:52:35 -0600 Subject: [PATCH 09/17] comments - add testing docs for the smoke tests - smoke tests depend on a stable package filename, instead of a filename that includes the version --- .../run-resource-management-smoke-tests.sh | 4 ++++ test/explicit-resource-management/package.json | 2 +- test/readme.md | 17 +++++++++-------- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/.evergreen/run-resource-management-smoke-tests.sh b/.evergreen/run-resource-management-smoke-tests.sh index 9d047969abc..70854dae1d8 100644 --- a/.evergreen/run-resource-management-smoke-tests.sh +++ b/.evergreen/run-resource-management-smoke-tests.sh @@ -6,6 +6,10 @@ echo "Building driver..." npm pack echo "Building driver...finished." +PACKAGE_FILE=$(ls mongodb-*.tgz) + +mv $PACKAGE_FILE mongodb-current.tgz + echo "Node version: $(node -v)" cd test/explicit-resource-management diff --git a/test/explicit-resource-management/package.json b/test/explicit-resource-management/package.json index b5ac33ef64a..659c8557441 100644 --- a/test/explicit-resource-management/package.json +++ b/test/explicit-resource-management/package.json @@ -14,7 +14,7 @@ "dependencies": { "@types/chai": "^4.3.16", "chai": "^4.4.1", - "mongodb": "../../mongodb-6.8.0.tgz", + "mongodb": "../../mongodb-current.tgz", "tslib": "^2.6.3" }, "devDependencies": { diff --git a/test/readme.md b/test/readme.md index 78230d35858..d07bfafa1c1 100644 --- a/test/readme.md +++ b/test/readme.md @@ -32,6 +32,7 @@ Below is a summary of the types of test automation in this repo. | TypeScript Definition | `/test/types` | The TypeScript definition tests verify the type definitions are correct. | `npm run check:tsd` | | GitHub Actions | `/test/action` | Tests that run as GitHub Actions such as dependency checking. | Currently, only `npm run check:dependencies` but could be expanded to more in the future. | | Code Examples | `/test/integration/node-specific/examples` | Code examples that are also paired with tests that show they are working examples. | Currently, `npm run check:lambda` to test the AWS Lambda example with default auth and `npm run check:lambda:aws` to test the AWS Lambda example with AWS auth. | +| Explicit Resource Management | `/test/integration/explicit-resource-management` | Tests that use explicit resource management with the drivers' disposable resources. | `bash .evergreen/run-resource-management-smoke-test.sh` | ### Spec Tests @@ -101,7 +102,7 @@ You can prefix `npm test` with a `MONGODB_URI` environment variable to point the MONGODB_URI=mongodb://localhost:27017 npm test ``` -For a replica set, you might use: +For a replica set, you might use: ```sh MONGODB_URI=mongodb://localhost:31000,localhost:31001,localhost:31002/?replicaSet=rs npm test @@ -115,14 +116,14 @@ The easiest way to run a single test is by appending `.only()` to the test conte it.only('cool test', function() {}) ``` -Then, run the test using `npm run check:test` for a functional or integration test or +Then, run the test using `npm run check:test` for a functional or integration test or `npm run check:unit` for a unit test. See [Mocha's documentation][mocha-only] for more detailed information on `.only()`. Another way to run a single test is to use Mocha's `grep` flag. For functional or integration tests, run: ```sh npm run check:test -- -g -``` +``` For unit tests, run: ```sh npm run check:unit -- -g @@ -133,7 +134,7 @@ See the [Mocha documentation][mocha-grep] for information on the `grep` flag. [Evergreen][evergreen-wiki] is the continuous integration (CI) system we use. Evergreen builds are automatically run whenever a pull request is created or when commits are pushed to particular branches (e.g., `main`, `4.0`, and `3.6`). -Each Evergreen build runs the test suite against a variety of build variants that include a combination of topologies, special environments, and operating systems. By default, commits in pull requests only run a subset of the build variants in order to save time and resources. To configure a build, update `.evergreen/config.yml.in` and then generate a new Evergreen config via: +Each Evergreen build runs the test suite against a variety of build variants that include a combination of topologies, special environments, and operating systems. By default, commits in pull requests only run a subset of the build variants in order to save time and resources. To configure a build, update `.evergreen/config.yml.in` and then generate a new Evergreen config via: ```sh node .evergreen/generate_evergreen_tasks.js @@ -353,13 +354,13 @@ The following steps will walk you through how to start and test a load balancer. Create two `mongos` running on ports `27017` and `27018`: ```sh - mongos --configdb test/localhost:27217 --bind_ip localhost --setParameter enableTestCommands=1 --setParameter loadBalancerPort=27050 + mongos --configdb test/localhost:27217 --bind_ip localhost --setParameter enableTestCommands=1 --setParameter loadBalancerPort=27050 mongos --configdb test/localhost:27217 --port 27018 --bind_ip localhost --setParameter enableTestCommands=1 --setParameter loadBalancerPort=27051 ``` Initiate cluster on `mongos` in shell: ```sh - mongosh "mongodb://localhost:27017" --eval "sh.addShard('testing/localhost:27218,localhost:27219,localhost:27220')" + mongosh "mongodb://localhost:27017" --eval "sh.addShard('testing/localhost:27218,localhost:27219,localhost:27220')" mongosh "mongodb://localhost:27017" --eval "sh.enableSharding('test')" ``` 1. An alternative way to the fully manual cluster setup is to use `mlaunch`: @@ -434,7 +435,7 @@ The following steps will walk you through how to run the tests for CSFLE. 1. Install [MongoDB Client Encryption][npm-csfle] if you haven't already: ```sh npm install mongodb-client-encryption - ``` + ``` > **Note:** if developing changes in `mongodb-client-encryption`, you can link it locally using `etc/tooling/fle.sh`. @@ -599,7 +600,7 @@ lerna run test --scope @mongosh/service-provider-server npm i --no-save mongodb-client-encryption ``` 6. Launch a MongoDB server -7. Run the full suite: +7. Run the full suite: ```sh npm run check:test ``` From d66d248d51380ed9007ac774383ac6268e788aab Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Tue, 30 Jul 2024 11:47:36 -0600 Subject: [PATCH 10/17] comments pt 2 --- .evergreen/config.in.yml | 2 +- .evergreen/config.yml | 6 +- .evergreen/generate_evergreen_tasks.js | 4 +- .../explicit-resource-management/main.test.ts | 116 ++++++------------ test/manual/resource_management.test.ts | 2 +- 5 files changed, 47 insertions(+), 83 deletions(-) diff --git a/.evergreen/config.in.yml b/.evergreen/config.in.yml index f65562ed74f..aa0f4fd5ab5 100644 --- a/.evergreen/config.in.yml +++ b/.evergreen/config.in.yml @@ -365,7 +365,7 @@ functions: args: - "${PROJECT_DIRECTORY}/.evergreen/run-resource-management.sh" - "check resource management smoke tests": + "check resource management feature integration": - command: subprocess.exec type: test params: diff --git a/.evergreen/config.yml b/.evergreen/config.yml index fef99ef6137..310ff753e17 100644 --- a/.evergreen/config.yml +++ b/.evergreen/config.yml @@ -327,7 +327,7 @@ functions: binary: bash args: - ${PROJECT_DIRECTORY}/.evergreen/run-resource-management.sh - check resource management smoke tests: + check resource management feature integration: - command: subprocess.exec type: test params: @@ -3508,7 +3508,7 @@ tasks: - {key: NPM_VERSION, value: '9'} - func: install dependencies - func: check resource management - - name: test-explicit-resource-management-smoke-tests + - name: test-explicit-resource-management-feature-integration tags: - resource-management commands: @@ -3521,7 +3521,7 @@ tasks: - {key: NODE_LTS_VERSION, value: latest} - func: install dependencies - func: bootstrap mongo-orchestration - - func: check resource management smoke tests + - func: check resource management feature integration - name: check-types-typescript-next-node-types-20.14.10 tags: - check-types-typescript-next diff --git a/.evergreen/generate_evergreen_tasks.js b/.evergreen/generate_evergreen_tasks.js index 3346679b235..52d5d2124e3 100644 --- a/.evergreen/generate_evergreen_tasks.js +++ b/.evergreen/generate_evergreen_tasks.js @@ -517,7 +517,7 @@ SINGLETON_TASKS.push( ] }, { - name: 'test-explicit-resource-management-smoke-tests', + name: 'test-explicit-resource-management-feature-integration', tags: ['resource-management'], commands: [ updateExpansions({ @@ -527,7 +527,7 @@ SINGLETON_TASKS.push( }), { func: 'install dependencies' }, { func: 'bootstrap mongo-orchestration' }, - { func: 'check resource management smoke tests' } + { func: 'check resource management feature integration' } ] }, ...Array.from(makeTypescriptTasks()) diff --git a/test/explicit-resource-management/main.test.ts b/test/explicit-resource-management/main.test.ts index 69c3dd54eeb..0722e9ad8ec 100644 --- a/test/explicit-resource-management/main.test.ts +++ b/test/explicit-resource-management/main.test.ts @@ -1,8 +1,6 @@ -import { expect } from 'chai'; import { describe, it } from 'mocha'; -import { AbstractCursor, ChangeStream, ClientSession, GridFSBucket, MongoClient } from 'mongodb/lib/beta'; -import * as sinon from 'sinon'; +import { GridFSBucket, MongoClient } from 'mongodb/lib/beta'; import { Readable } from 'stream'; import { pipeline } from 'stream/promises'; import { setTimeout } from 'timers/promises'; @@ -17,104 +15,70 @@ async function setUpCollection(client: MongoClient) { } describe('explicit resource management smoke tests', function () { - const clientSpy = sinon.spy(MongoClient.prototype, Symbol.asyncDispose); - const cursorSpy = sinon.spy(AbstractCursor.prototype, Symbol.asyncDispose); - const endSessionSpy = sinon.spy(ClientSession.prototype, Symbol.asyncDispose); - const changeStreamSpy = sinon.spy(ChangeStream.prototype, Symbol.asyncDispose); - const readableSpy = sinon.spy(Readable.prototype, Symbol.asyncDispose); - - afterEach(function () { - clientSpy.resetHistory(); - cursorSpy.resetHistory(); - endSessionSpy.resetHistory(); - changeStreamSpy.resetHistory(); - readableSpy.resetHistory(); - }); - describe('MongoClient', function () { - it('can be used with await-using syntax', async function () { - { - await using client = new MongoClient(process.env.MONGODB_URI!); - await client.connect(); - } - expect(clientSpy.called).to.be.true; - expect(clientSpy.callCount).to.equal(1); + it('does not crash or error when used with await-using syntax', async function () { + await using client = new MongoClient(process.env.MONGODB_URI!); + await client.connect(); }) }) describe('Cursors', function () { - it('can be used with await-using syntax', async function () { - { - await using client = new MongoClient(process.env.MONGODB_URI!); - await client.connect(); - - const collection = await setUpCollection(client); - - await using cursor = collection.find(); - await cursor.next(); - await cursor.next(); - await cursor.next(); - } - expect(cursorSpy.callCount).to.equal(1); + it('does not crash or error when used with await-using syntax', async function () { + await using client = new MongoClient(process.env.MONGODB_URI!); + await client.connect(); + + const collection = await setUpCollection(client); + + await using cursor = collection.find(); + await cursor.next(); + await cursor.next(); + await cursor.next(); }) describe('cursor streams', function() { - it('can be used with await-using syntax', async function() { - { - await using client = new MongoClient(process.env.MONGODB_URI!); - await client.connect(); + it('does not crash or error when used with await-using syntax', async function() { + await using client = new MongoClient(process.env.MONGODB_URI!); + await client.connect(); - const collection = await setUpCollection(client); + const collection = await setUpCollection(client); - await using readable = collection.find().stream(); - } - expect(readableSpy.callCount).to.equal(1); + await using readable = collection.find().stream(); }) }) }) describe('Sessions', function () { - it('can be used with await-using syntax', async function () { - { - await using client = new MongoClient(process.env.MONGODB_URI!); - await client.connect(); - - await using session = client.startSession(); - } - expect(endSessionSpy.callCount).to.equal(1); + it('does not crash or error when used with await-using syntax', async function () { + await using client = new MongoClient(process.env.MONGODB_URI!); + await client.connect(); + + await using session = client.startSession(); }) }) describe('ChangeStreams', function () { - it('can be used with await-using syntax', async function () { - { - await using client = new MongoClient(process.env.MONGODB_URI!); - await client.connect(); - - const collection = await setUpCollection(client); - await using cs = collection.watch(); - - setTimeout(1000).then(() => collection.insertOne({ name: 'bailey' })); - await cs.next(); - } - expect(changeStreamSpy.callCount).to.equal(1); + it('does not crash or error when used with await-using syntax', async function () { + await using client = new MongoClient(process.env.MONGODB_URI!); + await client.connect(); + + const collection = await setUpCollection(client); + await using cs = collection.watch(); + + setTimeout(1000).then(() => collection.insertOne({ name: 'bailey' })); + await cs.next(); }) }); describe('GridFSDownloadStream', function () { - it('can be used with await-using syntax', async function () { - { - await using client = new MongoClient(process.env.MONGODB_URI!); - await client.connect(); - - const bucket = new GridFSBucket(client.db('foo')); - const uploadStream = bucket.openUploadStream('foo.txt') - await pipeline(Readable.from("AAAAAAA".split('')), uploadStream); + it('does not crash or error when used with await-using syntax', async function () { + await using client = new MongoClient(process.env.MONGODB_URI!); + await client.connect(); - await using downloadStream = bucket.openDownloadStreamByName('foo.txt'); + const bucket = new GridFSBucket(client.db('foo')); + const uploadStream = bucket.openUploadStream('foo.txt') + await pipeline(Readable.from("AAAAAAA".split('')), uploadStream); - } - expect(readableSpy.callCount).to.equal(1); + await using downloadStream = bucket.openDownloadStreamByName('foo.txt'); }) }); }) diff --git a/test/manual/resource_management.test.ts b/test/manual/resource_management.test.ts index f41a98cdb69..16818383850 100644 --- a/test/manual/resource_management.test.ts +++ b/test/manual/resource_management.test.ts @@ -3,7 +3,7 @@ import * as sinon from 'sinon'; import { AbstractCursor, ChangeStream, ClientSession, MongoClient } from '../mongodb'; -describe('Explicit Resource Management Tests', function () { +describe('Symbol.asyncDispose implementation tests', function () { let client: MongoClient; afterEach(async function () { From 8e20e0c60138c20b73b16370e35d9a2662da2473 Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Tue, 30 Jul 2024 11:55:54 -0600 Subject: [PATCH 11/17] tabs to spaces --- .../explicit-resource-management/main.test.ts | 140 +++++++++--------- 1 file changed, 70 insertions(+), 70 deletions(-) diff --git a/test/explicit-resource-management/main.test.ts b/test/explicit-resource-management/main.test.ts index 0722e9ad8ec..e65738a3750 100644 --- a/test/explicit-resource-management/main.test.ts +++ b/test/explicit-resource-management/main.test.ts @@ -6,79 +6,79 @@ import { pipeline } from 'stream/promises'; import { setTimeout } from 'timers/promises'; async function setUpCollection(client: MongoClient) { - const collection = client.db('foo').collection<{ name: string }>('bar'); - const documents: Array<{ name: string }> = Array.from({ length: 5 }).map(i => ({ - name: String(i) - })); - await collection.insertMany(documents) - return collection; + const collection = client.db('foo').collection<{ name: string }>('bar'); + const documents: Array<{ name: string }> = Array.from({ length: 5 }).map(i => ({ + name: String(i) + })); + await collection.insertMany(documents) + return collection; } describe('explicit resource management smoke tests', function () { - describe('MongoClient', function () { - it('does not crash or error when used with await-using syntax', async function () { + describe('MongoClient', function () { + it('does not crash or error when used with await-using syntax', async function () { await using client = new MongoClient(process.env.MONGODB_URI!); await client.connect(); - }) - }) - - describe('Cursors', function () { - it('does not crash or error when used with await-using syntax', async function () { - await using client = new MongoClient(process.env.MONGODB_URI!); - await client.connect(); - - const collection = await setUpCollection(client); - - await using cursor = collection.find(); - await cursor.next(); - await cursor.next(); - await cursor.next(); - }) - - describe('cursor streams', function() { - it('does not crash or error when used with await-using syntax', async function() { - await using client = new MongoClient(process.env.MONGODB_URI!); - await client.connect(); - - const collection = await setUpCollection(client); - - await using readable = collection.find().stream(); - }) - }) - }) - - describe('Sessions', function () { - it('does not crash or error when used with await-using syntax', async function () { - await using client = new MongoClient(process.env.MONGODB_URI!); - await client.connect(); - - await using session = client.startSession(); - }) - }) - - describe('ChangeStreams', function () { - it('does not crash or error when used with await-using syntax', async function () { - await using client = new MongoClient(process.env.MONGODB_URI!); - await client.connect(); - - const collection = await setUpCollection(client); - await using cs = collection.watch(); - - setTimeout(1000).then(() => collection.insertOne({ name: 'bailey' })); - await cs.next(); - }) - }); - - describe('GridFSDownloadStream', function () { - it('does not crash or error when used with await-using syntax', async function () { - await using client = new MongoClient(process.env.MONGODB_URI!); - await client.connect(); - - const bucket = new GridFSBucket(client.db('foo')); - const uploadStream = bucket.openUploadStream('foo.txt') - await pipeline(Readable.from("AAAAAAA".split('')), uploadStream); - - await using downloadStream = bucket.openDownloadStreamByName('foo.txt'); - }) - }); + }) + }) + + describe('Cursors', function () { + it('does not crash or error when used with await-using syntax', async function () { + await using client = new MongoClient(process.env.MONGODB_URI!); + await client.connect(); + + const collection = await setUpCollection(client); + + await using cursor = collection.find(); + await cursor.next(); + await cursor.next(); + await cursor.next(); + }) + + describe('cursor streams', function() { + it('does not crash or error when used with await-using syntax', async function() { + await using client = new MongoClient(process.env.MONGODB_URI!); + await client.connect(); + + const collection = await setUpCollection(client); + + await using readable = collection.find().stream(); + }) + }) + }) + + describe('Sessions', function () { + it('does not crash or error when used with await-using syntax', async function () { + await using client = new MongoClient(process.env.MONGODB_URI!); + await client.connect(); + + await using session = client.startSession(); + }) + }) + + describe('ChangeStreams', function () { + it('does not crash or error when used with await-using syntax', async function () { + await using client = new MongoClient(process.env.MONGODB_URI!); + await client.connect(); + + const collection = await setUpCollection(client); + await using cs = collection.watch(); + + setTimeout(1000).then(() => collection.insertOne({ name: 'bailey' })); + await cs.next(); + }) + }); + + describe('GridFSDownloadStream', function () { + it('does not crash or error when used with await-using syntax', async function () { + await using client = new MongoClient(process.env.MONGODB_URI!); + await client.connect(); + + const bucket = new GridFSBucket(client.db('foo')); + const uploadStream = bucket.openUploadStream('foo.txt') + await pipeline(Readable.from("AAAAAAA".split('')), uploadStream); + + await using downloadStream = bucket.openDownloadStreamByName('foo.txt'); + }) + }); }) From 412829f30f05c2c8b31d07b1a59d59e663359345 Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Tue, 30 Jul 2024 16:02:22 -0600 Subject: [PATCH 12/17] comments pt 3 --- .evergreen/config.in.yml | 2 +- .evergreen/config.yml | 2 +- ....sh => run-resource-management-feature-integration.sh} | 0 test/explicit-resource-management/main.test.ts | 4 +--- test/explicit-resource-management/package.json | 4 +--- test/manual/resource_management.test.ts | 8 ++++---- test/readme.md | 2 +- 7 files changed, 9 insertions(+), 13 deletions(-) rename .evergreen/{run-resource-management-smoke-tests.sh => run-resource-management-feature-integration.sh} (100%) diff --git a/.evergreen/config.in.yml b/.evergreen/config.in.yml index aa0f4fd5ab5..85c963ad7a7 100644 --- a/.evergreen/config.in.yml +++ b/.evergreen/config.in.yml @@ -376,7 +376,7 @@ functions: MONGODB_URI: ${MONGODB_URI} binary: bash args: - - "${PROJECT_DIRECTORY}/.evergreen/run-resource-management-smoke-tests.sh" + - "${PROJECT_DIRECTORY}/.evergreen/run-resource-management-feature-integration.sh" "compile driver": - command: subprocess.exec diff --git a/.evergreen/config.yml b/.evergreen/config.yml index 310ff753e17..570435d3e2e 100644 --- a/.evergreen/config.yml +++ b/.evergreen/config.yml @@ -338,7 +338,7 @@ functions: MONGODB_URI: ${MONGODB_URI} binary: bash args: - - ${PROJECT_DIRECTORY}/.evergreen/run-resource-management-smoke-tests.sh + - ${PROJECT_DIRECTORY}/.evergreen/run-resource-management-feature-integration.sh compile driver: - command: subprocess.exec type: test diff --git a/.evergreen/run-resource-management-smoke-tests.sh b/.evergreen/run-resource-management-feature-integration.sh similarity index 100% rename from .evergreen/run-resource-management-smoke-tests.sh rename to .evergreen/run-resource-management-feature-integration.sh diff --git a/test/explicit-resource-management/main.test.ts b/test/explicit-resource-management/main.test.ts index e65738a3750..5094671c674 100644 --- a/test/explicit-resource-management/main.test.ts +++ b/test/explicit-resource-management/main.test.ts @@ -14,7 +14,7 @@ async function setUpCollection(client: MongoClient) { return collection; } -describe('explicit resource management smoke tests', function () { +describe('explicit resource management feature integration tests', function () { describe('MongoClient', function () { it('does not crash or error when used with await-using syntax', async function () { await using client = new MongoClient(process.env.MONGODB_URI!); @@ -31,8 +31,6 @@ describe('explicit resource management smoke tests', function () { await using cursor = collection.find(); await cursor.next(); - await cursor.next(); - await cursor.next(); }) describe('cursor streams', function() { diff --git a/test/explicit-resource-management/package.json b/test/explicit-resource-management/package.json index 659c8557441..adc23fb3109 100644 --- a/test/explicit-resource-management/package.json +++ b/test/explicit-resource-management/package.json @@ -15,9 +15,7 @@ "@types/chai": "^4.3.16", "chai": "^4.4.1", "mongodb": "../../mongodb-current.tgz", - "tslib": "^2.6.3" - }, - "devDependencies": { + "tslib": "^2.6.3", "mocha": "^10.7.0", "typescript": "5.5" } diff --git a/test/manual/resource_management.test.ts b/test/manual/resource_management.test.ts index 16818383850..d92315ba3bd 100644 --- a/test/manual/resource_management.test.ts +++ b/test/manual/resource_management.test.ts @@ -19,7 +19,7 @@ describe('Symbol.asyncDispose implementation tests', function () { }); describe('MongoClient', function () { - it('closes the the client', async function () { + it('the Symbol.asyncDispose method calls close()', async function () { client = new MongoClient('mongodb://localhost:27017'); const spy = sinon.spy(client, 'close'); @@ -29,7 +29,7 @@ describe('Symbol.asyncDispose implementation tests', function () { }); describe('ClientSession', function () { - it('ends the session', async function () { + it('the Symbol.asyncDispose method calls endSession()', async function () { client = new MongoClient('mongodb://localhost:27017'); const session = client.startSession(); @@ -40,7 +40,7 @@ describe('Symbol.asyncDispose implementation tests', function () { }); describe('ChangeStreams', function () { - it('closes the change stream', async function () { + it('the Symbol.asyncDispose method calls close()', async function () { client = new MongoClient('mongodb://localhost:27017'); const changeStream = client.watch(); @@ -51,7 +51,7 @@ describe('Symbol.asyncDispose implementation tests', function () { }); describe('cursors', function () { - it('closes the cursor', async function () { + it('the Symbol.asyncDispose method calls close()', async function () { client = new MongoClient('mongodb://localhost:27017'); const cursor = client.db('foo').collection('bar').find(); diff --git a/test/readme.md b/test/readme.md index d07bfafa1c1..34ce8373e3d 100644 --- a/test/readme.md +++ b/test/readme.md @@ -32,7 +32,7 @@ Below is a summary of the types of test automation in this repo. | TypeScript Definition | `/test/types` | The TypeScript definition tests verify the type definitions are correct. | `npm run check:tsd` | | GitHub Actions | `/test/action` | Tests that run as GitHub Actions such as dependency checking. | Currently, only `npm run check:dependencies` but could be expanded to more in the future. | | Code Examples | `/test/integration/node-specific/examples` | Code examples that are also paired with tests that show they are working examples. | Currently, `npm run check:lambda` to test the AWS Lambda example with default auth and `npm run check:lambda:aws` to test the AWS Lambda example with AWS auth. | -| Explicit Resource Management | `/test/integration/explicit-resource-management` | Tests that use explicit resource management with the drivers' disposable resources. | `bash .evergreen/run-resource-management-smoke-test.sh` | +| Explicit Resource Management | `/test/integration/explicit-resource-management` | Tests that use explicit resource management with the driver's disposable resources. | `bash .evergreen/run-resource-management-feature-integration.sh` | ### Spec Tests From 6ae7ff55dce0e854feb993eaf9ec7a6751ecf360 Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Wed, 31 Jul 2024 14:37:45 -0600 Subject: [PATCH 13/17] comments --- src/change_stream.ts | 5 +- src/cursor/abstract_cursor.ts | 5 +- src/mongo_client.ts | 5 +- src/resource_management.ts | 6 +- src/sessions.ts | 5 +- .../explicit-resource-management/main.test.ts | 235 +++++++++++++++++- test/readme.md | 2 +- 7 files changed, 253 insertions(+), 10 deletions(-) diff --git a/src/change_stream.ts b/src/change_stream.ts index 803ffc6f735..ef9a924cee0 100644 --- a/src/change_stream.ts +++ b/src/change_stream.ts @@ -551,7 +551,10 @@ export class ChangeStream< extends TypedEventEmitter> implements AsyncDisposable { - /** @beta */ + /** + * @beta + * @experimental + */ declare [Symbol.asyncDispose]: () => Promise; /** @internal */ async asyncDispose() { diff --git a/src/cursor/abstract_cursor.ts b/src/cursor/abstract_cursor.ts index 907d2e53727..3e23846f5c4 100644 --- a/src/cursor/abstract_cursor.ts +++ b/src/cursor/abstract_cursor.ts @@ -279,7 +279,10 @@ export abstract class AbstractCursor< return !!this.cursorClient.topology?.loadBalanced; } - /** @beta */ + /** + * @beta + * @experimental + */ declare [Symbol.asyncDispose]: () => Promise; /** @internal */ async asyncDispose() { diff --git a/src/mongo_client.ts b/src/mongo_client.ts index 657b6a21f1f..d35aefd41f1 100644 --- a/src/mongo_client.ts +++ b/src/mongo_client.ts @@ -405,7 +405,10 @@ export class MongoClient extends TypedEventEmitter implements this.checkForNonGenuineHosts(); } - /** @beta */ + /** + * @beta + * @experimental + */ declare [Symbol.asyncDispose]: () => Promise; /** @internal */ async asyncDispose() { diff --git a/src/resource_management.ts b/src/resource_management.ts index a0bf5994c8d..7e33f813341 100644 --- a/src/resource_management.ts +++ b/src/resource_management.ts @@ -2,7 +2,10 @@ * @public */ export interface AsyncDisposable { - /** @beta */ + /** + * @beta + * @experimental + */ [Symbol.asyncDispose](): Promise; /** @@ -28,6 +31,7 @@ export function configureResourceManagement(target: AsyncDisposable) { /** * @beta + * @experimental * * Attaches `Symbol.asyncDispose` methods to the MongoClient, Cursors, sessions and change streams * if Symbol.asyncDispose is defined. diff --git a/src/sessions.ts b/src/sessions.ts index 4ebbef6cca9..73db8d7bfd2 100644 --- a/src/sessions.ts +++ b/src/sessions.ts @@ -290,7 +290,10 @@ export class ClientSession maybeClearPinnedConnection(this, { force: true, ...options }); } } - /** @beta */ + /** + * @beta + * @experimental + */ declare [Symbol.asyncDispose]: () => Promise; /** @internal */ async asyncDispose() { diff --git a/test/explicit-resource-management/main.test.ts b/test/explicit-resource-management/main.test.ts index 5094671c674..0a1abda7e56 100644 --- a/test/explicit-resource-management/main.test.ts +++ b/test/explicit-resource-management/main.test.ts @@ -3,7 +3,10 @@ import { describe, it } from 'mocha'; import { GridFSBucket, MongoClient } from 'mongodb/lib/beta'; import { Readable } from 'stream'; import { pipeline } from 'stream/promises'; +import { expect } from 'chai'; import { setTimeout } from 'timers/promises'; +import { createReadStream } from 'fs'; +import { join } from 'path'; async function setUpCollection(client: MongoClient) { const collection = client.db('foo').collection<{ name: string }>('bar'); @@ -17,8 +20,31 @@ async function setUpCollection(client: MongoClient) { describe('explicit resource management feature integration tests', function () { describe('MongoClient', function () { it('does not crash or error when used with await-using syntax', async function () { - await using client = new MongoClient(process.env.MONGODB_URI!); - await client.connect(); + await using client = new MongoClient(process.env.MONGODB_URI!); + await client.connect(); + }) + + it('always cleans up the client, regardless of thrown errors', async function () { + const error = await (async () => { + await using client = new MongoClient(process.env.MONGODB_URI!); + await client.connect(); + + throw new Error('error thrown'); + })().catch(e => e); + + expect(error).to.match(/error thrown/); + }); + + it('works if client is explicitly closed', async function () { + const expected = await (async () => { + await using client = new MongoClient(process.env.MONGODB_URI!); + await client.connect(); + await client.close(); + + return 'not error'; + })(); + + expect(expected).to.equal('not error'); }) }) @@ -33,8 +59,42 @@ describe('explicit resource management feature integration tests', function () { await cursor.next(); }) - describe('cursor streams', function() { - it('does not crash or error when used with await-using syntax', async function() { + it('always cleans up the cursor, regardless of thrown errors', async function () { + const error = await (async () => { + await using client = new MongoClient(process.env.MONGODB_URI!); + await client.connect(); + + const collection = await setUpCollection(client); + + await using cursor = collection.find(); + await cursor.next(); + + throw new Error('error thrown'); + })().catch(e => e); + + expect(error).to.match(/error thrown/); + }); + + it('works if cursor is explicitly closed', async function () { + const expected = await (async () => { + await using client = new MongoClient(process.env.MONGODB_URI!); + await client.connect(); + + const collection = await setUpCollection(client); + + await using cursor = collection.find(); + await cursor.next(); + + await cursor.close(); + + return 'not error'; + })(); + + expect(expected).to.equal('not error'); + }) + + describe('cursor streams', function () { + it('does not crash or error when used with await-using syntax', async function () { await using client = new MongoClient(process.env.MONGODB_URI!); await client.connect(); @@ -42,6 +102,39 @@ describe('explicit resource management feature integration tests', function () { await using readable = collection.find().stream(); }) + + it('always cleans up the stream, regardless of thrown errors', async function () { + const error = await (async () => { + await using client = new MongoClient(process.env.MONGODB_URI!); + await client.connect(); + + const collection = await setUpCollection(client); + + await using readable = collection.find().stream(); + + throw new Error('error thrown'); + })().catch(e => e); + + expect(error).to.match(/error thrown/); + }); + + it('works if stream is explicitly closed', async function () { + const expected = await (async () => { + await using client = new MongoClient(process.env.MONGODB_URI!); + await client.connect(); + + const collection = await setUpCollection(client); + + await using readable = collection.find().stream(); + + readable.destroy(); + + return 'not error'; + })(); + + expect(expected).to.equal('not error'); + }) + }) }) @@ -52,6 +145,34 @@ describe('explicit resource management feature integration tests', function () { await using session = client.startSession(); }) + + it('always cleans up the session, regardless of thrown errors', async function () { + const error = await (async () => { + await using client = new MongoClient(process.env.MONGODB_URI!); + await client.connect(); + + await using session = client.startSession(); + + throw new Error('error thrown'); + })().catch(e => e); + + expect(error).to.match(/error thrown/); + }); + + it('works if session is explicitly closed', async function () { + const expected = await (async () => { + await using client = new MongoClient(process.env.MONGODB_URI!); + await client.connect(); + + await using session = client.startSession(); + + await session.endSession(); + + return 'not error'; + })(); + + expect(expected).to.equal('not error'); + }) }) describe('ChangeStreams', function () { @@ -65,6 +186,41 @@ describe('explicit resource management feature integration tests', function () { setTimeout(1000).then(() => collection.insertOne({ name: 'bailey' })); await cs.next(); }) + + it('always cleans up the change stream, regardless of thrown errors', async function () { + const error = await (async () => { + await using client = new MongoClient(process.env.MONGODB_URI!); + await client.connect(); + + const collection = await setUpCollection(client); + await using cs = collection.watch(); + + setTimeout(1000).then(() => collection.insertOne({ name: 'bailey' })); + await cs.next(); + + throw new Error('error thrown'); + })().catch(e => e); + + expect(error).to.match(/error thrown/); + }); + + it('works if change stream is explicitly closed', async function () { + const expected = await (async () => { + await using client = new MongoClient(process.env.MONGODB_URI!); + await client.connect(); + + const collection = await setUpCollection(client); + await using cs = collection.watch(); + + setTimeout(1000).then(() => collection.insertOne({ name: 'bailey' })); + await cs.next(); + await cs.close(); + + return 'not error'; + })(); + + expect(expected).to.equal('not error'); + }) }); describe('GridFSDownloadStream', function () { @@ -78,5 +234,76 @@ describe('explicit resource management feature integration tests', function () { await using downloadStream = bucket.openDownloadStreamByName('foo.txt'); }) + + it('always cleans up the stream, regardless of thrown errors', async function () { + const error = await (async () => { + await using client = new MongoClient(process.env.MONGODB_URI!); + await client.connect(); + + const bucket = new GridFSBucket(client.db('foo')); + const uploadStream = bucket.openUploadStream('foo.txt') + await pipeline(Readable.from("AAAAAAA".split('')), uploadStream); + + await using downloadStream = bucket.openDownloadStreamByName('foo.txt'); + + throw new Error('error thrown'); + })().catch(e => e); + + expect(error).to.match(/error thrown/); + }); + + it('works if stream is explicitly closed', async function () { + const expected = await (async () => { + await using client = new MongoClient(process.env.MONGODB_URI!); + await client.connect(); + + const bucket = new GridFSBucket(client.db('foo')); + const uploadStream = bucket.openUploadStream('foo.txt') + await pipeline(Readable.from("AAAAAAA".split('')), uploadStream); + + await using downloadStream = bucket.openDownloadStreamByName('foo.txt'); + + await downloadStream.abort(); + + return 'not error'; + })(); + + expect(expected).to.equal('not error'); + }) + + it('throws premature close error if explicitly destroyed early', async function () { + // Gridfs streams inherit their _destroy() and Symbol.asyncDispose implementations from + // Nodejs' readable implementation. This behavior matches the behavior for other readable streams + // (see the below test). + const expected = await (async () => { + await using client = new MongoClient(process.env.MONGODB_URI!); + await client.connect(); + + const bucket = new GridFSBucket(client.db('foo')); + const uploadStream = bucket.openUploadStream('foo.txt') + await pipeline(Readable.from("AAAAAAA".split('')), uploadStream); + + await using downloadStream = bucket.openDownloadStreamByName('foo.txt'); + + downloadStream.destroy(); + + return 'not error'; + })().catch(e => e); + + expect(expected).to.match(/Premature close/); + }) + + it('throws premature close error if explicitly destroyed early (builtin stream)', async function () { + // Gridfs streams inherit their _destroy() and Symbol.asyncDispose implementations from + // Nodejs' readable implementation. This behavior matches the behavior for other readable streams (ie - ReadFileStream) + const expected = await (async () => { + await using readStream = createReadStream(join(__dirname, 'main.test.ts')); + readStream.destroy(); + + return 'not error'; + })().catch(e => e); + + expect(expected).to.match(/Premature close/); + }) }); }) diff --git a/test/readme.md b/test/readme.md index 34ce8373e3d..40872c895c5 100644 --- a/test/readme.md +++ b/test/readme.md @@ -32,7 +32,7 @@ Below is a summary of the types of test automation in this repo. | TypeScript Definition | `/test/types` | The TypeScript definition tests verify the type definitions are correct. | `npm run check:tsd` | | GitHub Actions | `/test/action` | Tests that run as GitHub Actions such as dependency checking. | Currently, only `npm run check:dependencies` but could be expanded to more in the future. | | Code Examples | `/test/integration/node-specific/examples` | Code examples that are also paired with tests that show they are working examples. | Currently, `npm run check:lambda` to test the AWS Lambda example with default auth and `npm run check:lambda:aws` to test the AWS Lambda example with AWS auth. | -| Explicit Resource Management | `/test/integration/explicit-resource-management` | Tests that use explicit resource management with the driver's disposable resources. | `bash .evergreen/run-resource-management-feature-integration.sh` | +| Explicit Resource Management | `/test/explicit-resource-management` | Tests that use explicit resource management with the driver's disposable resources. | `bash .evergreen/run-resource-management-feature-integration.sh` | ### Spec Tests From a68bb87f59322049570c1517273ae0f24be909c6 Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Mon, 5 Aug 2024 10:36:14 -0600 Subject: [PATCH 14/17] add documentation of close methods" --- src/change_stream.ts | 5 ++++- src/cursor/abstract_cursor.ts | 4 ++++ src/mongo_client.ts | 11 ++++++++++- src/sessions.ts | 6 +++++- 4 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/change_stream.ts b/src/change_stream.ts index ef9a924cee0..edba6fbbba0 100644 --- a/src/change_stream.ts +++ b/src/change_stream.ts @@ -554,6 +554,7 @@ export class ChangeStream< /** * @beta * @experimental + * An alias for {@link ChangeStream.close|ChangeStream.close()}. */ declare [Symbol.asyncDispose]: () => Promise; /** @internal */ @@ -779,7 +780,9 @@ export class ChangeStream< return this[kClosed] || this.cursor.closed; } - /** Close the Change Stream */ + /** + * Frees the internal resources used by the change stream. + */ async close(): Promise { this[kClosed] = true; diff --git a/src/cursor/abstract_cursor.ts b/src/cursor/abstract_cursor.ts index 3e23846f5c4..1d697019163 100644 --- a/src/cursor/abstract_cursor.ts +++ b/src/cursor/abstract_cursor.ts @@ -282,6 +282,7 @@ export abstract class AbstractCursor< /** * @beta * @experimental + * An alias for {@link AbstractCursor.close|AbstractCursor.close()}. */ declare [Symbol.asyncDispose]: () => Promise; /** @internal */ @@ -460,6 +461,9 @@ export abstract class AbstractCursor< } } + /** + * Frees any client-side resources used by the cursor. + */ async close(): Promise { await this.cleanup(); } diff --git a/src/mongo_client.ts b/src/mongo_client.ts index d35aefd41f1..853df569d14 100644 --- a/src/mongo_client.ts +++ b/src/mongo_client.ts @@ -408,6 +408,7 @@ export class MongoClient extends TypedEventEmitter implements /** * @beta * @experimental + * An alias for {@link MongoClient.close|MongoClient.close()}. */ declare [Symbol.asyncDispose]: () => Promise; /** @internal */ @@ -581,7 +582,15 @@ export class MongoClient extends TypedEventEmitter implements } /** - * Close the client and its underlying connections + * Cleans up client-side resources used by the MongoCLient and . This includes: + * + * - Closes all open, unused connections (see note). + * - Ends all in-use sessions with {@link ClientSession#endSession|ClientSession.endSession()}. + * - Ends all unused sessions server-side. + * - Cleans up any resources being used for auto encryption if auto encryption is enabled. + * + * @remarks Any in-progress operations are not killed and any connections used by in progress operations + * will be cleaned up lazily as operations finish. * * @param force - Force close, emitting no events */ diff --git a/src/sessions.ts b/src/sessions.ts index 73db8d7bfd2..bd10e7e07c7 100644 --- a/src/sessions.ts +++ b/src/sessions.ts @@ -259,7 +259,10 @@ export class ClientSession } /** - * Ends this session on the server + * Frees any client-side resources held by the current session. If a session is in a transaction, + * the transaction is aborted. + * + * Does not end the session on the server. * * @param options - Optional settings. Currently reserved for future use */ @@ -293,6 +296,7 @@ export class ClientSession /** * @beta * @experimental + * An alias for {@link ClientSession.endSession|ClientSession.endSession()}. */ declare [Symbol.asyncDispose]: () => Promise; /** @internal */ From cc424d5c2df5d68579440ed6b520df663a217a9d Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Mon, 5 Aug 2024 14:26:06 -0600 Subject: [PATCH 15/17] remove void0 assignment --- src/beta.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/beta.ts b/src/beta.ts index 6cb01ed8360..9d068328d44 100644 --- a/src/beta.ts +++ b/src/beta.ts @@ -7,12 +7,11 @@ export * from './index'; * * Since we don't bundle tslib helpers, we need to polyfill this method. * - * This is used in the generated JS. + * This is used in the generated JS. Adapted from https://github.com/microsoft/TypeScript/blob/aafdfe5b3f76f5c41abeec412ce73c86da94c75f/src/compiler/factory/emitHelpers.ts#L1202. */ // eslint-disable-next-line @typescript-eslint/no-unused-vars function __exportStar(mod: Document) { for (const key of Object.keys(mod)) { - exports[key] = void 0; Object.defineProperty(exports, key, { enumerable: true, get: function () { From 025d952b3487f179119b278d57b9375b5320b244 Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Tue, 6 Aug 2024 11:11:00 -0600 Subject: [PATCH 16/17] add sinon assertions --- .../explicit-resource-management/main.test.ts | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/test/explicit-resource-management/main.test.ts b/test/explicit-resource-management/main.test.ts index 0a1abda7e56..fd120e86596 100644 --- a/test/explicit-resource-management/main.test.ts +++ b/test/explicit-resource-management/main.test.ts @@ -1,6 +1,6 @@ import { describe, it } from 'mocha'; -import { GridFSBucket, MongoClient } from 'mongodb/lib/beta'; +import { AbstractCursor, ChangeStream, ClientSession, GridFSBucket, MongoClient } from 'mongodb/lib/beta'; import { Readable } from 'stream'; import { pipeline } from 'stream/promises'; import { expect } from 'chai'; @@ -8,6 +8,8 @@ import { setTimeout } from 'timers/promises'; import { createReadStream } from 'fs'; import { join } from 'path'; +import * as sinon from 'sinon'; + async function setUpCollection(client: MongoClient) { const collection = client.db('foo').collection<{ name: string }>('bar'); const documents: Array<{ name: string }> = Array.from({ length: 5 }).map(i => ({ @@ -18,6 +20,19 @@ async function setUpCollection(client: MongoClient) { } describe('explicit resource management feature integration tests', function () { + const clientDisposeSpy = sinon.spy(MongoClient.prototype, Symbol.asyncDispose); + const sessionDisposeSpy = sinon.spy(ClientSession.prototype, Symbol.asyncDispose); + const changeStreamDisposeSpy = sinon.spy(ChangeStream.prototype, Symbol.asyncDispose); + const cursorDisposeSpy = sinon.spy(AbstractCursor.prototype, Symbol.asyncDispose); + const readableDisposeSpy = sinon.spy(Readable.prototype, Symbol.asyncDispose); + + afterEach(function(){ + clientDisposeSpy.resetHistory(); + sessionDisposeSpy.resetHistory(); + changeStreamDisposeSpy.resetHistory(); + cursorDisposeSpy.resetHistory(); + readableDisposeSpy.resetHistory(); + }) describe('MongoClient', function () { it('does not crash or error when used with await-using syntax', async function () { await using client = new MongoClient(process.env.MONGODB_URI!); @@ -33,6 +48,7 @@ describe('explicit resource management feature integration tests', function () { })().catch(e => e); expect(error).to.match(/error thrown/); + expect(clientDisposeSpy.called).to.be.true; }); it('works if client is explicitly closed', async function () { @@ -73,6 +89,7 @@ describe('explicit resource management feature integration tests', function () { })().catch(e => e); expect(error).to.match(/error thrown/); + expect(cursorDisposeSpy.called).to.be.true; }); it('works if cursor is explicitly closed', async function () { @@ -116,6 +133,7 @@ describe('explicit resource management feature integration tests', function () { })().catch(e => e); expect(error).to.match(/error thrown/); + expect(readableDisposeSpy.called).to.be.true; }); it('works if stream is explicitly closed', async function () { @@ -157,6 +175,7 @@ describe('explicit resource management feature integration tests', function () { })().catch(e => e); expect(error).to.match(/error thrown/); + expect(sessionDisposeSpy.called).to.be.true; }); it('works if session is explicitly closed', async function () { @@ -202,6 +221,7 @@ describe('explicit resource management feature integration tests', function () { })().catch(e => e); expect(error).to.match(/error thrown/); + expect(changeStreamDisposeSpy.called).to.be.true; }); it('works if change stream is explicitly closed', async function () { @@ -250,6 +270,7 @@ describe('explicit resource management feature integration tests', function () { })().catch(e => e); expect(error).to.match(/error thrown/); + expect(readableDisposeSpy.called).to.be.true; }); it('works if stream is explicitly closed', async function () { From f1da72b974c157c9e4c75a6e7ce723a613249557 Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Tue, 6 Aug 2024 14:04:47 -0600 Subject: [PATCH 17/17] remove unnecessary tests --- .../explicit-resource-management/main.test.ts | 235 +----------------- 1 file changed, 12 insertions(+), 223 deletions(-) diff --git a/test/explicit-resource-management/main.test.ts b/test/explicit-resource-management/main.test.ts index fd120e86596..0ba824ad861 100644 --- a/test/explicit-resource-management/main.test.ts +++ b/test/explicit-resource-management/main.test.ts @@ -35,48 +35,17 @@ describe('explicit resource management feature integration tests', function () { }) describe('MongoClient', function () { it('does not crash or error when used with await-using syntax', async function () { - await using client = new MongoClient(process.env.MONGODB_URI!); - await client.connect(); - }) - - it('always cleans up the client, regardless of thrown errors', async function () { - const error = await (async () => { + { await using client = new MongoClient(process.env.MONGODB_URI!); await client.connect(); - - throw new Error('error thrown'); - })().catch(e => e); - - expect(error).to.match(/error thrown/); + } expect(clientDisposeSpy.called).to.be.true; - }); - - it('works if client is explicitly closed', async function () { - const expected = await (async () => { - await using client = new MongoClient(process.env.MONGODB_URI!); - await client.connect(); - await client.close(); - - return 'not error'; - })(); - - expect(expected).to.equal('not error'); }) }) describe('Cursors', function () { it('does not crash or error when used with await-using syntax', async function () { - await using client = new MongoClient(process.env.MONGODB_URI!); - await client.connect(); - - const collection = await setUpCollection(client); - - await using cursor = collection.find(); - await cursor.next(); - }) - - it('always cleans up the cursor, regardless of thrown errors', async function () { - const error = await (async () => { + { await using client = new MongoClient(process.env.MONGODB_URI!); await client.connect(); @@ -84,130 +53,40 @@ describe('explicit resource management feature integration tests', function () { await using cursor = collection.find(); await cursor.next(); - - throw new Error('error thrown'); - })().catch(e => e); - - expect(error).to.match(/error thrown/); + } expect(cursorDisposeSpy.called).to.be.true; - }); - - it('works if cursor is explicitly closed', async function () { - const expected = await (async () => { - await using client = new MongoClient(process.env.MONGODB_URI!); - await client.connect(); - - const collection = await setUpCollection(client); - - await using cursor = collection.find(); - await cursor.next(); - - await cursor.close(); - - return 'not error'; - })(); - - expect(expected).to.equal('not error'); }) describe('cursor streams', function () { it('does not crash or error when used with await-using syntax', async function () { - await using client = new MongoClient(process.env.MONGODB_URI!); - await client.connect(); - - const collection = await setUpCollection(client); - - await using readable = collection.find().stream(); - }) - - it('always cleans up the stream, regardless of thrown errors', async function () { - const error = await (async () => { + { await using client = new MongoClient(process.env.MONGODB_URI!); await client.connect(); const collection = await setUpCollection(client); await using readable = collection.find().stream(); - - throw new Error('error thrown'); - })().catch(e => e); - - expect(error).to.match(/error thrown/); + } expect(readableDisposeSpy.called).to.be.true; - }); - - it('works if stream is explicitly closed', async function () { - const expected = await (async () => { - await using client = new MongoClient(process.env.MONGODB_URI!); - await client.connect(); - - const collection = await setUpCollection(client); - - await using readable = collection.find().stream(); - - readable.destroy(); - - return 'not error'; - })(); - - expect(expected).to.equal('not error'); }) - }) }) describe('Sessions', function () { it('does not crash or error when used with await-using syntax', async function () { - await using client = new MongoClient(process.env.MONGODB_URI!); - await client.connect(); - - await using session = client.startSession(); - }) - - it('always cleans up the session, regardless of thrown errors', async function () { - const error = await (async () => { + { await using client = new MongoClient(process.env.MONGODB_URI!); await client.connect(); await using session = client.startSession(); - - throw new Error('error thrown'); - })().catch(e => e); - - expect(error).to.match(/error thrown/); + } expect(sessionDisposeSpy.called).to.be.true; - }); - - it('works if session is explicitly closed', async function () { - const expected = await (async () => { - await using client = new MongoClient(process.env.MONGODB_URI!); - await client.connect(); - - await using session = client.startSession(); - - await session.endSession(); - - return 'not error'; - })(); - - expect(expected).to.equal('not error'); }) }) describe('ChangeStreams', function () { it('does not crash or error when used with await-using syntax', async function () { - await using client = new MongoClient(process.env.MONGODB_URI!); - await client.connect(); - - const collection = await setUpCollection(client); - await using cs = collection.watch(); - - setTimeout(1000).then(() => collection.insertOne({ name: 'bailey' })); - await cs.next(); - }) - - it('always cleans up the change stream, regardless of thrown errors', async function () { - const error = await (async () => { + { await using client = new MongoClient(process.env.MONGODB_URI!); await client.connect(); @@ -216,47 +95,14 @@ describe('explicit resource management feature integration tests', function () { setTimeout(1000).then(() => collection.insertOne({ name: 'bailey' })); await cs.next(); - - throw new Error('error thrown'); - })().catch(e => e); - - expect(error).to.match(/error thrown/); + } expect(changeStreamDisposeSpy.called).to.be.true; - }); - - it('works if change stream is explicitly closed', async function () { - const expected = await (async () => { - await using client = new MongoClient(process.env.MONGODB_URI!); - await client.connect(); - - const collection = await setUpCollection(client); - await using cs = collection.watch(); - - setTimeout(1000).then(() => collection.insertOne({ name: 'bailey' })); - await cs.next(); - await cs.close(); - - return 'not error'; - })(); - - expect(expected).to.equal('not error'); }) }); describe('GridFSDownloadStream', function () { it('does not crash or error when used with await-using syntax', async function () { - await using client = new MongoClient(process.env.MONGODB_URI!); - await client.connect(); - - const bucket = new GridFSBucket(client.db('foo')); - const uploadStream = bucket.openUploadStream('foo.txt') - await pipeline(Readable.from("AAAAAAA".split('')), uploadStream); - - await using downloadStream = bucket.openDownloadStreamByName('foo.txt'); - }) - - it('always cleans up the stream, regardless of thrown errors', async function () { - const error = await (async () => { + { await using client = new MongoClient(process.env.MONGODB_URI!); await client.connect(); @@ -265,66 +111,9 @@ describe('explicit resource management feature integration tests', function () { await pipeline(Readable.from("AAAAAAA".split('')), uploadStream); await using downloadStream = bucket.openDownloadStreamByName('foo.txt'); + } - throw new Error('error thrown'); - })().catch(e => e); - - expect(error).to.match(/error thrown/); expect(readableDisposeSpy.called).to.be.true; - }); - - it('works if stream is explicitly closed', async function () { - const expected = await (async () => { - await using client = new MongoClient(process.env.MONGODB_URI!); - await client.connect(); - - const bucket = new GridFSBucket(client.db('foo')); - const uploadStream = bucket.openUploadStream('foo.txt') - await pipeline(Readable.from("AAAAAAA".split('')), uploadStream); - - await using downloadStream = bucket.openDownloadStreamByName('foo.txt'); - - await downloadStream.abort(); - - return 'not error'; - })(); - - expect(expected).to.equal('not error'); - }) - - it('throws premature close error if explicitly destroyed early', async function () { - // Gridfs streams inherit their _destroy() and Symbol.asyncDispose implementations from - // Nodejs' readable implementation. This behavior matches the behavior for other readable streams - // (see the below test). - const expected = await (async () => { - await using client = new MongoClient(process.env.MONGODB_URI!); - await client.connect(); - - const bucket = new GridFSBucket(client.db('foo')); - const uploadStream = bucket.openUploadStream('foo.txt') - await pipeline(Readable.from("AAAAAAA".split('')), uploadStream); - - await using downloadStream = bucket.openDownloadStreamByName('foo.txt'); - - downloadStream.destroy(); - - return 'not error'; - })().catch(e => e); - - expect(expected).to.match(/Premature close/); - }) - - it('throws premature close error if explicitly destroyed early (builtin stream)', async function () { - // Gridfs streams inherit their _destroy() and Symbol.asyncDispose implementations from - // Nodejs' readable implementation. This behavior matches the behavior for other readable streams (ie - ReadFileStream) - const expected = await (async () => { - await using readStream = createReadStream(join(__dirname, 'main.test.ts')); - readStream.destroy(); - - return 'not error'; - })().catch(e => e); - - expect(expected).to.match(/Premature close/); }) }); })