diff --git a/.eslintrc b/.eslintrc index 433bc1c78c..f353d18213 100644 --- a/.eslintrc +++ b/.eslintrc @@ -8,7 +8,7 @@ "parserOptions": { "ecmaVersion": 2018 }, - "plugins": ["prettier", "jsdoc"], + "plugins": ["prettier", "promise", "jsdoc"], "rules": { "prettier/prettier": "error", @@ -30,8 +30,17 @@ "no-console": "off", "eqeqeq": ["error", "always", { "null": "ignore" }], - "strict": ["error", "global"] + "strict": ["error", "global"], + "promise/no-native": "error" }, + "overrides": [ + { + "files": ["test/**/*.js"], + "rules": { + "promise/no-native": "off" + } + } + ], "settings": { "jsdoc": { "check-types": false, diff --git a/index.js b/index.js index e262f0bb51..ab56041dfa 100644 --- a/index.js +++ b/index.js @@ -1,12 +1,23 @@ 'use strict'; + const error = require('./lib/error'); const Instrumentation = require('./lib/apm'); const { BSON } = require('./lib/deps'); const { Cursor, AggregationCursor, CommandCursor } = require('./lib/cursor'); +const PromiseProvider = require('./lib/promise_provider'); // Set up the connect function const connect = require('./lib/mongo_client').connect; +Object.defineProperty(connect, 'Promise', { + get: function() { + return PromiseProvider.get(); + }, + set: function(lib) { + PromiseProvider.set(lib); + } +}); + // Expose error class connect.MongoError = error.MongoError; connect.MongoNetworkError = error.MongoNetworkError; diff --git a/lib/admin.js b/lib/admin.js index bfbaf8d8fe..967feaeaf1 100644 --- a/lib/admin.js +++ b/lib/admin.js @@ -44,16 +44,14 @@ const executeOperation = require('./operations/execute_operation'); * @returns {Admin} a collection instance. * @param {any} db * @param {any} topology - * @param {any} promiseLibrary */ -function Admin(db, topology, promiseLibrary) { +function Admin(db, topology) { if (!(this instanceof Admin)) return new Admin(db, topology); // Internal state this.s = { db: db, - topology: topology, - promiseLibrary: promiseLibrary + topology: topology }; } diff --git a/lib/bulk/common.js b/lib/bulk/common.js index f19ddc93f5..9f156bab33 100644 --- a/lib/bulk/common.js +++ b/lib/bulk/common.js @@ -1,5 +1,6 @@ 'use strict'; +const PromiseProvider = require('../promise_provider'); const { BSON: { Long, ObjectId } } = require('../deps'); @@ -781,9 +782,6 @@ class BulkOperationBase { finalOptions = applyWriteConcern(finalOptions, { collection: collection }, options); const writeConcern = finalOptions.writeConcern; - // Get the promiseLibrary - const promiseLibrary = options.promiseLibrary || Promise; - // Final results const bulkResult = { ok: 1, @@ -832,8 +830,6 @@ class BulkOperationBase { executed: executed, // Collection collection: collection, - // Promise Library - promiseLibrary: promiseLibrary, // Fundamental error err: null, // check keys @@ -1027,12 +1023,14 @@ class BulkOperationBase { * @param {any} callback */ _handleEarlyError(err, callback) { + const Promise = PromiseProvider.get(); + if (typeof callback === 'function') { callback(err, null); return; } - return this.s.promiseLibrary.reject(err); + return Promise.reject(err); } /** diff --git a/lib/change_stream.js b/lib/change_stream.js index 29679fa5ae..454c9221bc 100644 --- a/lib/change_stream.js +++ b/lib/change_stream.js @@ -85,7 +85,6 @@ class ChangeStream extends EventEmitter { ); } - this.promiseLibrary = parent.s.promiseLibrary; if (!this.options.readPreference && parent.s.readPreference) { this.options.readPreference = parent.s.readPreference; } @@ -130,7 +129,7 @@ class ChangeStream extends EventEmitter { * @returns {Promise|void} returns Promise if no callback passed */ hasNext(callback) { - return maybePromise(this.parent, callback, cb => this.cursor.hasNext(cb)); + return maybePromise(callback, cb => this.cursor.hasNext(cb)); } /** @@ -142,7 +141,7 @@ class ChangeStream extends EventEmitter { * @returns {Promise|void} returns Promise if no callback passed */ next(callback) { - return maybePromise(this.parent, callback, cb => { + return maybePromise(callback, cb => { if (this.isClosed()) { return cb(new MongoError('ChangeStream is closed')); } @@ -170,7 +169,7 @@ class ChangeStream extends EventEmitter { * @returns {Promise} returns Promise if no callback passed */ close(callback) { - return maybePromise(this.parent, callback, cb => { + return maybePromise(callback, cb => { if (this.closed) return cb(); // flag the change stream as explicitly closed diff --git a/lib/collection.js b/lib/collection.js index b6d972fce5..e469cb892e 100644 --- a/lib/collection.js +++ b/lib/collection.js @@ -1,5 +1,7 @@ 'use strict'; +const { emitDeprecatedOptionWarning } = require('./utils'); +const PromiseProvider = require('./promise_provider'); const ReadPreference = require('./read_preference'); const { deprecate } = require('util'); const { @@ -111,6 +113,7 @@ const mergeKeys = ['ignoreUndefined']; */ function Collection(db, topology, dbName, name, pkFactory, options) { checkCollectionName(name); + emitDeprecatedOptionWarning(options, ['promiseLibrary']); // Unpack variables const internalHint = null; @@ -136,9 +139,6 @@ function Collection(db, topology, dbName, name, pkFactory, options) { const namespace = new MongoDBNamespace(dbName, name); - // Get the promiseLibrary - const promiseLibrary = options.promiseLibrary || Promise; - // Set custom primary key factory if provided pkFactory = pkFactory == null ? ObjectId : pkFactory; @@ -172,8 +172,6 @@ function Collection(db, topology, dbName, name, pkFactory, options) { internalHint: internalHint, // collectionHint collectionHint: collectionHint, - // Promise library - promiseLibrary: promiseLibrary, // Read Concern readConcern: ReadConcern.fromOptions(options), // Write Concern @@ -453,9 +451,6 @@ Collection.prototype.find = deprecateOptions( // Add db object to the new options newOptions.db = this.s.db; - // Add the promise library - newOptions.promiseLibrary = this.s.promiseLibrary; - // Set raw if available at collection level if (newOptions.raw == null && typeof this.s.raw === 'boolean') newOptions.raw = this.s.raw; // Set promoteLongs if available at collection level @@ -757,11 +752,12 @@ Collection.prototype.insert = deprecate(function(docs, options, callback) { Collection.prototype.updateOne = function(filter, update, options, callback) { if (typeof options === 'function') (callback = options), (options = {}); options = options || {}; + const Promise = PromiseProvider.get(); const err = checkForAtomicOperators(update); if (err) { if (typeof callback === 'function') return callback(err); - return this.s.promiseLibrary.reject(err); + return Promise.reject(err); } options = Object.assign({}, options); @@ -836,13 +832,14 @@ Collection.prototype.replaceOne = function(filter, doc, options, callback) { * @returns {Promise} returns Promise if no callback passed */ Collection.prototype.updateMany = function(filter, update, options, callback) { + const Promise = PromiseProvider.get(); if (typeof options === 'function') (callback = options), (options = {}); options = options || {}; const err = checkForAtomicOperators(update); if (err) { if (typeof callback === 'function') return callback(err); - return this.s.promiseLibrary.reject(err); + return Promise.reject(err); } options = Object.assign({}, options); @@ -1786,6 +1783,8 @@ Collection.prototype.findOneAndReplace = function(filter, replacement, options, * @returns {Promise} returns Promise if no callback passed */ Collection.prototype.findOneAndUpdate = function(filter, update, options, callback) { + const Promise = PromiseProvider.get(); + if (typeof options === 'function') (callback = options), (options = {}); options = options || {}; @@ -1798,7 +1797,7 @@ Collection.prototype.findOneAndUpdate = function(filter, update, options, callba const err = checkForAtomicOperators(update); if (err) { if (typeof callback === 'function') return callback(err); - return this.s.promiseLibrary.reject(err); + return Promise.reject(err); } const findOneAndUpdateOperation = new FindOneAndUpdateOperation(this, filter, update, options); @@ -2018,9 +2017,6 @@ Collection.prototype.parallelCollectionScan = deprecate(function(options, callba // Ensure we have the right read preference inheritance options.readPreference = resolveReadPreference(this, options); - // Add a promiseLibrary - options.promiseLibrary = this.s.promiseLibrary; - if (options.session) { options.session = undefined; } @@ -2174,7 +2170,6 @@ Collection.prototype.initializeUnorderedBulkOp = function(options) { options.ignoreUndefined = this.s.options.ignoreUndefined; } - options.promiseLibrary = this.s.promiseLibrary; return unordered(this.s.topology, this, options); }; @@ -2196,7 +2191,7 @@ Collection.prototype.initializeOrderedBulkOp = function(options) { if (options.ignoreUndefined == null) { options.ignoreUndefined = this.s.options.ignoreUndefined; } - options.promiseLibrary = this.s.promiseLibrary; + return ordered(this.s.topology, this, options); }; diff --git a/lib/cursor/cursor.js b/lib/cursor/cursor.js index 795d51f718..5633add051 100644 --- a/lib/cursor/cursor.js +++ b/lib/cursor/cursor.js @@ -1,5 +1,7 @@ 'use strict'; +const { emitDeprecatedOptionWarning } = require('../utils'); +const PromiseProvider = require('../promise_provider'); const ReadPreference = require('../read_preference'); const { Transform, PassThrough } = require('stream'); const { deprecate } = require('util'); @@ -101,14 +103,13 @@ class Cursor extends CoreCursor { options = this.operation.options; } + emitDeprecatedOptionWarning(options, ['promiseLibrary']); + // Tailable cursor options const numberOfRetries = options.numberOfRetries || 5; const tailableRetryInterval = options.tailableRetryInterval || 500; const currentNumberOfRetries = numberOfRetries; - // Get the promiseLibrary - const promiseLibrary = options.promiseLibrary || Promise; - // Internal cursor state this.s = { // Tailable cursor options @@ -117,8 +118,6 @@ class Cursor extends CoreCursor { currentNumberOfRetries: currentNumberOfRetries, // State state: CursorState.INIT, - // Promise library - promiseLibrary, // explicitlyIgnoreSession explicitlyIgnoreSession: !!options.explicitlyIgnoreSession }; @@ -193,7 +192,7 @@ class Cursor extends CoreCursor { throw MongoError.create({ message: 'Cursor is closed', driver: true }); } - return maybePromise(this, callback, cb => { + return maybePromise(callback, cb => { const cursor = this; if (cursor.isNotified()) { return cb(null, false); @@ -221,7 +220,7 @@ class Cursor extends CoreCursor { * @returns {Promise} returns Promise if no callback passed */ next(callback) { - return maybePromise(this, callback, cb => { + return maybePromise(callback, cb => { const cursor = this; if (cursor.s.state === CursorState.CLOSED || (cursor.isDead && cursor.isDead())) { cb(MongoError.create({ message: 'Cursor is closed', driver: true })); @@ -746,6 +745,7 @@ class Cursor extends CoreCursor { * @returns {Promise} if no callback supplied */ forEach(iterator, callback) { + const Promise = PromiseProvider.get(); // Rewind cursor state this.rewind(); @@ -770,7 +770,7 @@ class Cursor extends CoreCursor { } }); } else { - return new this.s.promiseLibrary((fulfill, reject) => { + return new Promise((fulfill, reject) => { each(this, (err, doc) => { if (err) { reject(err); @@ -840,7 +840,7 @@ class Cursor extends CoreCursor { driver: true }); } - return maybePromise(this, callback, cb => { + return maybePromise(callback, cb => { const cursor = this; const items = []; @@ -934,6 +934,8 @@ class Cursor extends CoreCursor { * @returns {Promise} returns Promise if no callback passed */ close(options, callback) { + const Promise = PromiseProvider.get(); + if (typeof options === 'function') (callback = options), (options = {}); options = Object.assign({}, { skipKillCursors: false }, options); @@ -953,7 +955,7 @@ class Cursor extends CoreCursor { } // Return a Promise - return new this.s.promiseLibrary(resolve => { + return new Promise(resolve => { resolve(); }); }; @@ -963,7 +965,7 @@ class Cursor extends CoreCursor { return this._endSession(() => completeClose()); } - return new this.s.promiseLibrary(resolve => { + return new Promise(resolve => { this._endSession(() => completeClose().then(resolve)); }); } @@ -1069,7 +1071,7 @@ class Cursor extends CoreCursor { if (this.cmd.readConcern) { delete this.cmd['readConcern']; } - return maybePromise(this, callback, cb => { + return maybePromise(callback, cb => { CoreCursor.prototype._next.apply(this, [cb]); }); } diff --git a/lib/db.js b/lib/db.js index 33162a8fbc..56af65997a 100644 --- a/lib/db.js +++ b/lib/db.js @@ -1,6 +1,7 @@ 'use strict'; const { deprecate } = require('util'); +const { emitDeprecatedOptionWarning } = require('./utils'); const { AggregationCursor, CommandCursor } = require('./cursor'); const { BSON: { ObjectId } @@ -84,7 +85,6 @@ const legalOptionNames = [ 'authSource', 'ignoreUndefined', 'promoteLongs', - 'promiseLibrary', 'readConcern', 'retryMiliSeconds', 'numberOfRetries', @@ -119,7 +119,7 @@ const legalOptionNames = [ * @param {number} [options.bufferMaxEntries=-1] Sets a cap on how many operations the driver will buffer up before giving up on getting a working connection, default is -1 which is unlimited. * @param {(ReadPreference|string)} [options.readPreference] The preferred read preference (ReadPreference.PRIMARY, ReadPreference.PRIMARY_PREFERRED, ReadPreference.SECONDARY, ReadPreference.SECONDARY_PREFERRED, ReadPreference.NEAREST). * @param {object} [options.pkFactory] A primary key factory object for generation of custom _id keys. - * @param {object} [options.promiseLibrary] A Promise library class the application wishes to use such as Bluebird, must be ES6 compatible + * @param {object} [options.promiseLibrary] DEPRECATED: A Promise library class the application wishes to use such as Bluebird, must be ES6 compatible * @param {object} [options.readConcern] Specify a read concern for the collection. (only MongoDB 3.2 or higher supported) * @param {ReadConcernLevel} [options.readConcern.level='local'] Specify a read concern level for the collection operations (only MongoDB 3.2 or higher supported) * @property {(Server|ReplSet|Mongos)} serverConfig Get the current db topology. @@ -132,19 +132,15 @@ const legalOptionNames = [ * @property {object} topology Access the topology object (single server, replicaset or mongos). * @returns {Db} a Db instance. */ + function Db(databaseName, topology, options) { options = options || {}; if (!(this instanceof Db)) return new Db(databaseName, topology, options); - - // Get the promiseLibrary - const promiseLibrary = options.promiseLibrary || Promise; + emitDeprecatedOptionWarning(options, ['promiseLibrary']); // Filter the options options = filterOptions(options, legalOptionNames); - // Ensure we put the promiseLib in the options - options.promiseLibrary = promiseLibrary; - // Internal state of the db object this.s = { // DbCache @@ -161,8 +157,6 @@ function Db(databaseName, topology, options) { bufferMaxEntries: typeof options.bufferMaxEntries === 'number' ? options.bufferMaxEntries : -1, // Set up the primary key factory or fallback to ObjectId pkFactory: options.pkFactory || ObjectId, - // Promise library - promiseLibrary: promiseLibrary, // No listener noListener: typeof options.noListener === 'boolean' ? options.noListener : false, // ReadConcern @@ -324,7 +318,7 @@ Db.prototype.aggregate = function(pipeline, options) { Db.prototype.admin = function() { const Admin = require('./admin'); - return new Admin(this, this.s.topology, this.s.promiseLibrary); + return new Admin(this, this.s.topology); }; /** @@ -380,9 +374,6 @@ Db.prototype.collection = function(name, options, callback) { options = options || {}; options = Object.assign({}, options); - // Set the promise library - options.promiseLibrary = this.s.promiseLibrary; - // If we have not set a collection level readConcern set the db level one options.readConcern = options.readConcern ? new ReadConcern(options.readConcern.level) @@ -490,7 +481,6 @@ Db.prototype.createCollection = deprecateOptions( function(name, options, callback) { if (typeof options === 'function') (callback = options), (options = {}); options = options || {}; - options.promiseLibrary = options.promiseLibrary || this.s.promiseLibrary; options.readConcern = options.readConcern ? new ReadConcern(options.readConcern.level) : this.readConcern; diff --git a/lib/gridfs-stream/index.js b/lib/gridfs-stream/index.js index 2521a81e84..c98e1cd3ec 100644 --- a/lib/gridfs-stream/index.js +++ b/lib/gridfs-stream/index.js @@ -50,8 +50,7 @@ function GridFSBucket(db, options) { _chunksCollection: db.collection(options.bucketName + '.chunks'), _filesCollection: db.collection(options.bucketName + '.files'), checkedIndexes: false, - calledOpenUploadStream: false, - promiseLibrary: db.s.promiseLibrary || Promise + calledOpenUploadStream: false }; } diff --git a/lib/gridfs-stream/upload.js b/lib/gridfs-stream/upload.js index a1c88fcae9..a4d48ecebd 100644 --- a/lib/gridfs-stream/upload.js +++ b/lib/gridfs-stream/upload.js @@ -1,5 +1,6 @@ 'use strict'; +const PromiseProvider = require('../promise_provider'); var crypto = require('crypto'); var stream = require('stream'); var util = require('util'); @@ -52,8 +53,7 @@ function GridFSBucketWriteStream(bucket, filename, options) { streamEnd: false, outstandingRequests: 0, errored: false, - aborted: false, - promiseLibrary: this.bucket.s.promiseLibrary + aborted: false }; if (!this.bucket.s.calledOpenUploadStream) { @@ -111,19 +111,20 @@ GridFSBucketWriteStream.prototype.write = function(chunk, encoding, callback) { */ GridFSBucketWriteStream.prototype.abort = function(callback) { + const Promise = PromiseProvider.get(); if (this.state.streamEnd) { var error = new Error('Cannot abort a stream that has already completed'); if (typeof callback === 'function') { return callback(error); } - return this.state.promiseLibrary.reject(error); + return Promise.reject(error); } if (this.state.aborted) { error = new Error('Cannot call abort() on a stream twice'); if (typeof callback === 'function') { return callback(error); } - return this.state.promiseLibrary.reject(error); + return Promise.reject(error); } this.state.aborted = true; this.chunks.deleteMany({ files_id: this.id }, function(error) { diff --git a/lib/mongo_client.js b/lib/mongo_client.js index b66b22d0f1..9c389844d4 100644 --- a/lib/mongo_client.js +++ b/lib/mongo_client.js @@ -9,6 +9,7 @@ const WriteConcern = require('./write_concern'); const { maybePromise, MongoDBNamespace } = require('./utils'); const { inherits, deprecate } = require('util'); const { connect, validOptions } = require('./operations/connect'); +const PromiseProvider = require('./promise_provider'); /** * @file The **MongoClient** class is a class that allows for making Connections to MongoDB. @@ -150,11 +151,14 @@ function MongoClient(url, options) { // Set up event emitter EventEmitter.call(this); + if (options && options.promiseLibrary) { + PromiseProvider.set(options.promiseLibrary); + } + // The internal state this.s = { url: url, options: options || {}, - promiseLibrary: (options && options.promiseLibrary) || Promise, dbCache: new Map(), sessions: new Set(), writeConcern: WriteConcern.fromOptions(options), @@ -203,7 +207,7 @@ MongoClient.prototype.connect = function(callback) { } const client = this; - return maybePromise(this, callback, cb => { + return maybePromise(callback, cb => { const err = validOptions(client.s.options); if (err) return cb(err); @@ -234,7 +238,7 @@ MongoClient.prototype.close = function(force, callback) { } const client = this; - return maybePromise(this, callback, cb => { + return maybePromise(callback, cb => { if (client.topology == null) { cb(); return; @@ -280,9 +284,6 @@ MongoClient.prototype.db = function(dbName, options) { return this.s.dbCache.get(dbName); } - // Add promiseLibrary - finalOptions.promiseLibrary = this.s.promiseLibrary; - // If no topology throw an error message if (!this.topology) { throw new MongoError('MongoClient must be connected before calling MongoClient.prototype.db'); @@ -397,6 +398,10 @@ MongoClient.connect = function(url, options, callback) { options = args.length ? args.shift() : null; options = options || {}; + if (options && options.promiseLibrary) { + PromiseProvider.set(options.promiseLibrary); + } + // Create client const mongoClient = new MongoClient(url, options); // Execute the connect method @@ -435,6 +440,7 @@ MongoClient.prototype.startSession = function(options) { MongoClient.prototype.withSession = function(options, operation) { if (typeof options === 'function') (operation = options), (options = undefined); const session = this.startSession(options); + const Promise = PromiseProvider.get(); let cleanupHandler = (err, result, opts) => { // prevent multiple calls to cleanupHandler diff --git a/lib/operations/connect.js b/lib/operations/connect.js index 12f2e41c7d..a16bc8ab63 100644 --- a/lib/operations/connect.js +++ b/lib/operations/connect.js @@ -303,9 +303,6 @@ function registerDeprecatedEventNotifiers(client) { } function createTopology(mongoClient, options, callback) { - // Pass in the promise library - options.promiseLibrary = mongoClient.s.promiseLibrary; - const translationOptions = { createServers: false }; diff --git a/lib/operations/db_ops.js b/lib/operations/db_ops.js index cc04247c23..a01dfc729b 100644 --- a/lib/operations/db_ops.js +++ b/lib/operations/db_ops.js @@ -50,7 +50,6 @@ const debugFields = [ 'retryMiliSeconds', 'readPreference', 'pkFactory', - 'promiseLibrary', 'noListener' ]; diff --git a/lib/operations/execute_operation.js b/lib/operations/execute_operation.js index 1ef1382739..51c4312823 100644 --- a/lib/operations/execute_operation.js +++ b/lib/operations/execute_operation.js @@ -1,5 +1,6 @@ 'use strict'; +const PromiseProvider = require('../promise_provider'); const ReadPreference = require('../read_preference'); const { MongoError, isRetryableError } = require('../error'); const { Aspect, OperationBase } = require('./operation'); @@ -19,6 +20,8 @@ const { maxWireVersion } = require('../utils'); * @param {Function} callback The command result callback */ function executeOperation(topology, operation, callback) { + const Promise = PromiseProvider.get(); + if (topology == null) { throw new TypeError('This method requires a valid topology instance'); } @@ -31,8 +34,6 @@ function executeOperation(topology, operation, callback) { return selectServerForSessionSupport(topology, operation, callback); } - const Promise = topology.s.promiseLibrary; - // The driver sessions spec mandates that we implicitly create sessions for operations // that are not explicitly provided with a session. let session, owner; @@ -156,7 +157,7 @@ function executeWithServerSelection(topology, operation, callback) { // TODO: This is only supported for unified topology, it should go away once // we remove support for legacy topology types. function selectServerForSessionSupport(topology, operation, callback) { - const Promise = topology.s.promiseLibrary; + const Promise = PromiseProvider.get(); let result; if (typeof callback !== 'function') { diff --git a/lib/promise_provider.js b/lib/promise_provider.js new file mode 100644 index 0000000000..5003d03a83 --- /dev/null +++ b/lib/promise_provider.js @@ -0,0 +1,40 @@ +'use strict'; + +const kPromise = Symbol('promise'); + +const store = { + [kPromise]: undefined +}; + +/** global promise store allowing user-provided promises */ +class PromiseProvider { + /** + * validates the passed in promise library + * + * @param {Function} lib promise implementation + */ + static validate(lib) { + if (typeof lib !== 'function') throw new Error(`Promise must be a function, got ${lib}`); + return lib; + } + + /** + * sets the promise library + * + * @param {Function} lib promise implementation + */ + static set(lib) { + store[kPromise] = PromiseProvider.validate(lib); + } + + /** + * get the stored promise library, or resolves passed in + */ + static get() { + return store[kPromise]; + } +} + +PromiseProvider.set(global.Promise); + +module.exports = PromiseProvider; diff --git a/lib/sdam/topology.js b/lib/sdam/topology.js index b9d7cf5f43..119c7a4556 100644 --- a/lib/sdam/topology.js +++ b/lib/sdam/topology.js @@ -1,4 +1,5 @@ 'use strict'; +const { emitDeprecatedOptionWarning } = require('../utils'); const Denque = require('denque'); const EventEmitter = require('events'); const ReadPreference = require('../read_preference'); @@ -104,6 +105,9 @@ class Topology extends EventEmitter { */ constructor(seedlist, options) { super(); + + emitDeprecatedOptionWarning(options, ['promiseLibrary']); + if (typeof options === 'undefined' && typeof seedlist !== 'string') { options = seedlist; seedlist = []; @@ -176,8 +180,6 @@ class Topology extends EventEmitter { sessionPool: new ServerSessionPool(this), // Active client sessions sessions: new Set(), - // Promise library - promiseLibrary: options.promiseLibrary || Promise, credentials: options.credentials, clusterTime: null, diff --git a/lib/sessions.js b/lib/sessions.js index b8a2ac840c..660501ae21 100644 --- a/lib/sessions.js +++ b/lib/sessions.js @@ -1,5 +1,6 @@ 'use strict'; +const PromiseProvider = require('./promise_provider'); const EventEmitter = require('events'); const { BSON: { Binary, Long } @@ -214,6 +215,8 @@ class ClientSession extends EventEmitter { * @returns {Promise} A promise is returned if no callback is provided */ commitTransaction(callback) { + const Promise = PromiseProvider.get(); + if (typeof callback === 'function') { endTransaction(this, 'commitTransaction', callback); return; @@ -233,6 +236,8 @@ class ClientSession extends EventEmitter { * @returns {Promise} A promise is returned if no callback is provided */ abortTransaction(callback) { + const Promise = PromiseProvider.get(); + if (typeof callback === 'function') { endTransaction(this, 'abortTransaction', callback); return; @@ -338,6 +343,7 @@ function userExplicitlyEndedTransaction(session) { } function attemptTransaction(session, startTime, fn, options) { + const Promise = PromiseProvider.get(); session.startTransaction(options); let promise; diff --git a/lib/utils.js b/lib/utils.js index ded9a5b966..ec24c03d5f 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -1,4 +1,6 @@ 'use strict'; + +const PromiseProvider = require('./promise_provider'); const os = require('os'); const crypto = require('crypto'); const { MongoError } = require('./error'); @@ -358,6 +360,8 @@ var mergeOptionsAndWriteConcern = function(targetOptions, sourceOptions, keys, m * @param {object} [options] Options that modify the behavior of the method */ const executeLegacyOperation = (topology, operation, args, options) => { + const Promise = PromiseProvider.get(); + if (topology == null) { throw new TypeError('This method requires a valid topology instance'); } @@ -367,7 +371,7 @@ const executeLegacyOperation = (topology, operation, args, options) => { } options = options || {}; - const Promise = topology.s.promiseLibrary; + let callback = args[args.length - 1]; // The driver sessions spec mandates that we implicitly create sessions for operations @@ -681,19 +685,16 @@ function* makeCounter(seed) { /** * Helper function for either accepting a callback, or returning a promise * - * @param {object} parent an instance of parent with promiseLibrary. - * @param {object} parent.s parent internal state - * @param {any} parent.s.promiseLibrary an object containing promiseLibrary. * @param {?Function} callback The last function argument in exposed method, controls if a Promise is returned * @param {Function} wrapper A function that wraps the callback * @returns {Promise|void} Returns nothing if a callback is supplied, else returns a Promise. */ -function maybePromise(parent, callback, wrapper) { - const PromiseLibrary = (parent && parent.s && parent.s.promiseLibrary) || Promise; +function maybePromise(callback, wrapper) { + const Promise = PromiseProvider.get(); let result; if (typeof callback !== 'function') { - result = new PromiseLibrary((resolve, reject) => { + result = new Promise((resolve, reject) => { callback = (err, res) => { if (err) return reject(err); resolve(res); @@ -929,6 +930,20 @@ function makeClientMetadata(options) { const noop = () => {}; +/** + * Loops over deprecated keys, will emit warning if key matched in options. + * + * @param {object} options an object of options + * @param {string[]} list deprecated option keys + */ +function emitDeprecatedOptionWarning(options, list) { + list.forEach(option => { + if (options && typeof options[option] !== 'undefined') { + emitDeprecationWarning(`option [${option}] is deprecated`); + } + }); +} + module.exports = { filterOptions, mergeOptions, @@ -956,6 +971,7 @@ module.exports = { MongoDBNamespace, resolveReadPreference, emitDeprecationWarning, + emitDeprecatedOptionWarning, makeCounter, maybePromise, databaseNamespace, diff --git a/package-lock.json b/package-lock.json index 400ce76ea7..a912b806ce 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1671,6 +1671,12 @@ "prettier-linter-helpers": "^1.0.0" } }, + "eslint-plugin-promise": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-4.2.1.tgz", + "integrity": "sha512-VoM09vT7bfA7D+upt+FjeBO5eHIJQBUWki1aPvB+vbNiHS3+oGIJGIeyBtKQTME6UPXXy3vV07OL1tHd3ANuDw==", + "dev": true + }, "eslint-scope": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.0.0.tgz", diff --git a/package.json b/package.json index 83c65d6ba7..f35763b477 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "eslint-config-prettier": "^6.10.1", "eslint-plugin-jsdoc": "^22.1.0", "eslint-plugin-prettier": "^3.1.2", + "eslint-plugin-promise": "^4.2.1", "jsdoc": "^3.6.3", "lodash.camelcase": "^4.3.0", "madge": "^3.8.0", diff --git a/test/functional/byo_promises.test.js b/test/functional/byo_promises.test.js index 70cb706590..4b2fb493fd 100644 --- a/test/functional/byo_promises.test.js +++ b/test/functional/byo_promises.test.js @@ -1,13 +1,10 @@ 'use strict'; -const maybePromise = require('./../../lib/utils').maybePromise; var expect = require('chai').expect; class CustomPromise extends Promise {} CustomPromise.prototype.isCustomMongo = true; -const parent = { s: { promiseLibrary: CustomPromise } }; - describe('Optional PromiseLibrary / maybePromise', function() { it('should correctly implement custom dependency-less promise', function(done) { const getCustomPromise = v => new CustomPromise(resolve => resolve(v)); @@ -19,33 +16,6 @@ describe('Optional PromiseLibrary / maybePromise', function() { done(); }); - it('should return a promise with extra property CustomMongo', function() { - const prom = maybePromise(parent, undefined, () => 'example'); - expect(prom).to.have.property('isCustomMongo'); - expect(prom).to.have.property('then'); - }); - - it('should return a native promise with no parent', function(done) { - const prom = maybePromise(undefined, undefined, () => 'example'); - expect(prom).to.not.have.property('isCustomMongo'); - expect(prom).to.have.property('then'); - done(); - }); - - it('should return a native promise with empty parent', function(done) { - const prom = maybePromise({}, undefined, () => 'example'); - expect(prom).to.not.have.property('isCustomMongo'); - expect(prom).to.have.property('then'); - done(); - }); - - it('should return a native promise with emtpy "s"', function(done) { - const prom = maybePromise({ s: {} }, undefined, () => 'example'); - expect(prom).to.not.have.property('isCustomMongo'); - expect(prom).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 }, { poolSize: 1 }); diff --git a/test/functional/collection.test.js b/test/functional/collection.test.js index c060ef4acb..f62e34fe2d 100644 --- a/test/functional/collection.test.js +++ b/test/functional/collection.test.js @@ -950,7 +950,7 @@ describe('Collection', function() { const docsPromise = collection.countDocuments(); const close = e => client.close(() => done(e)); - expect(docsPromise).to.exist.and.to.be.an.instanceof(collection.s.promiseLibrary); + expect(docsPromise).to.exist.and.to.be.an.instanceof(Promise); docsPromise .then(result => expect(result).to.equal(0)) diff --git a/test/functional/cursor.test.js b/test/functional/cursor.test.js index 295752ea3a..f206860084 100644 --- a/test/functional/cursor.test.js +++ b/test/functional/cursor.test.js @@ -3992,7 +3992,7 @@ describe('Cursor', function() { const cursor = collection.find(); const promise = cursor.forEach(); - expect(promise).to.exist.and.to.be.an.instanceof(cursor.s.promiseLibrary); + expect(promise).to.exist.and.to.be.an.instanceof(Promise); promise.catch(() => {}); cursor.close(() => client.close(() => done())); diff --git a/test/functional/promises_db.test.js b/test/functional/promises_db.test.js index 73caf092c1..93585d2fc8 100644 --- a/test/functional/promises_db.test.js +++ b/test/functional/promises_db.test.js @@ -363,30 +363,4 @@ describe('Promises (Db)', function() { }); } }); - - it('Should correctly execute createCollection using passed down CustomPromise Promise', { - metadata: { - requires: { - topology: ['single'] - } - }, - - test: function(done) { - var configuration = this.configuration; - var db = null; - - const client = configuration.newClient({}, { promiseLibrary: CustomPromise }); - client - .connect() - .then(function() { - db = client.db(configuration.db); - return db.createCollection('test'); - }) - .then(function(col) { - test.ok(col.s.options.promiseLibrary != null); - - client.close(done); - }); - } - }); }); diff --git a/test/unit/db.test.js b/test/unit/db.test.js index b28c889a48..9ff95af63d 100644 --- a/test/unit/db.test.js +++ b/test/unit/db.test.js @@ -7,9 +7,6 @@ const sinon = require('sinon'); class MockTopology extends EventEmitter { constructor() { super(); - this.s = { - promiseLibrary: Promise - }; } isDestroyed() { diff --git a/test/unit/execute_legacy_operation.test.js b/test/unit/execute_legacy_operation.test.js index bcceb2f060..3cad227441 100644 --- a/test/unit/execute_legacy_operation.test.js +++ b/test/unit/execute_legacy_operation.test.js @@ -9,10 +9,7 @@ describe('executeLegacyOperation', function() { let callbackError, caughtError; const topology = { - logicalSessionTimeoutMinutes: null, - s: { - promiseLibrary: Promise - } + logicalSessionTimeoutMinutes: null }; const operation = () => { throw expectedError; @@ -36,10 +33,7 @@ describe('executeLegacyOperation', function() { let callbackError; const topology = { - logicalSessionTimeoutMinutes: null, - s: { - promiseLibrary: Promise - } + logicalSessionTimeoutMinutes: null }; const operation = () => { throw expectedError;