utils.js 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997
  1. 'use strict';
  2. const MongoError = require('./core/error').MongoError;
  3. const WriteConcern = require('./write_concern');
  4. var shallowClone = function(obj) {
  5. var copy = {};
  6. for (var name in obj) copy[name] = obj[name];
  7. return copy;
  8. };
  9. // Set simple property
  10. var getSingleProperty = function(obj, name, value) {
  11. Object.defineProperty(obj, name, {
  12. enumerable: true,
  13. get: function() {
  14. return value;
  15. }
  16. });
  17. };
  18. var formatSortValue = (exports.formatSortValue = function(sortDirection) {
  19. var value = ('' + sortDirection).toLowerCase();
  20. switch (value) {
  21. case 'ascending':
  22. case 'asc':
  23. case '1':
  24. return 1;
  25. case 'descending':
  26. case 'desc':
  27. case '-1':
  28. return -1;
  29. default:
  30. throw new Error(
  31. 'Illegal sort clause, must be of the form ' +
  32. "[['field1', '(ascending|descending)'], " +
  33. "['field2', '(ascending|descending)']]"
  34. );
  35. }
  36. });
  37. var formattedOrderClause = (exports.formattedOrderClause = function(sortValue) {
  38. var orderBy = new Map();
  39. if (sortValue == null) return null;
  40. if (Array.isArray(sortValue)) {
  41. if (sortValue.length === 0) {
  42. return null;
  43. }
  44. for (var i = 0; i < sortValue.length; i++) {
  45. if (sortValue[i].constructor === String) {
  46. orderBy.set(`${sortValue[i]}`, 1);
  47. } else {
  48. orderBy.set(`${sortValue[i][0]}`, formatSortValue(sortValue[i][1]));
  49. }
  50. }
  51. } else if (sortValue != null && typeof sortValue === 'object') {
  52. if (sortValue instanceof Map) {
  53. orderBy = sortValue;
  54. } else {
  55. var sortKeys = Object.keys(sortValue);
  56. for (var k of sortKeys) {
  57. orderBy.set(k, sortValue[k]);
  58. }
  59. }
  60. } else if (typeof sortValue === 'string') {
  61. orderBy.set(`${sortValue}`, 1);
  62. } else {
  63. throw new Error(
  64. 'Illegal sort clause, must be of the form ' +
  65. "[['field1', '(ascending|descending)'], ['field2', '(ascending|descending)']]"
  66. );
  67. }
  68. return orderBy;
  69. });
  70. var checkCollectionName = function checkCollectionName(collectionName) {
  71. if ('string' !== typeof collectionName) {
  72. throw new MongoError('collection name must be a String');
  73. }
  74. if (!collectionName || collectionName.indexOf('..') !== -1) {
  75. throw new MongoError('collection names cannot be empty');
  76. }
  77. if (
  78. collectionName.indexOf('$') !== -1 &&
  79. collectionName.match(/((^\$cmd)|(oplog\.\$main))/) == null
  80. ) {
  81. throw new MongoError("collection names must not contain '$'");
  82. }
  83. if (collectionName.match(/^\.|\.$/) != null) {
  84. throw new MongoError("collection names must not start or end with '.'");
  85. }
  86. // Validate that we are not passing 0x00 in the collection name
  87. if (collectionName.indexOf('\x00') !== -1) {
  88. throw new MongoError('collection names cannot contain a null character');
  89. }
  90. };
  91. var handleCallback = function(callback, err, value1, value2) {
  92. try {
  93. if (callback == null) return;
  94. if (callback) {
  95. return value2 ? callback(err, value1, value2) : callback(err, value1);
  96. }
  97. } catch (err) {
  98. process.nextTick(function() {
  99. throw err;
  100. });
  101. return false;
  102. }
  103. return true;
  104. };
  105. /**
  106. * Wrap a Mongo error document in an Error instance
  107. * @ignore
  108. * @api private
  109. */
  110. var toError = function(error) {
  111. if (error instanceof Error) return error;
  112. var msg = error.err || error.errmsg || error.errMessage || error;
  113. var e = MongoError.create({ message: msg, driver: true });
  114. // Get all object keys
  115. var keys = typeof error === 'object' ? Object.keys(error) : [];
  116. for (var i = 0; i < keys.length; i++) {
  117. try {
  118. e[keys[i]] = error[keys[i]];
  119. } catch (err) {
  120. // continue
  121. }
  122. }
  123. return e;
  124. };
  125. /**
  126. * @ignore
  127. */
  128. var normalizeHintField = function normalizeHintField(hint) {
  129. var finalHint = null;
  130. if (typeof hint === 'string') {
  131. finalHint = hint;
  132. } else if (Array.isArray(hint)) {
  133. finalHint = {};
  134. hint.forEach(function(param) {
  135. finalHint[param] = 1;
  136. });
  137. } else if (hint != null && typeof hint === 'object') {
  138. finalHint = {};
  139. for (var name in hint) {
  140. finalHint[name] = hint[name];
  141. }
  142. }
  143. return finalHint;
  144. };
  145. /**
  146. * Create index name based on field spec
  147. *
  148. * @ignore
  149. * @api private
  150. */
  151. var parseIndexOptions = function(fieldOrSpec) {
  152. var fieldHash = {};
  153. var indexes = [];
  154. var keys;
  155. // Get all the fields accordingly
  156. if ('string' === typeof fieldOrSpec) {
  157. // 'type'
  158. indexes.push(fieldOrSpec + '_' + 1);
  159. fieldHash[fieldOrSpec] = 1;
  160. } else if (Array.isArray(fieldOrSpec)) {
  161. fieldOrSpec.forEach(function(f) {
  162. if ('string' === typeof f) {
  163. // [{location:'2d'}, 'type']
  164. indexes.push(f + '_' + 1);
  165. fieldHash[f] = 1;
  166. } else if (Array.isArray(f)) {
  167. // [['location', '2d'],['type', 1]]
  168. indexes.push(f[0] + '_' + (f[1] || 1));
  169. fieldHash[f[0]] = f[1] || 1;
  170. } else if (isObject(f)) {
  171. // [{location:'2d'}, {type:1}]
  172. keys = Object.keys(f);
  173. keys.forEach(function(k) {
  174. indexes.push(k + '_' + f[k]);
  175. fieldHash[k] = f[k];
  176. });
  177. } else {
  178. // undefined (ignore)
  179. }
  180. });
  181. } else if (isObject(fieldOrSpec)) {
  182. // {location:'2d', type:1}
  183. keys = Object.keys(fieldOrSpec);
  184. keys.forEach(function(key) {
  185. indexes.push(key + '_' + fieldOrSpec[key]);
  186. fieldHash[key] = fieldOrSpec[key];
  187. });
  188. }
  189. return {
  190. name: indexes.join('_'),
  191. keys: keys,
  192. fieldHash: fieldHash
  193. };
  194. };
  195. var isObject = (exports.isObject = function(arg) {
  196. return '[object Object]' === Object.prototype.toString.call(arg);
  197. });
  198. var debugOptions = function(debugFields, options) {
  199. var finaloptions = {};
  200. debugFields.forEach(function(n) {
  201. finaloptions[n] = options[n];
  202. });
  203. return finaloptions;
  204. };
  205. var decorateCommand = function(command, options, exclude) {
  206. for (var name in options) {
  207. if (exclude.indexOf(name) === -1) command[name] = options[name];
  208. }
  209. return command;
  210. };
  211. var mergeOptions = function(target, source) {
  212. for (var name in source) {
  213. target[name] = source[name];
  214. }
  215. return target;
  216. };
  217. // Merge options with translation
  218. var translateOptions = function(target, source) {
  219. var translations = {
  220. // SSL translation options
  221. sslCA: 'ca',
  222. sslCRL: 'crl',
  223. sslValidate: 'rejectUnauthorized',
  224. sslKey: 'key',
  225. sslCert: 'cert',
  226. sslPass: 'passphrase',
  227. // SocketTimeout translation options
  228. socketTimeoutMS: 'socketTimeout',
  229. connectTimeoutMS: 'connectionTimeout',
  230. // Replicaset options
  231. replicaSet: 'setName',
  232. rs_name: 'setName',
  233. secondaryAcceptableLatencyMS: 'acceptableLatency',
  234. connectWithNoPrimary: 'secondaryOnlyConnectionAllowed',
  235. // Mongos options
  236. acceptableLatencyMS: 'localThresholdMS'
  237. };
  238. for (var name in source) {
  239. if (translations[name]) {
  240. target[translations[name]] = source[name];
  241. } else {
  242. target[name] = source[name];
  243. }
  244. }
  245. return target;
  246. };
  247. var filterOptions = function(options, names) {
  248. var filterOptions = {};
  249. for (var name in options) {
  250. if (names.indexOf(name) !== -1) filterOptions[name] = options[name];
  251. }
  252. // Filtered options
  253. return filterOptions;
  254. };
  255. // Write concern keys
  256. const WRITE_CONCERN_KEYS = ['w', 'j', 'wtimeout', 'fsync', 'writeConcern'];
  257. /**
  258. * If there is no WriteConcern related options defined on target then inherit from source.
  259. * Otherwise, do not inherit **any** options from source.
  260. * @internal
  261. * @param {object} target - options object conditionally receiving the writeConcern options
  262. * @param {object} source - options object containing the potentially inherited writeConcern options
  263. */
  264. function conditionallyMergeWriteConcern(target, source) {
  265. let found = false;
  266. for (const wcKey of WRITE_CONCERN_KEYS) {
  267. if (wcKey in target) {
  268. // Found a writeConcern option
  269. found = true;
  270. break;
  271. }
  272. }
  273. if (!found) {
  274. for (const wcKey of WRITE_CONCERN_KEYS) {
  275. if (source[wcKey]) {
  276. if (!('writeConcern' in target)) {
  277. target.writeConcern = {};
  278. }
  279. target.writeConcern[wcKey] = source[wcKey];
  280. }
  281. }
  282. }
  283. return target;
  284. }
  285. /**
  286. * Executes the given operation with provided arguments.
  287. *
  288. * This method reduces large amounts of duplication in the entire codebase by providing
  289. * a single point for determining whether callbacks or promises should be used. Additionally
  290. * it allows for a single point of entry to provide features such as implicit sessions, which
  291. * are required by the Driver Sessions specification in the event that a ClientSession is
  292. * not provided
  293. *
  294. * @param {object} topology The topology to execute this operation on
  295. * @param {function} operation The operation to execute
  296. * @param {array} args Arguments to apply the provided operation
  297. * @param {object} [options] Options that modify the behavior of the method
  298. */
  299. const executeLegacyOperation = (topology, operation, args, options) => {
  300. if (topology == null) {
  301. throw new TypeError('This method requires a valid topology instance');
  302. }
  303. if (!Array.isArray(args)) {
  304. throw new TypeError('This method requires an array of arguments to apply');
  305. }
  306. options = options || {};
  307. const Promise = topology.s.promiseLibrary;
  308. let callback = args[args.length - 1];
  309. // The driver sessions spec mandates that we implicitly create sessions for operations
  310. // that are not explicitly provided with a session.
  311. let session, opOptions, owner;
  312. if (!options.skipSessions && topology.hasSessionSupport()) {
  313. opOptions = args[args.length - 2];
  314. if (opOptions == null || opOptions.session == null) {
  315. owner = Symbol();
  316. session = topology.startSession({ owner });
  317. const optionsIndex = args.length - 2;
  318. args[optionsIndex] = Object.assign({}, args[optionsIndex], { session: session });
  319. } else if (opOptions.session && opOptions.session.hasEnded) {
  320. throw new MongoError('Use of expired sessions is not permitted');
  321. }
  322. }
  323. const makeExecuteCallback = (resolve, reject) =>
  324. function executeCallback(err, result) {
  325. if (session && session.owner === owner && !options.returnsCursor) {
  326. session.endSession(() => {
  327. delete opOptions.session;
  328. if (err) return reject(err);
  329. resolve(result);
  330. });
  331. } else {
  332. if (err) return reject(err);
  333. resolve(result);
  334. }
  335. };
  336. // Execute using callback
  337. if (typeof callback === 'function') {
  338. callback = args.pop();
  339. const handler = makeExecuteCallback(
  340. result => callback(null, result),
  341. err => callback(err, null)
  342. );
  343. args.push(handler);
  344. try {
  345. return operation.apply(null, args);
  346. } catch (e) {
  347. handler(e);
  348. throw e;
  349. }
  350. }
  351. // Return a Promise
  352. if (args[args.length - 1] != null) {
  353. throw new TypeError('final argument to `executeLegacyOperation` must be a callback');
  354. }
  355. return new Promise(function(resolve, reject) {
  356. const handler = makeExecuteCallback(resolve, reject);
  357. args[args.length - 1] = handler;
  358. try {
  359. return operation.apply(null, args);
  360. } catch (e) {
  361. handler(e);
  362. }
  363. });
  364. };
  365. /**
  366. * Applies retryWrites: true to a command if retryWrites is set on the command's database.
  367. *
  368. * @param {object} target The target command to which we will apply retryWrites.
  369. * @param {object} db The database from which we can inherit a retryWrites value.
  370. */
  371. function applyRetryableWrites(target, db) {
  372. if (db && db.s.options.retryWrites) {
  373. target.retryWrites = true;
  374. }
  375. return target;
  376. }
  377. /**
  378. * Applies a write concern to a command based on well defined inheritance rules, optionally
  379. * detecting support for the write concern in the first place.
  380. *
  381. * @param {Object} target the target command we will be applying the write concern to
  382. * @param {Object} sources sources where we can inherit default write concerns from
  383. * @param {Object} [options] optional settings passed into a command for write concern overrides
  384. * @returns {Object} the (now) decorated target
  385. */
  386. function applyWriteConcern(target, sources, options) {
  387. options = options || {};
  388. const db = sources.db;
  389. const coll = sources.collection;
  390. if (options.session && options.session.inTransaction()) {
  391. // writeConcern is not allowed within a multi-statement transaction
  392. if (target.writeConcern) {
  393. delete target.writeConcern;
  394. }
  395. return target;
  396. }
  397. const writeConcern = WriteConcern.fromOptions(options);
  398. if (writeConcern) {
  399. return Object.assign(target, { writeConcern });
  400. }
  401. if (coll && coll.writeConcern) {
  402. return Object.assign(target, { writeConcern: Object.assign({}, coll.writeConcern) });
  403. }
  404. if (db && db.writeConcern) {
  405. return Object.assign(target, { writeConcern: Object.assign({}, db.writeConcern) });
  406. }
  407. return target;
  408. }
  409. /**
  410. * Checks if a given value is a Promise
  411. *
  412. * @param {*} maybePromise
  413. * @return true if the provided value is a Promise
  414. */
  415. function isPromiseLike(maybePromise) {
  416. return maybePromise && typeof maybePromise.then === 'function';
  417. }
  418. /**
  419. * Applies collation to a given command.
  420. *
  421. * @param {object} [command] the command on which to apply collation
  422. * @param {(Cursor|Collection)} [target] target of command
  423. * @param {object} [options] options containing collation settings
  424. */
  425. function decorateWithCollation(command, target, options) {
  426. const topology = (target.s && target.s.topology) || target.topology;
  427. if (!topology) {
  428. throw new TypeError('parameter "target" is missing a topology');
  429. }
  430. const capabilities = topology.capabilities();
  431. if (options.collation && typeof options.collation === 'object') {
  432. if (capabilities && capabilities.commandsTakeCollation) {
  433. command.collation = options.collation;
  434. } else {
  435. throw new MongoError(`Current topology does not support collation`);
  436. }
  437. }
  438. }
  439. /**
  440. * Applies a read concern to a given command.
  441. *
  442. * @param {object} command the command on which to apply the read concern
  443. * @param {Collection} coll the parent collection of the operation calling this method
  444. */
  445. function decorateWithReadConcern(command, coll, options) {
  446. if (options && options.session && options.session.inTransaction()) {
  447. return;
  448. }
  449. let readConcern = Object.assign({}, command.readConcern || {});
  450. if (coll.s.readConcern) {
  451. Object.assign(readConcern, coll.s.readConcern);
  452. }
  453. if (Object.keys(readConcern).length > 0) {
  454. Object.assign(command, { readConcern: readConcern });
  455. }
  456. }
  457. /**
  458. * Applies an explain to a given command.
  459. * @internal
  460. *
  461. * @param {object} command - the command on which to apply the explain
  462. * @param {Explain} explain - the options containing the explain verbosity
  463. * @return the new command
  464. */
  465. function decorateWithExplain(command, explain) {
  466. if (command.explain) {
  467. return command;
  468. }
  469. return { explain: command, verbosity: explain.verbosity };
  470. }
  471. const nodejsMajorVersion = +process.version.split('.')[0].substring(1);
  472. const emitProcessWarning = msg =>
  473. nodejsMajorVersion <= 6
  474. ? process.emitWarning(msg, 'DeprecationWarning', MONGODB_WARNING_CODE)
  475. : process.emitWarning(msg, { type: 'DeprecationWarning', code: MONGODB_WARNING_CODE });
  476. // eslint-disable-next-line no-console
  477. const emitConsoleWarning = msg => console.error(msg);
  478. const emitDeprecationWarning = process.emitWarning ? emitProcessWarning : emitConsoleWarning;
  479. /**
  480. * Default message handler for generating deprecation warnings.
  481. *
  482. * @param {string} name function name
  483. * @param {string} option option name
  484. * @return {string} warning message
  485. * @ignore
  486. * @api private
  487. */
  488. function defaultMsgHandler(name, option) {
  489. return `${name} option [${option}] is deprecated and will be removed in a later version.`;
  490. }
  491. /**
  492. * Deprecates a given function's options.
  493. *
  494. * @param {object} config configuration for deprecation
  495. * @param {string} config.name function name
  496. * @param {Array} config.deprecatedOptions options to deprecate
  497. * @param {number} config.optionsIndex index of options object in function arguments array
  498. * @param {function} [config.msgHandler] optional custom message handler to generate warnings
  499. * @param {function} fn the target function of deprecation
  500. * @return {function} modified function that warns once per deprecated option, and executes original function
  501. * @ignore
  502. * @api private
  503. */
  504. function deprecateOptions(config, fn) {
  505. if (process.noDeprecation === true) {
  506. return fn;
  507. }
  508. const msgHandler = config.msgHandler ? config.msgHandler : defaultMsgHandler;
  509. const optionsWarned = new Set();
  510. function deprecated() {
  511. const options = arguments[config.optionsIndex];
  512. // ensure options is a valid, non-empty object, otherwise short-circuit
  513. if (!isObject(options) || Object.keys(options).length === 0) {
  514. return fn.apply(this, arguments);
  515. }
  516. config.deprecatedOptions.forEach(deprecatedOption => {
  517. if (
  518. Object.prototype.hasOwnProperty.call(options, deprecatedOption) &&
  519. !optionsWarned.has(deprecatedOption)
  520. ) {
  521. optionsWarned.add(deprecatedOption);
  522. const msg = msgHandler(config.name, deprecatedOption);
  523. emitDeprecationWarning(msg);
  524. if (this && this.getLogger) {
  525. const logger = this.getLogger();
  526. if (logger) {
  527. logger.warn(msg);
  528. }
  529. }
  530. }
  531. });
  532. return fn.apply(this, arguments);
  533. }
  534. // These lines copied from https://github.com/nodejs/node/blob/25e5ae41688676a5fd29b2e2e7602168eee4ceb5/lib/internal/util.js#L73-L80
  535. // The wrapper will keep the same prototype as fn to maintain prototype chain
  536. Object.setPrototypeOf(deprecated, fn);
  537. if (fn.prototype) {
  538. // Setting this (rather than using Object.setPrototype, as above) ensures
  539. // that calling the unwrapped constructor gives an instanceof the wrapped
  540. // constructor.
  541. deprecated.prototype = fn.prototype;
  542. }
  543. return deprecated;
  544. }
  545. const SUPPORTS = {};
  546. // Test asyncIterator support
  547. try {
  548. require('./async/async_iterator');
  549. SUPPORTS.ASYNC_ITERATOR = true;
  550. } catch (e) {
  551. SUPPORTS.ASYNC_ITERATOR = false;
  552. }
  553. class MongoDBNamespace {
  554. constructor(db, collection) {
  555. this.db = db;
  556. this.collection = collection;
  557. }
  558. toString() {
  559. return this.collection ? `${this.db}.${this.collection}` : this.db;
  560. }
  561. withCollection(collection) {
  562. return new MongoDBNamespace(this.db, collection);
  563. }
  564. static fromString(namespace) {
  565. if (!namespace) {
  566. throw new Error(`Cannot parse namespace from "${namespace}"`);
  567. }
  568. const index = namespace.indexOf('.');
  569. return new MongoDBNamespace(namespace.substring(0, index), namespace.substring(index + 1));
  570. }
  571. }
  572. function* makeCounter(seed) {
  573. let count = seed || 0;
  574. while (true) {
  575. const newCount = count;
  576. count += 1;
  577. yield newCount;
  578. }
  579. }
  580. /**
  581. * Helper function for either accepting a callback, or returning a promise
  582. *
  583. * @param {Object} parent an instance of parent with promiseLibrary.
  584. * @param {object} parent.s an object containing promiseLibrary.
  585. * @param {function} parent.s.promiseLibrary an object containing promiseLibrary.
  586. * @param {[Function]} callback an optional callback.
  587. * @param {Function} fn A function that takes a callback
  588. * @returns {Promise|void} Returns nothing if a callback is supplied, else returns a Promise.
  589. */
  590. function maybePromise(parent, callback, fn) {
  591. const PromiseLibrary = (parent && parent.s && parent.s.promiseLibrary) || Promise;
  592. let result;
  593. if (typeof callback !== 'function') {
  594. result = new PromiseLibrary((resolve, reject) => {
  595. callback = (err, res) => {
  596. if (err) return reject(err);
  597. resolve(res);
  598. };
  599. });
  600. }
  601. fn(function(err, res) {
  602. if (err != null) {
  603. try {
  604. callback(err);
  605. } catch (error) {
  606. return process.nextTick(() => {
  607. throw error;
  608. });
  609. }
  610. return;
  611. }
  612. callback(err, res);
  613. });
  614. return result;
  615. }
  616. function now() {
  617. const hrtime = process.hrtime();
  618. return Math.floor(hrtime[0] * 1000 + hrtime[1] / 1000000);
  619. }
  620. function calculateDurationInMs(started) {
  621. if (typeof started !== 'number') {
  622. throw TypeError('numeric value required to calculate duration');
  623. }
  624. const elapsed = now() - started;
  625. return elapsed < 0 ? 0 : elapsed;
  626. }
  627. /**
  628. * Creates an interval timer which is able to be woken up sooner than
  629. * the interval. The timer will also debounce multiple calls to wake
  630. * ensuring that the function is only ever called once within a minimum
  631. * interval window.
  632. *
  633. * @param {function} fn An async function to run on an interval, must accept a `callback` as its only parameter
  634. * @param {object} [options] Optional settings
  635. * @param {number} [options.interval] The interval at which to run the provided function
  636. * @param {number} [options.minInterval] The minimum time which must pass between invocations of the provided function
  637. * @param {boolean} [options.immediate] Execute the function immediately when the interval is started
  638. */
  639. function makeInterruptableAsyncInterval(fn, options) {
  640. let timerId;
  641. let lastCallTime;
  642. let lastWakeTime;
  643. let stopped = false;
  644. options = options || {};
  645. const interval = options.interval || 1000;
  646. const minInterval = options.minInterval || 500;
  647. const immediate = typeof options.immediate === 'boolean' ? options.immediate : false;
  648. const clock = typeof options.clock === 'function' ? options.clock : now;
  649. function wake() {
  650. const currentTime = clock();
  651. const timeSinceLastWake = currentTime - lastWakeTime;
  652. const timeSinceLastCall = currentTime - lastCallTime;
  653. const timeUntilNextCall = interval - timeSinceLastCall;
  654. lastWakeTime = currentTime;
  655. // For the streaming protocol: there is nothing obviously stopping this
  656. // interval from being woken up again while we are waiting "infinitely"
  657. // for `fn` to be called again`. Since the function effectively
  658. // never completes, the `timeUntilNextCall` will continue to grow
  659. // negatively unbounded, so it will never trigger a reschedule here.
  660. // debounce multiple calls to wake within the `minInterval`
  661. if (timeSinceLastWake < minInterval) {
  662. return;
  663. }
  664. // reschedule a call as soon as possible, ensuring the call never happens
  665. // faster than the `minInterval`
  666. if (timeUntilNextCall > minInterval) {
  667. reschedule(minInterval);
  668. }
  669. // This is possible in virtualized environments like AWS Lambda where our
  670. // clock is unreliable. In these cases the timer is "running" but never
  671. // actually completes, so we want to execute immediately and then attempt
  672. // to reschedule.
  673. if (timeUntilNextCall < 0) {
  674. executeAndReschedule();
  675. }
  676. }
  677. function stop() {
  678. stopped = true;
  679. if (timerId) {
  680. clearTimeout(timerId);
  681. timerId = null;
  682. }
  683. lastCallTime = 0;
  684. lastWakeTime = 0;
  685. }
  686. function reschedule(ms) {
  687. if (stopped) return;
  688. clearTimeout(timerId);
  689. timerId = setTimeout(executeAndReschedule, ms || interval);
  690. }
  691. function executeAndReschedule() {
  692. lastWakeTime = 0;
  693. lastCallTime = clock();
  694. fn(err => {
  695. if (err) throw err;
  696. reschedule(interval);
  697. });
  698. }
  699. if (immediate) {
  700. executeAndReschedule();
  701. } else {
  702. lastCallTime = clock();
  703. reschedule();
  704. }
  705. return { wake, stop };
  706. }
  707. function hasAtomicOperators(doc) {
  708. if (Array.isArray(doc)) {
  709. return doc.reduce((err, u) => err || hasAtomicOperators(u), null);
  710. }
  711. return (
  712. Object.keys(typeof doc.toBSON !== 'function' ? doc : doc.toBSON())
  713. .map(k => k[0])
  714. .indexOf('$') >= 0
  715. );
  716. }
  717. /**
  718. * When the driver used emitWarning the code will be equal to this.
  719. * @public
  720. *
  721. * @example
  722. * ```js
  723. * process.on('warning', (warning) => {
  724. * if (warning.code === MONGODB_WARNING_CODE) console.error('Ah an important warning! :)')
  725. * })
  726. * ```
  727. */
  728. const MONGODB_WARNING_CODE = 'MONGODB DRIVER';
  729. /**
  730. * @internal
  731. * @param {string} message - message to warn about
  732. */
  733. function emitWarning(message) {
  734. if (process.emitWarning) {
  735. return nodejsMajorVersion <= 6
  736. ? process.emitWarning(message, undefined, MONGODB_WARNING_CODE)
  737. : process.emitWarning(message, { code: MONGODB_WARNING_CODE });
  738. } else {
  739. // Approximate the style of print out on node versions pre 8.x
  740. // eslint-disable-next-line no-console
  741. return console.error(`[${MONGODB_WARNING_CODE}] Warning:`, message);
  742. }
  743. }
  744. const emittedWarnings = new Set();
  745. /**
  746. * Will emit a warning once for the duration of the application.
  747. * Uses the message to identify if it has already been emitted
  748. * so using string interpolation can cause multiple emits
  749. * @internal
  750. * @param {string} message - message to warn about
  751. */
  752. function emitWarningOnce(message) {
  753. if (!emittedWarnings.has(message)) {
  754. emittedWarnings.add(message);
  755. return emitWarning(message);
  756. }
  757. }
  758. function isSuperset(set, subset) {
  759. set = Array.isArray(set) ? new Set(set) : set;
  760. subset = Array.isArray(subset) ? new Set(subset) : subset;
  761. for (const elem of subset) {
  762. if (!set.has(elem)) {
  763. return false;
  764. }
  765. }
  766. return true;
  767. }
  768. function isRecord(value, requiredKeys) {
  769. const toString = Object.prototype.toString;
  770. const hasOwnProperty = Object.prototype.hasOwnProperty;
  771. const isObject = v => toString.call(v) === '[object Object]';
  772. if (!isObject(value)) {
  773. return false;
  774. }
  775. const ctor = value.constructor;
  776. if (ctor && ctor.prototype) {
  777. if (!isObject(ctor.prototype)) {
  778. return false;
  779. }
  780. // Check to see if some method exists from the Object exists
  781. if (!hasOwnProperty.call(ctor.prototype, 'isPrototypeOf')) {
  782. return false;
  783. }
  784. }
  785. if (requiredKeys) {
  786. const keys = Object.keys(value);
  787. return isSuperset(keys, requiredKeys);
  788. }
  789. return true;
  790. }
  791. /**
  792. * Make a deep copy of an object
  793. *
  794. * NOTE: This is not meant to be the perfect implementation of a deep copy,
  795. * but instead something that is good enough for the purposes of
  796. * command monitoring.
  797. */
  798. function deepCopy(value) {
  799. if (value == null) {
  800. return value;
  801. } else if (Array.isArray(value)) {
  802. return value.map(item => deepCopy(item));
  803. } else if (isRecord(value)) {
  804. const res = {};
  805. for (const key in value) {
  806. res[key] = deepCopy(value[key]);
  807. }
  808. return res;
  809. }
  810. const ctor = value.constructor;
  811. if (ctor) {
  812. switch (ctor.name.toLowerCase()) {
  813. case 'date':
  814. return new ctor(Number(value));
  815. case 'map':
  816. return new Map(value);
  817. case 'set':
  818. return new Set(value);
  819. case 'buffer':
  820. return Buffer.from(value);
  821. }
  822. }
  823. return value;
  824. }
  825. /**
  826. * @param {{version: string}} pkg
  827. * @returns {{ major: number; minor: number; patch: number }}
  828. */
  829. function parsePackageVersion(pkg) {
  830. const versionParts = pkg.version.split('.').map(n => Number.parseInt(n, 10));
  831. return { major: versionParts[0], minor: versionParts[1], patch: versionParts[2] };
  832. }
  833. module.exports = {
  834. filterOptions,
  835. mergeOptions,
  836. translateOptions,
  837. shallowClone,
  838. getSingleProperty,
  839. checkCollectionName,
  840. toError,
  841. formattedOrderClause,
  842. parseIndexOptions,
  843. normalizeHintField,
  844. handleCallback,
  845. decorateCommand,
  846. isObject,
  847. debugOptions,
  848. MAX_JS_INT: Number.MAX_SAFE_INTEGER + 1,
  849. conditionallyMergeWriteConcern,
  850. executeLegacyOperation,
  851. applyRetryableWrites,
  852. applyWriteConcern,
  853. isPromiseLike,
  854. decorateWithCollation,
  855. decorateWithReadConcern,
  856. decorateWithExplain,
  857. deprecateOptions,
  858. SUPPORTS,
  859. MongoDBNamespace,
  860. emitDeprecationWarning,
  861. makeCounter,
  862. maybePromise,
  863. now,
  864. calculateDurationInMs,
  865. makeInterruptableAsyncInterval,
  866. hasAtomicOperators,
  867. MONGODB_WARNING_CODE,
  868. emitWarning,
  869. emitWarningOnce,
  870. deepCopy,
  871. parsePackageVersion
  872. };