mongodb 3.5.4 → 3.5.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/HISTORY.md CHANGED
@@ -2,6 +2,64 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4
4
 
5
+ <a name="3.5.8"></a>
6
+ ## [3.5.8](https://github.com/mongodb/node-mongodb-native/compare/v3.5.7...v3.5.8) (2020-05-28)
7
+
8
+
9
+ ### Bug Fixes
10
+
11
+ * always clear cancelled wait queue members during processing ([0394f9d](https://github.com/mongodb/node-mongodb-native/commit/0394f9d))
12
+ * always include `writeErrors` on a `BulkWriteError` instance ([58b4f94](https://github.com/mongodb/node-mongodb-native/commit/58b4f94))
13
+ * ensure implicit sessions are ended consistently ([5c6fda1](https://github.com/mongodb/node-mongodb-native/commit/5c6fda1))
14
+ * filter servers before applying reducers ([4faf9f5](https://github.com/mongodb/node-mongodb-native/commit/4faf9f5))
15
+ * unordered bulk write should attempt to execute all batches ([6cee96b](https://github.com/mongodb/node-mongodb-native/commit/6cee96b))
16
+ * **ChangeStream:** should resume from errors when iterating ([5ecf18e](https://github.com/mongodb/node-mongodb-native/commit/5ecf18e))
17
+ * honor journal=true in connection string ([#2359](https://github.com/mongodb/node-mongodb-native/issues/2359)) ([246669f](https://github.com/mongodb/node-mongodb-native/commit/246669f))
18
+ * **ChangeStream:** whitelist resumable errors ([#2337](https://github.com/mongodb/node-mongodb-native/issues/2337)) ([a9d3965](https://github.com/mongodb/node-mongodb-native/commit/a9d3965)), closes [#17](https://github.com/mongodb/node-mongodb-native/issues/17) [#18](https://github.com/mongodb/node-mongodb-native/issues/18)
19
+
20
+
21
+
22
+ <a name="3.5.7"></a>
23
+ ## [3.5.7](https://github.com/mongodb/node-mongodb-native/compare/v3.5.6...v3.5.7) (2020-04-29)
24
+
25
+
26
+ ### Bug Fixes
27
+
28
+ * limit growth of server sessions through lazy acquisition ([3d05a6d](https://github.com/mongodb/node-mongodb-native/commit/3d05a6d))
29
+ * remove circular dependency warnings on node 14 ([56a1b8a](https://github.com/mongodb/node-mongodb-native/commit/56a1b8a))
30
+
31
+
32
+
33
+ <a name="3.5.6"></a>
34
+ ## [3.5.6](https://github.com/mongodb/node-mongodb-native/compare/v3.5.5...v3.5.6) (2020-04-14)
35
+
36
+
37
+ ### Bug Fixes
38
+
39
+ * always return empty array for selection on unknown topology ([f9e786a](https://github.com/mongodb/node-mongodb-native/commit/f9e786a))
40
+ * createCollection only uses listCollections in strict mode ([d368f12](https://github.com/mongodb/node-mongodb-native/commit/d368f12))
41
+ * don't throw if `withTransaction()` callback rejects with a null reason ([153646c](https://github.com/mongodb/node-mongodb-native/commit/153646c))
42
+ * only mark server session dirty if the client session is alive ([611be8d](https://github.com/mongodb/node-mongodb-native/commit/611be8d))
43
+ * polyfill for util.promisify ([1c4cf6c](https://github.com/mongodb/node-mongodb-native/commit/1c4cf6c))
44
+ * single `readPreferenceTags` should be parsed as an array ([a50611b](https://github.com/mongodb/node-mongodb-native/commit/a50611b))
45
+ * **cursor:** transforms should only be applied once to documents ([704f30a](https://github.com/mongodb/node-mongodb-native/commit/704f30a))
46
+
47
+
48
+
49
+ <a name="3.5.5"></a>
50
+ ## [3.5.5](https://github.com/mongodb/node-mongodb-native/compare/v3.5.4...v3.5.5) (2020-03-11)
51
+
52
+
53
+ ### Bug Fixes
54
+
55
+ * correctly use template string for connection string error message ([6238c84](https://github.com/mongodb/node-mongodb-native/commit/6238c84))
56
+ * don't depend on private node api for `Timeout` wrapper ([3ddaa3e](https://github.com/mongodb/node-mongodb-native/commit/3ddaa3e))
57
+ * multiple concurrent attempts to process the queue may fail ([f69f51c](https://github.com/mongodb/node-mongodb-native/commit/f69f51c))
58
+ * pass optional promise lib to maybePromise ([cde11ec](https://github.com/mongodb/node-mongodb-native/commit/cde11ec))
59
+ * **cursor:** hasNext consumes documents on cursor with limit ([ef04d00](https://github.com/mongodb/node-mongodb-native/commit/ef04d00))
60
+
61
+
62
+
5
63
  <a name="3.5.4"></a>
6
64
  ## [3.5.4](https://github.com/mongodb/node-mongodb-native/compare/v3.5.3...v3.5.4) (2020-02-25)
7
65
 
package/README.md CHANGED
@@ -33,7 +33,7 @@ Core Server (i.e. SERVER) project are **public**.
33
33
 
34
34
  ### Support / Feedback
35
35
 
36
- For issues with, questions about, or feedback for the Node.js driver, please look into our [support channels](http://www.mongodb.org/about/support). Please do not email any of the driver developers directly with issues or questions - you're more likely to get an answer on the [mongodb-user](http://groups.google.com/group/mongodb-user>) list on Google Groups.
36
+ For issues with, questions about, or feedback for the Node.js driver, please look into our [support channels](https://docs.mongodb.com/manual/support). Please do not email any of the driver developers directly with issues or questions - you're more likely to get an answer on the [MongoDB Community Forums](https://community.mongodb.com/tags/c/drivers-odms-connectors/7/node-js-driver).
37
37
 
38
38
  ### Change Log
39
39
 
@@ -1200,19 +1200,10 @@ class BulkOperationBase {
1200
1200
  * @ignore
1201
1201
  * @param {function} callback
1202
1202
  * @param {BulkWriteResult} writeResult
1203
- * @param {class} self either OrderedBulkOperation or UnorderdBulkOperation
1203
+ * @param {class} self either OrderedBulkOperation or UnorderedBulkOperation
1204
1204
  */
1205
1205
  handleWriteError(callback, writeResult) {
1206
1206
  if (this.s.bulkResult.writeErrors.length > 0) {
1207
- if (this.s.bulkResult.writeErrors.length === 1) {
1208
- handleCallback(
1209
- callback,
1210
- new BulkWriteError(toError(this.s.bulkResult.writeErrors[0]), writeResult),
1211
- null
1212
- );
1213
- return true;
1214
- }
1215
-
1216
1207
  const msg = this.s.bulkResult.writeErrors[0].errmsg
1217
1208
  ? this.s.bulkResult.writeErrors[0].errmsg
1218
1209
  : 'write operation failed';
@@ -1230,7 +1221,9 @@ class BulkOperationBase {
1230
1221
  null
1231
1222
  );
1232
1223
  return true;
1233
- } else if (writeResult.getWriteConcernError()) {
1224
+ }
1225
+
1226
+ if (writeResult.getWriteConcernError()) {
1234
1227
  handleCallback(
1235
1228
  callback,
1236
1229
  new BulkWriteError(toError(writeResult.getWriteConcernError()), writeResult),
@@ -108,6 +108,14 @@ class UnorderedBulkOperation extends BulkOperationBase {
108
108
 
109
109
  super(topology, collection, options, false);
110
110
  }
111
+
112
+ handleWriteError(callback, writeResult) {
113
+ if (this.s.batches.length) {
114
+ return false;
115
+ }
116
+
117
+ return super.handleWriteError(callback, writeResult);
118
+ }
111
119
  }
112
120
 
113
121
  /**
@@ -1,13 +1,17 @@
1
1
  'use strict';
2
2
 
3
+ const Denque = require('denque');
3
4
  const EventEmitter = require('events');
4
5
  const isResumableError = require('./error').isResumableError;
5
6
  const MongoError = require('./core').MongoError;
6
7
  const Cursor = require('./cursor');
7
8
  const relayEvents = require('./core/utils').relayEvents;
8
9
  const maxWireVersion = require('./core/utils').maxWireVersion;
10
+ const maybePromise = require('./utils').maybePromise;
9
11
  const AggregateOperation = require('./operations/aggregate');
10
12
 
13
+ const kResumeQueue = Symbol('resumeQueue');
14
+
11
15
  const CHANGE_STREAM_OPTIONS = ['resumeAfter', 'startAfter', 'startAtOperationTime', 'fullDocument'];
12
16
  const CURSOR_OPTIONS = ['batchSize', 'maxAwaitTimeMS', 'collation', 'readPreference'].concat(
13
17
  CHANGE_STREAM_OPTIONS
@@ -90,15 +94,17 @@ class ChangeStream extends EventEmitter {
90
94
  this.options.readPreference = parent.s.readPreference;
91
95
  }
92
96
 
97
+ this[kResumeQueue] = new Denque();
98
+
93
99
  // Create contained Change Stream cursor
94
100
  this.cursor = createChangeStreamCursor(this, options);
95
101
 
102
+ this.closed = false;
103
+
96
104
  // Listen for any `change` listeners being added to ChangeStream
97
105
  this.on('newListener', eventName => {
98
106
  if (eventName === 'change' && this.cursor && this.listenerCount('change') === 0) {
99
- this.cursor.on('data', change =>
100
- processNewChange({ changeStream: this, change, eventEmitter: true })
101
- );
107
+ this.cursor.on('data', change => processNewChange(this, change));
102
108
  }
103
109
  });
104
110
 
@@ -124,10 +130,15 @@ class ChangeStream extends EventEmitter {
124
130
  * @function ChangeStream.prototype.hasNext
125
131
  * @param {ChangeStream~resultCallback} [callback] The result callback.
126
132
  * @throws {MongoError}
127
- * @return {Promise} returns Promise if no callback passed
133
+ * @returns {Promise|void} returns Promise if no callback passed
128
134
  */
129
135
  hasNext(callback) {
130
- return this.cursor.hasNext(callback);
136
+ return maybePromise(this.parent, callback, cb => {
137
+ getCursor(this, (err, cursor) => {
138
+ if (err) return cb(err); // failed to resume, raise an error
139
+ cursor.hasNext(cb);
140
+ });
141
+ });
131
142
  }
132
143
 
133
144
  /**
@@ -135,31 +146,32 @@ class ChangeStream extends EventEmitter {
135
146
  * @function ChangeStream.prototype.next
136
147
  * @param {ChangeStream~resultCallback} [callback] The result callback.
137
148
  * @throws {MongoError}
138
- * @return {Promise} returns Promise if no callback passed
149
+ * @returns {Promise|void} returns Promise if no callback passed
139
150
  */
140
151
  next(callback) {
141
- var self = this;
142
- if (this.isClosed()) {
143
- if (callback) return callback(new Error('Change Stream is not open.'), null);
144
- return self.promiseLibrary.reject(new Error('Change Stream is not open.'));
145
- }
146
-
147
- return this.cursor
148
- .next()
149
- .then(change => processNewChange({ changeStream: self, change, callback }))
150
- .catch(error => processNewChange({ changeStream: self, error, callback }));
152
+ return maybePromise(this.parent, callback, cb => {
153
+ getCursor(this, (err, cursor) => {
154
+ if (err) return cb(err); // failed to resume, raise an error
155
+ cursor.next((error, change) => {
156
+ if (error) {
157
+ this[kResumeQueue].push(() => this.next(cb));
158
+ processError(this, error, cb);
159
+ return;
160
+ }
161
+ processNewChange(this, change, cb);
162
+ });
163
+ });
164
+ });
151
165
  }
152
166
 
153
167
  /**
154
- * Is the cursor closed
168
+ * Is the change stream closed
155
169
  * @method ChangeStream.prototype.isClosed
170
+ * @param {boolean} [checkCursor=true] also check if the underlying cursor is closed
156
171
  * @return {boolean}
157
172
  */
158
173
  isClosed() {
159
- if (this.cursor) {
160
- return this.cursor.isClosed();
161
- }
162
- return true;
174
+ return this.closed || (this.cursor && this.cursor.isClosed());
163
175
  }
164
176
 
165
177
  /**
@@ -169,31 +181,22 @@ class ChangeStream extends EventEmitter {
169
181
  * @return {Promise} returns Promise if no callback passed
170
182
  */
171
183
  close(callback) {
172
- if (!this.cursor) {
173
- if (callback) return callback();
174
- return this.promiseLibrary.resolve();
175
- }
184
+ return maybePromise(this.parent, callback, cb => {
185
+ if (this.closed) return cb();
176
186
 
177
- // Tidy up the existing cursor
178
- const cursor = this.cursor;
187
+ // flag the change stream as explicitly closed
188
+ this.closed = true;
179
189
 
180
- if (callback) {
181
- return cursor.close(err => {
182
- ['data', 'close', 'end', 'error'].forEach(event => cursor.removeAllListeners(event));
183
- delete this.cursor;
190
+ if (!this.cursor) return cb();
184
191
 
185
- return callback(err);
186
- });
187
- }
192
+ // Tidy up the existing cursor
193
+ const cursor = this.cursor;
188
194
 
189
- const PromiseCtor = this.promiseLibrary || Promise;
190
- return new PromiseCtor((resolve, reject) => {
191
- cursor.close(err => {
195
+ return cursor.close(err => {
192
196
  ['data', 'close', 'end', 'error'].forEach(event => cursor.removeAllListeners(event));
193
- delete this.cursor;
197
+ this.cursor = undefined;
194
198
 
195
- if (err) return reject(err);
196
- resolve();
199
+ return cb(err);
197
200
  });
198
201
  });
199
202
  }
@@ -288,7 +291,9 @@ class ChangeStreamCursor extends Cursor {
288
291
  ['resumeAfter', 'startAfter', 'startAtOperationTime'].forEach(key => delete result[key]);
289
292
 
290
293
  if (this.resumeToken) {
291
- result.resumeAfter = this.resumeToken;
294
+ const resumeKey =
295
+ this.options.startAfter && !this.hasReceived ? 'startAfter' : 'resumeAfter';
296
+ result[resumeKey] = this.resumeToken;
292
297
  } else if (this.startAtOperationTime && maxWireVersion(this.server) >= 7) {
293
298
  result.startAtOperationTime = this.startAtOperationTime;
294
299
  }
@@ -297,10 +302,30 @@ class ChangeStreamCursor extends Cursor {
297
302
  return result;
298
303
  }
299
304
 
305
+ cacheResumeToken(resumeToken) {
306
+ if (this.bufferedCount() === 0 && this.cursorState.postBatchResumeToken) {
307
+ this.resumeToken = this.cursorState.postBatchResumeToken;
308
+ } else {
309
+ this.resumeToken = resumeToken;
310
+ }
311
+ this.hasReceived = true;
312
+ }
313
+
314
+ _processBatch(batchName, response) {
315
+ const cursor = response.cursor;
316
+ if (cursor.postBatchResumeToken) {
317
+ this.cursorState.postBatchResumeToken = cursor.postBatchResumeToken;
318
+
319
+ if (cursor[batchName].length === 0) {
320
+ this.resumeToken = cursor.postBatchResumeToken;
321
+ }
322
+ }
323
+ }
324
+
300
325
  _initializeCursor(callback) {
301
326
  super._initializeCursor((err, result) => {
302
327
  if (err) {
303
- callback(err, null);
328
+ callback(err);
304
329
  return;
305
330
  }
306
331
 
@@ -315,15 +340,9 @@ class ChangeStreamCursor extends Cursor {
315
340
  this.startAtOperationTime = response.operationTime;
316
341
  }
317
342
 
318
- const cursor = response.cursor;
319
- if (cursor.postBatchResumeToken) {
320
- this.cursorState.postBatchResumeToken = cursor.postBatchResumeToken;
321
-
322
- if (cursor.firstBatch.length === 0) {
323
- this.resumeToken = cursor.postBatchResumeToken;
324
- }
325
- }
343
+ this._processBatch('firstBatch', response);
326
344
 
345
+ this.emit('init', result);
327
346
  this.emit('response');
328
347
  callback(err, result);
329
348
  });
@@ -332,19 +351,13 @@ class ChangeStreamCursor extends Cursor {
332
351
  _getMore(callback) {
333
352
  super._getMore((err, response) => {
334
353
  if (err) {
335
- callback(err, null);
354
+ callback(err);
336
355
  return;
337
356
  }
338
357
 
339
- const cursor = response.cursor;
340
- if (cursor.postBatchResumeToken) {
341
- this.cursorState.postBatchResumeToken = cursor.postBatchResumeToken;
342
-
343
- if (cursor.nextBatch.length === 0) {
344
- this.resumeToken = cursor.postBatchResumeToken;
345
- }
346
- }
358
+ this._processBatch('nextBatch', response);
347
359
 
360
+ this.emit('more', response);
348
361
  this.emit('response');
349
362
  callback(err, response);
350
363
  });
@@ -367,6 +380,7 @@ function createChangeStreamCursor(self, options) {
367
380
 
368
381
  const pipeline = [{ $changeStream: changeStreamStageOptions }].concat(self.pipeline);
369
382
  const cursorOptions = applyKnownOptions({}, options, CURSOR_OPTIONS);
383
+
370
384
  const changeStreamCursor = new ChangeStreamCursor(
371
385
  self.topology,
372
386
  new AggregateOperation(self.parent, pipeline, options),
@@ -385,7 +399,7 @@ function createChangeStreamCursor(self, options) {
385
399
  */
386
400
  if (self.listenerCount('change') > 0) {
387
401
  changeStreamCursor.on('data', function(change) {
388
- processNewChange({ changeStream: self, change, eventEmitter: true });
402
+ processNewChange(self, change);
389
403
  });
390
404
  }
391
405
 
@@ -417,7 +431,7 @@ function createChangeStreamCursor(self, options) {
417
431
  * @type {Error}
418
432
  */
419
433
  changeStreamCursor.on('error', function(error) {
420
- processNewChange({ changeStream: self, error, eventEmitter: true });
434
+ processError(self, error);
421
435
  });
422
436
 
423
437
  if (self.pipeDestinations) {
@@ -450,120 +464,141 @@ function waitForTopologyConnected(topology, options, callback) {
450
464
  const timeout = options.timeout || SELECTION_TIMEOUT;
451
465
  const readPreference = options.readPreference;
452
466
 
453
- if (topology.isConnected({ readPreference })) return callback(null, null);
467
+ if (topology.isConnected({ readPreference })) return callback();
454
468
  const hrElapsed = process.hrtime(start);
455
469
  const elapsed = (hrElapsed[0] * 1e9 + hrElapsed[1]) / 1e6;
456
470
  if (elapsed > timeout) return callback(new MongoError('Timed out waiting for connection'));
457
471
  waitForTopologyConnected(topology, options, callback);
458
- }, 3000); // this is an arbitrary wait time to allow SDAM to transition
472
+ }, 500); // this is an arbitrary wait time to allow SDAM to transition
459
473
  }
460
474
 
461
- // Handle new change events. This method brings together the routes from the callback, event emitter, and promise ways of using ChangeStream.
462
- function processNewChange(args) {
463
- const changeStream = args.changeStream;
464
- const error = args.error;
465
- const change = args.change;
466
- const callback = args.callback;
467
- const eventEmitter = args.eventEmitter || false;
475
+ function processNewChange(changeStream, change, callback) {
476
+ const cursor = changeStream.cursor;
468
477
 
469
- // If the changeStream is closed, then it should not process a change.
470
- if (changeStream.isClosed()) {
471
- // We do not error in the eventEmitter case.
472
- if (eventEmitter) {
473
- return;
474
- }
478
+ if (changeStream.closed) {
479
+ if (callback) callback(new MongoError('ChangeStream is closed'));
480
+ return;
481
+ }
475
482
 
476
- const error = new MongoError('ChangeStream is closed');
477
- return typeof callback === 'function'
478
- ? callback(error, null)
479
- : changeStream.promiseLibrary.reject(error);
483
+ if (change && !change._id) {
484
+ const noResumeTokenError = new Error(
485
+ 'A change stream document has been received that lacks a resume token (_id).'
486
+ );
487
+
488
+ if (!callback) return changeStream.emit('error', noResumeTokenError);
489
+ return callback(noResumeTokenError);
480
490
  }
481
491
 
482
- const cursor = changeStream.cursor;
492
+ // cache the resume token
493
+ cursor.cacheResumeToken(change._id);
494
+
495
+ // wipe the startAtOperationTime if there was one so that there won't be a conflict
496
+ // between resumeToken and startAtOperationTime if we need to reconnect the cursor
497
+ changeStream.options.startAtOperationTime = undefined;
498
+
499
+ // Return the change
500
+ if (!callback) return changeStream.emit('change', change);
501
+ return callback(undefined, change);
502
+ }
503
+
504
+ function processError(changeStream, error, callback) {
483
505
  const topology = changeStream.topology;
484
- const options = changeStream.cursor.options;
506
+ const cursor = changeStream.cursor;
485
507
 
486
- if (error) {
487
- if (isResumableError(error) && !changeStream.attemptingResume) {
488
- changeStream.attemptingResume = true;
508
+ // If the change stream has been closed explictly, do not process error.
509
+ if (changeStream.closed) {
510
+ if (callback) callback(new MongoError('ChangeStream is closed'));
511
+ return;
512
+ }
489
513
 
490
- // stop listening to all events from old cursor
491
- ['data', 'close', 'end', 'error'].forEach(event =>
492
- changeStream.cursor.removeAllListeners(event)
493
- );
514
+ // if the resume succeeds, continue with the new cursor
515
+ function resumeWithCursor(newCursor) {
516
+ changeStream.cursor = newCursor;
517
+ processResumeQueue(changeStream);
518
+ }
494
519
 
495
- // close internal cursor, ignore errors
496
- changeStream.cursor.close();
520
+ // otherwise, raise an error and close the change stream
521
+ function unresumableError(err) {
522
+ if (!callback) {
523
+ changeStream.emit('error', err);
524
+ changeStream.emit('close');
525
+ }
526
+ processResumeQueue(changeStream, err);
527
+ changeStream.closed = true;
528
+ }
497
529
 
498
- // attempt recreating the cursor
499
- if (eventEmitter) {
500
- waitForTopologyConnected(topology, { readPreference: options.readPreference }, err => {
501
- if (err) {
502
- changeStream.emit('error', err);
503
- changeStream.emit('close');
504
- return;
505
- }
506
- changeStream.cursor = createChangeStreamCursor(changeStream, cursor.resumeOptions);
507
- });
530
+ if (cursor && isResumableError(error, maxWireVersion(cursor.server))) {
531
+ changeStream.cursor = undefined;
508
532
 
509
- return;
510
- }
533
+ // stop listening to all events from old cursor
534
+ ['data', 'close', 'end', 'error'].forEach(event => cursor.removeAllListeners(event));
511
535
 
512
- if (callback) {
513
- waitForTopologyConnected(topology, { readPreference: options.readPreference }, err => {
514
- if (err) return callback(err, null);
536
+ // close internal cursor, ignore errors
537
+ cursor.close();
515
538
 
516
- changeStream.cursor = createChangeStreamCursor(changeStream, cursor.resumeOptions);
517
- changeStream.next(callback);
518
- });
539
+ waitForTopologyConnected(topology, { readPreference: cursor.options.readPreference }, err => {
540
+ // if the topology can't reconnect, close the stream
541
+ if (err) return unresumableError(err);
519
542
 
520
- return;
521
- }
543
+ // create a new cursor, preserving the old cursor's options
544
+ const newCursor = createChangeStreamCursor(changeStream, cursor.resumeOptions);
522
545
 
523
- return new Promise((resolve, reject) => {
524
- waitForTopologyConnected(topology, { readPreference: options.readPreference }, err => {
525
- if (err) return reject(err);
526
- resolve();
527
- });
528
- })
529
- .then(
530
- () => (changeStream.cursor = createChangeStreamCursor(changeStream, cursor.resumeOptions))
531
- )
532
- .then(() => changeStream.next());
533
- }
546
+ // attempt to continue in emitter mode
547
+ if (!callback) return resumeWithCursor(newCursor);
534
548
 
535
- if (eventEmitter) return changeStream.emit('error', error);
536
- if (typeof callback === 'function') return callback(error, null);
537
- return changeStream.promiseLibrary.reject(error);
549
+ // attempt to continue in iterator mode
550
+ newCursor.hasNext(err => {
551
+ // if there's an error immediately after resuming, close the stream
552
+ if (err) return unresumableError(err);
553
+ resumeWithCursor(newCursor);
554
+ });
555
+ });
556
+ return;
538
557
  }
539
558
 
540
- changeStream.attemptingResume = false;
541
-
542
- if (change && !change._id) {
543
- const noResumeTokenError = new Error(
544
- 'A change stream document has been received that lacks a resume token (_id).'
545
- );
559
+ if (!callback) return changeStream.emit('error', error);
560
+ return callback(error);
561
+ }
546
562
 
547
- if (eventEmitter) return changeStream.emit('error', noResumeTokenError);
548
- if (typeof callback === 'function') return callback(noResumeTokenError, null);
549
- return changeStream.promiseLibrary.reject(noResumeTokenError);
563
+ /**
564
+ * Safely provides a cursor across resume attempts
565
+ *
566
+ * @param {ChangeStream} changeStream the parent ChangeStream
567
+ * @param {function} callback gets the cursor or error
568
+ * @param {ChangeStreamCursor} [oldCursor] when resuming from an error, carry over options from previous cursor
569
+ */
570
+ function getCursor(changeStream, callback) {
571
+ if (changeStream.isClosed()) {
572
+ callback(new MongoError('ChangeStream is closed.'));
573
+ return;
550
574
  }
551
575
 
552
- // cache the resume token
553
- if (cursor.bufferedCount() === 0 && cursor.cursorState.postBatchResumeToken) {
554
- cursor.resumeToken = cursor.cursorState.postBatchResumeToken;
555
- } else {
556
- cursor.resumeToken = change._id;
576
+ // if a cursor exists and it is open, return it
577
+ if (changeStream.cursor) {
578
+ callback(undefined, changeStream.cursor);
579
+ return;
557
580
  }
558
581
 
559
- // wipe the startAtOperationTime if there was one so that there won't be a conflict
560
- // between resumeToken and startAtOperationTime if we need to reconnect the cursor
561
- changeStream.options.startAtOperationTime = undefined;
582
+ // no cursor, queue callback until topology reconnects
583
+ changeStream[kResumeQueue].push(callback);
584
+ }
562
585
 
563
- // Return the change
564
- if (eventEmitter) return changeStream.emit('change', change);
565
- if (typeof callback === 'function') return callback(error, change);
566
- return changeStream.promiseLibrary.resolve(change);
586
+ /**
587
+ * Drain the resume queue when a new has become available
588
+ *
589
+ * @param {ChangeStream} changeStream the parent ChangeStream
590
+ * @param {ChangeStreamCursor?} changeStream.cursor the new cursor
591
+ * @param {Error} [err] error getting a new cursor
592
+ */
593
+ function processResumeQueue(changeStream, err) {
594
+ while (changeStream[kResumeQueue].length) {
595
+ const request = changeStream[kResumeQueue].pop();
596
+ if (changeStream.isClosed() && !err) {
597
+ request(new MongoError('Change Stream is not open.'));
598
+ return;
599
+ }
600
+ request(err, changeStream.cursor);
601
+ }
567
602
  }
568
603
 
569
604
  /**
@@ -198,6 +198,10 @@ class ConnectionPool extends EventEmitter {
198
198
  return this[kConnections].length;
199
199
  }
200
200
 
201
+ get waitQueueSize() {
202
+ return this[kWaitQueue].length;
203
+ }
204
+
201
205
  /**
202
206
  * Check a connection out of this pool. The connection will continue to be tracked, but no reference to it
203
207
  * will be held by the pool. This means that if a connection is checked out it MUST be checked back in or
@@ -295,7 +299,7 @@ class ConnectionPool extends EventEmitter {
295
299
  this[kCancellationToken].emit('cancel');
296
300
 
297
301
  // drain the wait queue
298
- while (this[kWaitQueue].length) {
302
+ while (this.waitQueueSize) {
299
303
  const waitQueueMember = this[kWaitQueue].pop();
300
304
  clearTimeout(waitQueueMember.timer);
301
305
  if (!waitQueueMember[kCancelled]) {
@@ -449,13 +453,17 @@ function processWaitQueue(pool) {
449
453
  return;
450
454
  }
451
455
 
452
- while (pool[kWaitQueue].length && pool.availableConnectionCount) {
456
+ while (pool.waitQueueSize) {
453
457
  const waitQueueMember = pool[kWaitQueue].peekFront();
454
458
  if (waitQueueMember[kCancelled]) {
455
459
  pool[kWaitQueue].shift();
456
460
  continue;
457
461
  }
458
462
 
463
+ if (!pool.availableConnectionCount) {
464
+ break;
465
+ }
466
+
459
467
  const connection = pool[kConnections].shift();
460
468
  const isStale = connectionIsStale(pool, connection);
461
469
  const isIdle = connectionIsIdle(pool, connection);
@@ -472,7 +480,7 @@ function processWaitQueue(pool) {
472
480
  }
473
481
 
474
482
  const maxPoolSize = pool.options.maxPoolSize;
475
- if (pool[kWaitQueue].length && (maxPoolSize <= 0 || pool.totalConnectionCount < maxPoolSize)) {
483
+ if (pool.waitQueueSize && (maxPoolSize <= 0 || pool.totalConnectionCount < maxPoolSize)) {
476
484
  createConnection(pool, (err, connection) => {
477
485
  const waitQueueMember = pool[kWaitQueue].shift();
478
486
  if (waitQueueMember == null) {
package/lib/collection.js CHANGED
@@ -310,7 +310,7 @@ const DEPRECATED_FIND_OPTIONS = ['maxScan', 'fields', 'snapshot'];
310
310
  * @param {(ReadPreference|string)} [options.readPreference] The preferred read preference (ReadPreference.PRIMARY, ReadPreference.PRIMARY_PREFERRED, ReadPreference.SECONDARY, ReadPreference.SECONDARY_PREFERRED, ReadPreference.NEAREST).
311
311
  * @param {boolean} [options.partial=false] Specify if the cursor should return partial results when querying against a sharded system
312
312
  * @param {number} [options.maxTimeMS] Number of milliseconds to wait before aborting the query.
313
- * @param {number} [options.maxAwaitTimeMS] The maximum amount of time for the server to wait on new documents to satisfy a tailable cursor query. Requires `taiable` and `awaitData` to be true
313
+ * @param {number} [options.maxAwaitTimeMS] The maximum amount of time for the server to wait on new documents to satisfy a tailable cursor query. Requires `tailable` and `awaitData` to be true
314
314
  * @param {boolean} [options.noCursorTimeout] The server normally times out idle cursors after an inactivity period (10 minutes) to prevent excess memory use. Set this option to prevent that.
315
315
  * @param {object} [options.collation] Specify collation (MongoDB 3.4 or higher) settings for update operation (see 3.4 documentation for available fields).
316
316
  * @param {ClientSession} [options.session] optional session to use for this operation