mongodb 3.5.7 → 3.5.11

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