diff --git a/etc/notes/CHANGES_5.0.0.md b/etc/notes/CHANGES_5.0.0.md index 1b509a68e5e..b0a465e8bad 100644 --- a/etc/notes/CHANGES_5.0.0.md +++ b/etc/notes/CHANGES_5.0.0.md @@ -28,3 +28,8 @@ npm install --save snappy@7 ### Minimum supported Node version The new minimum supported Node.js version is now 14.20.1. + +### Custom Promise library support removed + +The MongoClient option `promiseLibrary` along with the `Promise.set` export that allows specifying a custom promise library has been removed. +This allows the driver to adopt async/await syntax which has [performance benefits](https://v8.dev/blog/fast-async) over manual promise construction. diff --git a/package-lock.json b/package-lock.json index 606ff375f8d..e4842fc5c62 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,7 +32,6 @@ "@types/whatwg-url": "^11.0.0", "@typescript-eslint/eslint-plugin": "^5.40.0", "@typescript-eslint/parser": "^5.40.0", - "bluebird": "^3.7.2", "chai": "^4.3.6", "chai-subset": "^1.6.0", "chalk": "^4.1.2", @@ -2907,12 +2906,6 @@ "node": ">=8" } }, - "node_modules/bluebird": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", - "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", - "dev": true - }, "node_modules/body-parser": { "version": "1.20.1", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", @@ -11626,12 +11619,6 @@ "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", "dev": true }, - "bluebird": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", - "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", - "dev": true - }, "body-parser": { "version": "1.20.1", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", diff --git a/package.json b/package.json index 496968de071..3805fea991d 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,6 @@ "@types/whatwg-url": "^11.0.0", "@typescript-eslint/eslint-plugin": "^5.40.0", "@typescript-eslint/parser": "^5.40.0", - "bluebird": "^3.7.2", "chai": "^4.3.6", "chai-subset": "^1.6.0", "chalk": "^4.1.2", diff --git a/src/connection_string.ts b/src/connection_string.ts index a1a177433d5..807457ee61b 100644 --- a/src/connection_string.ts +++ b/src/connection_string.ts @@ -25,7 +25,6 @@ import { ServerApiVersion } from './mongo_client'; import { MongoLogger, MongoLoggerEnvOptions, MongoLoggerMongoClientOptions } from './mongo_logger'; -import { PromiseProvider } from './promise_provider'; import { ReadConcern, ReadConcernLevel } from './read_concern'; import { ReadPreference, ReadPreferenceMode } from './read_preference'; import type { TagSet } from './sdam/server_description'; @@ -431,10 +430,6 @@ export function parseOptions( mongoOptions.dbName = 'test'; } - if (options.promiseLibrary) { - PromiseProvider.set(options.promiseLibrary); - } - validateLoadBalancedOptions(hosts, mongoOptions, isSRV); if (mongoClient && mongoOptions.autoEncryption) { @@ -960,10 +955,6 @@ export const OPTIONS = { ); } }, - promiseLibrary: { - deprecated: true, - type: 'any' - }, promoteBuffers: { type: 'boolean' }, diff --git a/src/cursor/abstract_cursor.ts b/src/cursor/abstract_cursor.ts index 8aa59aa651d..95b8bdaef7c 100644 --- a/src/cursor/abstract_cursor.ts +++ b/src/cursor/abstract_cursor.ts @@ -17,7 +17,6 @@ import { TODO_NODE_3286, TypedEventEmitter } from '../mongo_types'; import { executeOperation, ExecutionResult } from '../operations/execute_operation'; import { GetMoreOperation } from '../operations/get_more'; import { KillCursorsOperation } from '../operations/kill_cursors'; -import { PromiseProvider } from '../promise_provider'; import { ReadConcern, ReadConcernLike } from '../read_concern'; import { ReadPreference, ReadPreferenceLike } from '../read_preference'; import type { Server } from '../sdam/server'; @@ -297,47 +296,36 @@ export abstract class AbstractCursor< return bufferedDocs; } - [Symbol.asyncIterator](): AsyncIterator { - async function* nativeAsyncIterator(this: AbstractCursor) { - if (this.closed) { - return; - } + async *[Symbol.asyncIterator](): AsyncIterator { + if (this.closed) { + return; + } - while (true) { - const document = await this.next(); + while (true) { + const document = await this.next(); - // Intentional strict null check, because users can map cursors to falsey values. - // We allow mapping to all values except for null. - // eslint-disable-next-line no-restricted-syntax - if (document === null) { - if (!this.closed) { - const message = - 'Cursor returned a `null` document, but the cursor is not exhausted. Mapping documents to `null` is not supported in the cursor transform.'; + // Intentional strict null check, because users can map cursors to falsey values. + // We allow mapping to all values except for null. + // eslint-disable-next-line no-restricted-syntax + if (document === null) { + if (!this.closed) { + const message = + 'Cursor returned a `null` document, but the cursor is not exhausted. Mapping documents to `null` is not supported in the cursor transform.'; - await cleanupCursorAsync(this, { needsToEmitClosed: true }).catch(() => null); + await cleanupCursorAsync(this, { needsToEmitClosed: true }).catch(() => null); - throw new MongoAPIError(message); - } - break; - } - - yield document; - - if (this[kId] === Long.ZERO) { - // Cursor exhausted - break; + throw new MongoAPIError(message); } + break; } - } - const iterator = nativeAsyncIterator.call(this); + yield document; - if (PromiseProvider.get() == null) { - return iterator; + if (this[kId] === Long.ZERO) { + // Cursor exhausted + break; + } } - return { - next: () => maybeCallback(() => iterator.next(), null) - }; } stream(options?: CursorStreamOptions): Readable & AsyncIterable { diff --git a/src/index.ts b/src/index.ts index 68fdd89b422..75d9b9c7dec 100644 --- a/src/index.ts +++ b/src/index.ts @@ -16,7 +16,6 @@ import { GridFSBucketWriteStream } from './gridfs/upload'; import { Logger } from './logger'; import { MongoClient } from './mongo_client'; import { CancellationToken } from './mongo_types'; -import { PromiseProvider } from './promise_provider'; import { ClientSession } from './sessions'; /** @internal */ @@ -100,9 +99,6 @@ export { UnorderedBulkOperation }; -// Deprecated, remove in next major -export { PromiseProvider as Promise }; - // enums export { BatchType } from './bulk/common'; export { GSSAPICanonicalizationValue } from './cmap/auth/gssapi'; diff --git a/src/mongo_client.ts b/src/mongo_client.ts index 4d94d87747e..63b46cf9ecb 100644 --- a/src/mongo_client.ts +++ b/src/mongo_client.ts @@ -228,11 +228,6 @@ export interface MongoClientOptions extends BSONSerializeOptions, SupportedNodeC forceServerObjectId?: boolean; /** A primary key factory function for generation of custom `_id` keys */ pkFactory?: PkFactory; - /** - * A Promise library class the application wishes to use such as Bluebird, must be ES6 compatible - * @deprecated Setting a custom promise library is deprecated the next major version will use the global Promise constructor only. - */ - promiseLibrary?: any; /** The logging level */ loggerLevel?: LegacyLoggerLevel; /** Custom logger object */ @@ -743,7 +738,6 @@ export interface MongoOptions | 'monitorCommands' | 'noDelay' | 'pkFactory' - | 'promiseLibrary' | 'raw' | 'replicaSet' | 'retryReads' diff --git a/src/promise_provider.ts b/src/promise_provider.ts deleted file mode 100644 index eb9a28c756e..00000000000 --- a/src/promise_provider.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { MongoInvalidArgumentError } from './error'; - -/** @internal */ -const kPromise = Symbol('promise'); - -interface PromiseStore { - [kPromise]: PromiseConstructor | null; -} - -const store: PromiseStore = { - [kPromise]: null -}; - -/** - * Global promise store allowing user-provided promises - * @deprecated Setting a custom promise library is deprecated the next major version will use the global Promise constructor only. - * @public - */ -export class PromiseProvider { - /** - * Validates the passed in promise library - * @deprecated Setting a custom promise library is deprecated the next major version will use the global Promise constructor only. - */ - static validate(lib: unknown): lib is PromiseConstructor { - if (typeof lib !== 'function') - throw new MongoInvalidArgumentError(`Promise must be a function, got ${lib}`); - return !!lib; - } - - /** - * Sets the promise library - * @deprecated Setting a custom promise library is deprecated the next major version will use the global Promise constructor only. - */ - static set(lib: PromiseConstructor | null): void { - // eslint-disable-next-line no-restricted-syntax - if (lib === null) { - // Check explicitly against null since `.set()` (no args) should fall through to validate - store[kPromise] = null; - return; - } - - if (!PromiseProvider.validate(lib)) { - // validate - return; - } - store[kPromise] = lib; - } - - /** - * Get the stored promise library, or resolves passed in - * @deprecated Setting a custom promise library is deprecated the next major version will use the global Promise constructor only. - */ - static get(): PromiseConstructor | null { - return store[kPromise]; - } -} diff --git a/src/sessions.ts b/src/sessions.ts index 803a174dec5..1774f7c7806 100644 --- a/src/sessions.ts +++ b/src/sessions.ts @@ -25,7 +25,6 @@ import type { MongoClient, MongoOptions } from './mongo_client'; import { TypedEventEmitter } from './mongo_types'; import { executeOperation } from './operations/execute_operation'; import { RunAdminCommandOperation } from './operations/run_command'; -import { PromiseProvider } from './promise_provider'; import { ReadConcernLevel } from './read_concern'; import { ReadPreference } from './read_preference'; import { _advanceClusterTime, ClusterTime, TopologyType } from './sdam/common'; @@ -605,8 +604,7 @@ function attemptTransaction( try { promise = fn(session); } catch (err) { - const PromiseConstructor = PromiseProvider.get() ?? Promise; - promise = PromiseConstructor.reject(err); + promise = Promise.reject(err); } if (!isPromiseLike(promise)) { diff --git a/src/utils.ts b/src/utils.ts index f0784214204..f6027c8e58b 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -23,7 +23,6 @@ import type { Explain } from './explain'; import type { MongoClient } from './mongo_client'; import type { CommandOperationOptions, OperationParent } from './operations/command'; import type { Hint, OperationOptions } from './operations/operation'; -import { PromiseProvider } from './promise_provider'; import { ReadConcern } from './read_concern'; import { ReadPreference } from './read_preference'; import { ServerType } from './sdam/common'; @@ -445,17 +444,9 @@ export function maybeCallback( promiseFn: () => Promise, callback?: Callback | null ): Promise | void { - const PromiseConstructor = PromiseProvider.get(); - const promise = promiseFn(); if (callback == null) { - if (PromiseConstructor == null) { - return promise; - } else { - return new PromiseConstructor((resolve, reject) => { - promise.then(resolve, reject); - }); - } + return promise; } promise.then( diff --git a/test/integration/collection-management/promise_collection_db_management.test.ts b/test/integration/collection-management/promise_collection_db_management.test.ts index b7f789ed22a..8277ccdec90 100644 --- a/test/integration/collection-management/promise_collection_db_management.test.ts +++ b/test/integration/collection-management/promise_collection_db_management.test.ts @@ -1,9 +1,5 @@ import { assert as test, setupDatabase } from '../shared'; -class CustomPromise extends Promise {} -// @ts-expect-error: Dynamic addition to detect custom promise -CustomPromise.prototype.isCustomMongo = true; - describe('Collection Management and Db Management (promise tests)', function () { before(function () { return setupDatabase(this.configuration); diff --git a/test/integration/crud/promise_stats.test.js b/test/integration/crud/promise_stats.test.js index 1de801a2ac7..c41352a0253 100644 --- a/test/integration/crud/promise_stats.test.js +++ b/test/integration/crud/promise_stats.test.js @@ -3,9 +3,6 @@ const { assert: test, setupDatabase } = require('../shared'); const f = require('util').format; -class CustomPromise extends Promise {} -CustomPromise.prototype.isCustomMongo = true; - describe('stats', function () { before(function () { return setupDatabase(this.configuration); diff --git a/test/integration/mongodb-handshake/promise_handshake.test.js b/test/integration/mongodb-handshake/promise_handshake.test.js index 1824a8ab77f..ac4abfafbce 100644 --- a/test/integration/mongodb-handshake/promise_handshake.test.js +++ b/test/integration/mongodb-handshake/promise_handshake.test.js @@ -3,9 +3,6 @@ const { assert: test, setupDatabase } = require('../shared'); const f = require('util').format; const { LEGACY_HELLO_COMMAND } = require('../../../src/constants'); -class CustomPromise extends Promise {} -CustomPromise.prototype.isCustomMongo = true; - describe('Handshake', function () { before(function () { return setupDatabase(this.configuration); diff --git a/test/integration/node-specific/cursor_async_iterator.test.js b/test/integration/node-specific/cursor_async_iterator.test.js index 944362bf346..4635c414541 100644 --- a/test/integration/node-specific/cursor_async_iterator.test.js +++ b/test/integration/node-specific/cursor_async_iterator.test.js @@ -1,9 +1,6 @@ 'use strict'; const { expect } = require('chai'); -const Sinon = require('sinon'); -const { Promise: BluebirdPromise } = require('bluebird'); -const { PromiseProvider } = require('../../../src/promise_provider'); describe('Cursor Async Iterator Tests', function () { context('default promise library', function () { @@ -83,58 +80,4 @@ describe('Cursor Async Iterator Tests', function () { expect(count).to.equal(1); }); }); - context('custom promise library', () => { - let client, collection, promiseSpy; - beforeEach(async function () { - promiseSpy = Sinon.spy(BluebirdPromise.prototype, 'then'); - client = this.configuration.newClient({}, { promiseLibrary: BluebirdPromise }); - - const connectPromise = client.connect(); - expect(connectPromise).to.be.instanceOf(BluebirdPromise); - await connectPromise; - const docs = Array.from({ length: 1 }).map((_, index) => ({ foo: index, bar: 1 })); - - collection = client.db(this.configuration.db).collection('async_cursor_tests'); - - await collection.deleteMany({}); - await collection.insertMany(docs); - await client.close(); - }); - - beforeEach(async function () { - client = this.configuration.newClient(); - await client.connect(); - collection = client.db(this.configuration.db).collection('async_cursor_tests'); - }); - - afterEach(() => { - promiseSpy.restore(); - PromiseProvider.set(null); - return client.close(); - }); - - it('should properly use custom promise', async function () { - const cursor = collection.find(); - const countBeforeIteration = promiseSpy.callCount; - for await (const doc of cursor) { - expect(doc).to.exist; - } - expect(countBeforeIteration).to.not.equal(promiseSpy.callCount); - expect(promiseSpy.called).to.equal(true); - }); - - it('should properly use custom promise manual iteration', async function () { - const cursor = collection.find(); - - const iterator = cursor[Symbol.asyncIterator](); - let isDone; - do { - const promiseFromIterator = iterator.next(); - expect(promiseFromIterator).to.be.instanceOf(BluebirdPromise); - const { done, value } = await promiseFromIterator; - if (done) expect(value).to.be.a('undefined'); - isDone = done; - } while (!isDone); - }); - }); }); diff --git a/test/integration/node-specific/custom_promise_library.test.js b/test/integration/node-specific/custom_promise_library.test.js deleted file mode 100644 index 3a93bb28118..00000000000 --- a/test/integration/node-specific/custom_promise_library.test.js +++ /dev/null @@ -1,80 +0,0 @@ -'use strict'; - -const { once } = require('events'); -const { expect } = require('chai'); -const { PromiseProvider } = require('../../../src/promise_provider'); -const { MongoClient } = require('../../../src/mongo_client'); - -class CustomPromise extends Promise {} -CustomPromise.prototype.isCustomMongo = true; - -describe('Optional PromiseLibrary', function () { - beforeEach(() => { - PromiseProvider.set(null); - }); - - afterEach(() => { - PromiseProvider.set(null); - }); - - it('should initially be set to null', () => { - expect(PromiseProvider.get()).to.be.null; - }); - - it('should allow passing null to .set() to clear the set promise', () => { - PromiseProvider.set(Promise); - expect(PromiseProvider.get()).to.equal(Promise); - expect(() => PromiseProvider.set(null)).to.not.throw(); - expect(PromiseProvider.get()).to.be.null; - }); - - it('should emit a deprecation warning when a promiseLibrary is set', async () => { - const willEmitWarning = once(process, 'warning'); - new MongoClient('mongodb://iLoveJavascript', { promiseLibrary: () => {} }); - const [warning] = await willEmitWarning; - expect(warning).to.have.property('message', 'promiseLibrary is a deprecated option'); - }); - - it('should correctly implement custom dependency-less promise', function (done) { - const getCustomPromise = v => new CustomPromise(resolve => resolve(v)); - const getNativePromise = v => new Promise(resolve => resolve(v)); - expect(getNativePromise()).to.not.have.property('isCustomMongo'); - expect(getCustomPromise()).to.have.property('isCustomMongo'); - expect(getNativePromise()).to.have.property('then'); - expect(getCustomPromise()).to.have.property('then'); - done(); - }); - - it('should have cursor return native promise', function (done) { - const configuration = this.configuration; - const client = this.configuration.newClient({ w: 1 }, { maxPoolSize: 1 }); - client.connect((err, client) => { - expect(err).to.not.exist; - const db = client.db(configuration.db); - const collection = db.collection('test'); - const cursor = collection.find({}); - const isPromise = cursor.toArray(); - expect(isPromise).to.not.have.property('isCustomMongo'); - expect(isPromise).to.have.property('then'); - isPromise.then(() => client.close(done)); - }); - }); - - it('should have cursor return custom promise from new client options', function (done) { - const configuration = this.configuration; - const client = this.configuration.newClient( - { w: 1 }, - { maxPoolSize: 1, promiseLibrary: CustomPromise } - ); - client.connect((err, client) => { - const db = client.db(configuration.db); - expect(err).to.not.exist; - const collection = db.collection('test'); - const cursor = collection.find({}); - const isPromise = cursor.toArray(); - expect(isPromise).to.have.property('isCustomMongo'); - expect(isPromise).to.have.property('then'); - isPromise.then(() => client.close(done)); - }); - }); -}); diff --git a/test/integration/server_write_command.js b/test/integration/server_write_command.js deleted file mode 100644 index 7355c24e6c7..00000000000 --- a/test/integration/server_write_command.js +++ /dev/null @@ -1,33 +0,0 @@ -'use strict'; -const { setupDatabase } = require('./shared'); - -class CustomPromise extends Promise {} -CustomPromise.prototype.isCustomMongo = true; - -describe('Server Write Command', function () { - before(function () { - return setupDatabase(this.configuration); - }); - - it('Should correctly catch command error using Promise', { - metadata: { - requires: { - topology: ['single'] - } - }, - - test: function (done) { - const client = this.configuration.newClient({ maxPoolSize: 5 }); - client - .db(this.configuration.db) - .command({ nosuchcommand: true }) - .then(function () {}) - .catch(function () { - // Execute close using promise - client.close().then(function () { - done(); - }); - }); - } - }); -}); diff --git a/test/types/mongodb.test-d.ts b/test/types/mongodb.test-d.ts index fe75b1e223b..eb9164659e4 100644 --- a/test/types/mongodb.test-d.ts +++ b/test/types/mongodb.test-d.ts @@ -22,20 +22,10 @@ expectDeprecated(Db.prototype.unref); expectDeprecated(MongoDBDriver.ObjectID); expectNotDeprecated(MongoDBDriver.ObjectId); -// We cannot attach a deprecation tag to an export -// We tried export const Promise = PromiseProvider; -// but then api-extractor claims PromiseProvider is not exported -// Instead we've deprecated all the methods on the class -expectNotDeprecated(MongoDBDriver.Promise); -expectDeprecated(MongoDBDriver.Promise.validate); -expectDeprecated(MongoDBDriver.Promise.get); -expectDeprecated(MongoDBDriver.Promise.set); - declare const options: MongoDBDriver.MongoClientOptions; expectDeprecated(options.w); expectDeprecated(options.journal); expectDeprecated(options.wtimeoutMS); -expectDeprecated(options.promiseLibrary); expectNotDeprecated(options.writeConcern); expectType(options.writeConcern); diff --git a/test/unit/index.test.ts b/test/unit/index.test.ts index 2ebd2293c94..115a2363293 100644 --- a/test/unit/index.test.ts +++ b/test/unit/index.test.ts @@ -102,7 +102,6 @@ const EXPECTED_EXPORTS = [ 'ObjectID', 'OrderedBulkOperation', 'ProfilingLevel', - 'Promise', 'ReadConcern', 'ReadConcernLevel', 'ReadPreference', diff --git a/test/unit/utils.test.ts b/test/unit/utils.test.ts index fccfa524732..205f8c7bbae 100644 --- a/test/unit/utils.test.ts +++ b/test/unit/utils.test.ts @@ -1,9 +1,8 @@ -import { Promise as BluebirdPromise } from 'bluebird'; import { expect } from 'chai'; import { LEGACY_HELLO_COMMAND } from '../../src/constants'; import { MongoRuntimeError } from '../../src/error'; -import { ObjectId, Promise as PromiseProvider } from '../../src/index'; +import { ObjectId } from '../../src/index'; import { BufferPool, compareObjectId, @@ -775,44 +774,6 @@ describe('driver utils', function () { expect(await result).to.equal(2); }); }); - - describe('when a custom promise constructor is set', () => { - beforeEach(() => { - PromiseProvider.set(BluebirdPromise); - }); - - afterEach(() => { - PromiseProvider.set(null); - }); - - it('should return the custom promise if no callback is provided', async () => { - const superPromiseSuccess = Promise.resolve(2); - const result = maybeCallback(() => superPromiseSuccess); - expect(result).to.not.equal(superPromiseSuccess); - expect(result).to.be.instanceOf(BluebirdPromise); - }); - - it('should return a rejected custom promise instance if promiseFn rejects', async () => { - const superPromiseFailure = Promise.reject(new Error('ah!')); - const result = maybeCallback(() => superPromiseFailure); - expect(result).to.not.equal(superPromiseFailure); - expect(result).to.be.instanceOf(BluebirdPromise); - // @ts-expect-error: There is no overload to change the return type not be nullish, - // and we do not want to add one in fear of making it too easy to neglect adding the callback argument - expect(await result.catch(e => e)).to.have.property('message', 'ah!'); - }); - - it('should return void even if a custom promise is set and a callback is provided', async () => { - const superPromiseSuccess = Promise.resolve(2); - const result = maybeCallback( - () => superPromiseSuccess, - () => { - // ignore - } - ); - expect(result).to.be.undefined; - }); - }); }); describe('compareObjectId()', () => {