mongodb 3.5.5 → 3.5.9

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,67 @@
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.9"></a>
6
+ ## [3.5.9](https://github.com/mongodb/node-mongodb-native/compare/v3.5.8...v3.5.9) (2020-06-12)
7
+
8
+
9
+ ### Bug Fixes
10
+
11
+ * don't try to calculate sMax if there are no viable servers ([be51347](https://github.com/mongodb/node-mongodb-native/commit/be51347))
12
+ * use async interruptable interval for server monitoring ([1f855a4](https://github.com/mongodb/node-mongodb-native/commit/1f855a4))
13
+ * use duration of handshake if no previous roundTripTime exists ([ddfa41b](https://github.com/mongodb/node-mongodb-native/commit/ddfa41b))
14
+
15
+
16
+ ### Features
17
+
18
+ * introduce an interruptable async interval timer ([9e12cd5](https://github.com/mongodb/node-mongodb-native/commit/9e12cd5))
19
+
20
+
21
+
22
+ <a name="3.5.8"></a>
23
+ ## [3.5.8](https://github.com/mongodb/node-mongodb-native/compare/v3.5.7...v3.5.8) (2020-05-28)
24
+
25
+
26
+ ### Bug Fixes
27
+
28
+ * always clear cancelled wait queue members during processing ([0394f9d](https://github.com/mongodb/node-mongodb-native/commit/0394f9d))
29
+ * always include `writeErrors` on a `BulkWriteError` instance ([58b4f94](https://github.com/mongodb/node-mongodb-native/commit/58b4f94))
30
+ * ensure implicit sessions are ended consistently ([5c6fda1](https://github.com/mongodb/node-mongodb-native/commit/5c6fda1))
31
+ * filter servers before applying reducers ([4faf9f5](https://github.com/mongodb/node-mongodb-native/commit/4faf9f5))
32
+ * unordered bulk write should attempt to execute all batches ([6cee96b](https://github.com/mongodb/node-mongodb-native/commit/6cee96b))
33
+ * **ChangeStream:** should resume from errors when iterating ([5ecf18e](https://github.com/mongodb/node-mongodb-native/commit/5ecf18e))
34
+ * 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))
35
+ * **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)
36
+
37
+
38
+
39
+ <a name="3.5.7"></a>
40
+ ## [3.5.7](https://github.com/mongodb/node-mongodb-native/compare/v3.5.6...v3.5.7) (2020-04-29)
41
+
42
+
43
+ ### Bug Fixes
44
+
45
+ * limit growth of server sessions through lazy acquisition ([3d05a6d](https://github.com/mongodb/node-mongodb-native/commit/3d05a6d))
46
+ * remove circular dependency warnings on node 14 ([56a1b8a](https://github.com/mongodb/node-mongodb-native/commit/56a1b8a))
47
+
48
+
49
+
50
+ <a name="3.5.6"></a>
51
+ ## [3.5.6](https://github.com/mongodb/node-mongodb-native/compare/v3.5.5...v3.5.6) (2020-04-14)
52
+
53
+
54
+ ### Bug Fixes
55
+
56
+ * always return empty array for selection on unknown topology ([f9e786a](https://github.com/mongodb/node-mongodb-native/commit/f9e786a))
57
+ * createCollection only uses listCollections in strict mode ([d368f12](https://github.com/mongodb/node-mongodb-native/commit/d368f12))
58
+ * don't throw if `withTransaction()` callback rejects with a null reason ([153646c](https://github.com/mongodb/node-mongodb-native/commit/153646c))
59
+ * only mark server session dirty if the client session is alive ([611be8d](https://github.com/mongodb/node-mongodb-native/commit/611be8d))
60
+ * polyfill for util.promisify ([1c4cf6c](https://github.com/mongodb/node-mongodb-native/commit/1c4cf6c))
61
+ * single `readPreferenceTags` should be parsed as an array ([a50611b](https://github.com/mongodb/node-mongodb-native/commit/a50611b))
62
+ * **cursor:** transforms should only be applied once to documents ([704f30a](https://github.com/mongodb/node-mongodb-native/commit/704f30a))
63
+
64
+
65
+
5
66
  <a name="3.5.5"></a>
6
67
  ## [3.5.5](https://github.com/mongodb/node-mongodb-native/compare/v3.5.4...v3.5.5) (2020-03-11)
7
68
 
@@ -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,19 @@
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;
11
+ const now = require('./utils').now;
12
+ const calculateDurationInMs = require('./utils').calculateDurationInMs;
9
13
  const AggregateOperation = require('./operations/aggregate');
10
14
 
15
+ const kResumeQueue = Symbol('resumeQueue');
16
+
11
17
  const CHANGE_STREAM_OPTIONS = ['resumeAfter', 'startAfter', 'startAtOperationTime', 'fullDocument'];
12
18
  const CURSOR_OPTIONS = ['batchSize', 'maxAwaitTimeMS', 'collation', 'readPreference'].concat(
13
19
  CHANGE_STREAM_OPTIONS
@@ -90,15 +96,17 @@ class ChangeStream extends EventEmitter {
90
96
  this.options.readPreference = parent.s.readPreference;
91
97
  }
92
98
 
99
+ this[kResumeQueue] = new Denque();
100
+
93
101
  // Create contained Change Stream cursor
94
102
  this.cursor = createChangeStreamCursor(this, options);
95
103
 
104
+ this.closed = false;
105
+
96
106
  // Listen for any `change` listeners being added to ChangeStream
97
107
  this.on('newListener', eventName => {
98
108
  if (eventName === 'change' && this.cursor && this.listenerCount('change') === 0) {
99
- this.cursor.on('data', change =>
100
- processNewChange({ changeStream: this, change, eventEmitter: true })
101
- );
109
+ this.cursor.on('data', change => processNewChange(this, change));
102
110
  }
103
111
  });
104
112
 
@@ -124,10 +132,15 @@ class ChangeStream extends EventEmitter {
124
132
  * @function ChangeStream.prototype.hasNext
125
133
  * @param {ChangeStream~resultCallback} [callback] The result callback.
126
134
  * @throws {MongoError}
127
- * @return {Promise} returns Promise if no callback passed
135
+ * @returns {Promise|void} returns Promise if no callback passed
128
136
  */
129
137
  hasNext(callback) {
130
- return this.cursor.hasNext(callback);
138
+ return maybePromise(this.parent, callback, cb => {
139
+ getCursor(this, (err, cursor) => {
140
+ if (err) return cb(err); // failed to resume, raise an error
141
+ cursor.hasNext(cb);
142
+ });
143
+ });
131
144
  }
132
145
 
133
146
  /**
@@ -135,31 +148,32 @@ class ChangeStream extends EventEmitter {
135
148
  * @function ChangeStream.prototype.next
136
149
  * @param {ChangeStream~resultCallback} [callback] The result callback.
137
150
  * @throws {MongoError}
138
- * @return {Promise} returns Promise if no callback passed
151
+ * @returns {Promise|void} returns Promise if no callback passed
139
152
  */
140
153
  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 }));
154
+ return maybePromise(this.parent, callback, cb => {
155
+ getCursor(this, (err, cursor) => {
156
+ if (err) return cb(err); // failed to resume, raise an error
157
+ cursor.next((error, change) => {
158
+ if (error) {
159
+ this[kResumeQueue].push(() => this.next(cb));
160
+ processError(this, error, cb);
161
+ return;
162
+ }
163
+ processNewChange(this, change, cb);
164
+ });
165
+ });
166
+ });
151
167
  }
152
168
 
153
169
  /**
154
- * Is the cursor closed
170
+ * Is the change stream closed
155
171
  * @method ChangeStream.prototype.isClosed
172
+ * @param {boolean} [checkCursor=true] also check if the underlying cursor is closed
156
173
  * @return {boolean}
157
174
  */
158
175
  isClosed() {
159
- if (this.cursor) {
160
- return this.cursor.isClosed();
161
- }
162
- return true;
176
+ return this.closed || (this.cursor && this.cursor.isClosed());
163
177
  }
164
178
 
165
179
  /**
@@ -169,31 +183,22 @@ class ChangeStream extends EventEmitter {
169
183
  * @return {Promise} returns Promise if no callback passed
170
184
  */
171
185
  close(callback) {
172
- if (!this.cursor) {
173
- if (callback) return callback();
174
- return this.promiseLibrary.resolve();
175
- }
186
+ return maybePromise(this.parent, callback, cb => {
187
+ if (this.closed) return cb();
176
188
 
177
- // Tidy up the existing cursor
178
- const cursor = this.cursor;
189
+ // flag the change stream as explicitly closed
190
+ this.closed = true;
179
191
 
180
- if (callback) {
181
- return cursor.close(err => {
182
- ['data', 'close', 'end', 'error'].forEach(event => cursor.removeAllListeners(event));
183
- delete this.cursor;
192
+ if (!this.cursor) return cb();
184
193
 
185
- return callback(err);
186
- });
187
- }
194
+ // Tidy up the existing cursor
195
+ const cursor = this.cursor;
188
196
 
189
- const PromiseCtor = this.promiseLibrary || Promise;
190
- return new PromiseCtor((resolve, reject) => {
191
- cursor.close(err => {
197
+ return cursor.close(err => {
192
198
  ['data', 'close', 'end', 'error'].forEach(event => cursor.removeAllListeners(event));
193
- delete this.cursor;
199
+ this.cursor = undefined;
194
200
 
195
- if (err) return reject(err);
196
- resolve();
201
+ return cb(err);
197
202
  });
198
203
  });
199
204
  }
@@ -288,7 +293,9 @@ class ChangeStreamCursor extends Cursor {
288
293
  ['resumeAfter', 'startAfter', 'startAtOperationTime'].forEach(key => delete result[key]);
289
294
 
290
295
  if (this.resumeToken) {
291
- result.resumeAfter = this.resumeToken;
296
+ const resumeKey =
297
+ this.options.startAfter && !this.hasReceived ? 'startAfter' : 'resumeAfter';
298
+ result[resumeKey] = this.resumeToken;
292
299
  } else if (this.startAtOperationTime && maxWireVersion(this.server) >= 7) {
293
300
  result.startAtOperationTime = this.startAtOperationTime;
294
301
  }
@@ -297,10 +304,30 @@ class ChangeStreamCursor extends Cursor {
297
304
  return result;
298
305
  }
299
306
 
307
+ cacheResumeToken(resumeToken) {
308
+ if (this.bufferedCount() === 0 && this.cursorState.postBatchResumeToken) {
309
+ this.resumeToken = this.cursorState.postBatchResumeToken;
310
+ } else {
311
+ this.resumeToken = resumeToken;
312
+ }
313
+ this.hasReceived = true;
314
+ }
315
+
316
+ _processBatch(batchName, response) {
317
+ const cursor = response.cursor;
318
+ if (cursor.postBatchResumeToken) {
319
+ this.cursorState.postBatchResumeToken = cursor.postBatchResumeToken;
320
+
321
+ if (cursor[batchName].length === 0) {
322
+ this.resumeToken = cursor.postBatchResumeToken;
323
+ }
324
+ }
325
+ }
326
+
300
327
  _initializeCursor(callback) {
301
328
  super._initializeCursor((err, result) => {
302
329
  if (err) {
303
- callback(err, null);
330
+ callback(err);
304
331
  return;
305
332
  }
306
333
 
@@ -315,15 +342,9 @@ class ChangeStreamCursor extends Cursor {
315
342
  this.startAtOperationTime = response.operationTime;
316
343
  }
317
344
 
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
- }
345
+ this._processBatch('firstBatch', response);
326
346
 
347
+ this.emit('init', result);
327
348
  this.emit('response');
328
349
  callback(err, result);
329
350
  });
@@ -332,19 +353,13 @@ class ChangeStreamCursor extends Cursor {
332
353
  _getMore(callback) {
333
354
  super._getMore((err, response) => {
334
355
  if (err) {
335
- callback(err, null);
356
+ callback(err);
336
357
  return;
337
358
  }
338
359
 
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
- }
360
+ this._processBatch('nextBatch', response);
347
361
 
362
+ this.emit('more', response);
348
363
  this.emit('response');
349
364
  callback(err, response);
350
365
  });
@@ -367,6 +382,7 @@ function createChangeStreamCursor(self, options) {
367
382
 
368
383
  const pipeline = [{ $changeStream: changeStreamStageOptions }].concat(self.pipeline);
369
384
  const cursorOptions = applyKnownOptions({}, options, CURSOR_OPTIONS);
385
+
370
386
  const changeStreamCursor = new ChangeStreamCursor(
371
387
  self.topology,
372
388
  new AggregateOperation(self.parent, pipeline, options),
@@ -385,7 +401,7 @@ function createChangeStreamCursor(self, options) {
385
401
  */
386
402
  if (self.listenerCount('change') > 0) {
387
403
  changeStreamCursor.on('data', function(change) {
388
- processNewChange({ changeStream: self, change, eventEmitter: true });
404
+ processNewChange(self, change);
389
405
  });
390
406
  }
391
407
 
@@ -417,7 +433,7 @@ function createChangeStreamCursor(self, options) {
417
433
  * @type {Error}
418
434
  */
419
435
  changeStreamCursor.on('error', function(error) {
420
- processNewChange({ changeStream: self, error, eventEmitter: true });
436
+ processError(self, error);
421
437
  });
422
438
 
423
439
  if (self.pipeDestinations) {
@@ -445,125 +461,152 @@ function applyKnownOptions(target, source, optionNames) {
445
461
  const SELECTION_TIMEOUT = 30000;
446
462
  function waitForTopologyConnected(topology, options, callback) {
447
463
  setTimeout(() => {
448
- if (options && options.start == null) options.start = process.hrtime();
449
- const start = options.start || process.hrtime();
464
+ if (options && options.start == null) {
465
+ options.start = now();
466
+ }
467
+
468
+ const start = options.start || now();
450
469
  const timeout = options.timeout || SELECTION_TIMEOUT;
451
470
  const readPreference = options.readPreference;
471
+ if (topology.isConnected({ readPreference })) {
472
+ return callback();
473
+ }
474
+
475
+ if (calculateDurationInMs(start) > timeout) {
476
+ return callback(new MongoError('Timed out waiting for connection'));
477
+ }
452
478
 
453
- if (topology.isConnected({ readPreference })) return callback(null, null);
454
- const hrElapsed = process.hrtime(start);
455
- const elapsed = (hrElapsed[0] * 1e9 + hrElapsed[1]) / 1e6;
456
- if (elapsed > timeout) return callback(new MongoError('Timed out waiting for connection'));
457
479
  waitForTopologyConnected(topology, options, callback);
458
- }, 3000); // this is an arbitrary wait time to allow SDAM to transition
480
+ }, 500); // this is an arbitrary wait time to allow SDAM to transition
459
481
  }
460
482
 
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;
483
+ function processNewChange(changeStream, change, callback) {
484
+ const cursor = changeStream.cursor;
468
485
 
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
- }
486
+ if (changeStream.closed) {
487
+ if (callback) callback(new MongoError('ChangeStream is closed'));
488
+ return;
489
+ }
490
+
491
+ if (change && !change._id) {
492
+ const noResumeTokenError = new Error(
493
+ 'A change stream document has been received that lacks a resume token (_id).'
494
+ );
475
495
 
476
- const error = new MongoError('ChangeStream is closed');
477
- return typeof callback === 'function'
478
- ? callback(error, null)
479
- : changeStream.promiseLibrary.reject(error);
496
+ if (!callback) return changeStream.emit('error', noResumeTokenError);
497
+ return callback(noResumeTokenError);
480
498
  }
481
499
 
482
- const cursor = changeStream.cursor;
500
+ // cache the resume token
501
+ cursor.cacheResumeToken(change._id);
502
+
503
+ // wipe the startAtOperationTime if there was one so that there won't be a conflict
504
+ // between resumeToken and startAtOperationTime if we need to reconnect the cursor
505
+ changeStream.options.startAtOperationTime = undefined;
506
+
507
+ // Return the change
508
+ if (!callback) return changeStream.emit('change', change);
509
+ return callback(undefined, change);
510
+ }
511
+
512
+ function processError(changeStream, error, callback) {
483
513
  const topology = changeStream.topology;
484
- const options = changeStream.cursor.options;
514
+ const cursor = changeStream.cursor;
485
515
 
486
- if (error) {
487
- if (isResumableError(error) && !changeStream.attemptingResume) {
488
- changeStream.attemptingResume = true;
516
+ // If the change stream has been closed explictly, do not process error.
517
+ if (changeStream.closed) {
518
+ if (callback) callback(new MongoError('ChangeStream is closed'));
519
+ return;
520
+ }
489
521
 
490
- // stop listening to all events from old cursor
491
- ['data', 'close', 'end', 'error'].forEach(event =>
492
- changeStream.cursor.removeAllListeners(event)
493
- );
522
+ // if the resume succeeds, continue with the new cursor
523
+ function resumeWithCursor(newCursor) {
524
+ changeStream.cursor = newCursor;
525
+ processResumeQueue(changeStream);
526
+ }
494
527
 
495
- // close internal cursor, ignore errors
496
- changeStream.cursor.close();
528
+ // otherwise, raise an error and close the change stream
529
+ function unresumableError(err) {
530
+ if (!callback) {
531
+ changeStream.emit('error', err);
532
+ changeStream.emit('close');
533
+ }
534
+ processResumeQueue(changeStream, err);
535
+ changeStream.closed = true;
536
+ }
497
537
 
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
- });
538
+ if (cursor && isResumableError(error, maxWireVersion(cursor.server))) {
539
+ changeStream.cursor = undefined;
508
540
 
509
- return;
510
- }
541
+ // stop listening to all events from old cursor
542
+ ['data', 'close', 'end', 'error'].forEach(event => cursor.removeAllListeners(event));
511
543
 
512
- if (callback) {
513
- waitForTopologyConnected(topology, { readPreference: options.readPreference }, err => {
514
- if (err) return callback(err, null);
544
+ // close internal cursor, ignore errors
545
+ cursor.close();
515
546
 
516
- changeStream.cursor = createChangeStreamCursor(changeStream, cursor.resumeOptions);
517
- changeStream.next(callback);
518
- });
547
+ waitForTopologyConnected(topology, { readPreference: cursor.options.readPreference }, err => {
548
+ // if the topology can't reconnect, close the stream
549
+ if (err) return unresumableError(err);
519
550
 
520
- return;
521
- }
551
+ // create a new cursor, preserving the old cursor's options
552
+ const newCursor = createChangeStreamCursor(changeStream, cursor.resumeOptions);
522
553
 
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
- }
554
+ // attempt to continue in emitter mode
555
+ if (!callback) return resumeWithCursor(newCursor);
534
556
 
535
- if (eventEmitter) return changeStream.emit('error', error);
536
- if (typeof callback === 'function') return callback(error, null);
537
- return changeStream.promiseLibrary.reject(error);
557
+ // attempt to continue in iterator mode
558
+ newCursor.hasNext(err => {
559
+ // if there's an error immediately after resuming, close the stream
560
+ if (err) return unresumableError(err);
561
+ resumeWithCursor(newCursor);
562
+ });
563
+ });
564
+ return;
538
565
  }
539
566
 
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
- );
567
+ if (!callback) return changeStream.emit('error', error);
568
+ return callback(error);
569
+ }
546
570
 
547
- if (eventEmitter) return changeStream.emit('error', noResumeTokenError);
548
- if (typeof callback === 'function') return callback(noResumeTokenError, null);
549
- return changeStream.promiseLibrary.reject(noResumeTokenError);
571
+ /**
572
+ * Safely provides a cursor across resume attempts
573
+ *
574
+ * @param {ChangeStream} changeStream the parent ChangeStream
575
+ * @param {function} callback gets the cursor or error
576
+ * @param {ChangeStreamCursor} [oldCursor] when resuming from an error, carry over options from previous cursor
577
+ */
578
+ function getCursor(changeStream, callback) {
579
+ if (changeStream.isClosed()) {
580
+ callback(new MongoError('ChangeStream is closed.'));
581
+ return;
550
582
  }
551
583
 
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;
584
+ // if a cursor exists and it is open, return it
585
+ if (changeStream.cursor) {
586
+ callback(undefined, changeStream.cursor);
587
+ return;
557
588
  }
558
589
 
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;
590
+ // no cursor, queue callback until topology reconnects
591
+ changeStream[kResumeQueue].push(callback);
592
+ }
562
593
 
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);
594
+ /**
595
+ * Drain the resume queue when a new has become available
596
+ *
597
+ * @param {ChangeStream} changeStream the parent ChangeStream
598
+ * @param {ChangeStreamCursor?} changeStream.cursor the new cursor
599
+ * @param {Error} [err] error getting a new cursor
600
+ */
601
+ function processResumeQueue(changeStream, err) {
602
+ while (changeStream[kResumeQueue].length) {
603
+ const request = changeStream[kResumeQueue].pop();
604
+ if (changeStream.isClosed() && !err) {
605
+ request(new MongoError('Change Stream is not open.'));
606
+ return;
607
+ }
608
+ request(err, changeStream.cursor);
609
+ }
567
610
  }
568
611
 
569
612
  /**
@@ -11,6 +11,8 @@ const wp = require('../core/wireprotocol');
11
11
  const apm = require('../core/connection/apm');
12
12
  const updateSessionFromResponse = require('../core/sessions').updateSessionFromResponse;
13
13
  const uuidV4 = require('../core/utils').uuidV4;
14
+ const now = require('../utils').now;
15
+ const calculateDurationInMs = require('../utils').calculateDurationInMs;
14
16
 
15
17
  const kStream = Symbol('stream');
16
18
  const kQueue = Symbol('queue');
@@ -37,7 +39,7 @@ class Connection extends EventEmitter {
37
39
 
38
40
  this[kDescription] = new StreamDescription(this.address, options);
39
41
  this[kGeneration] = options.generation;
40
- this[kLastUseTime] = Date.now();
42
+ this[kLastUseTime] = now();
41
43
 
42
44
  // retain a reference to an `AutoEncrypter` if present
43
45
  if (options.autoEncrypter) {
@@ -108,7 +110,7 @@ class Connection extends EventEmitter {
108
110
  }
109
111
 
110
112
  get idleTime() {
111
- return Date.now() - this[kLastUseTime];
113
+ return calculateDurationInMs(this[kLastUseTime]);
112
114
  }
113
115
 
114
116
  get clusterTime() {
@@ -120,7 +122,7 @@ class Connection extends EventEmitter {
120
122
  }
121
123
 
122
124
  markAvailable() {
123
- this[kLastUseTime] = Date.now();
125
+ this[kLastUseTime] = now();
124
126
  }
125
127
 
126
128
  destroy(options, callback) {
@@ -326,7 +328,7 @@ function write(command, options, callback) {
326
328
  if (this.monitorCommands) {
327
329
  this.emit('commandStarted', new apm.CommandStartedEvent(this, command));
328
330
 
329
- operationDescription.started = process.hrtime();
331
+ operationDescription.started = now();
330
332
  operationDescription.cb = (err, reply) => {
331
333
  if (err) {
332
334
  this.emit(