@@ -46,7 +46,7 @@ class ChangeStream extends EventEmitter {
46
46
47
47
if ( changeDomain instanceof Collection ) {
48
48
this . type = CHANGE_DOMAIN_TYPES . COLLECTION ;
49
- this . serverConfig = changeDomain . s . db . serverConfig ;
49
+ this . topology = changeDomain . s . db . serverConfig ;
50
50
51
51
this . namespace = {
52
52
collection : changeDomain . collectionName ,
@@ -58,12 +58,12 @@ class ChangeStream extends EventEmitter {
58
58
this . type = CHANGE_DOMAIN_TYPES . DATABASE ;
59
59
this . namespace = { collection : '' , database : changeDomain . databaseName } ;
60
60
this . cursorNamespace = this . namespace . database ;
61
- this . serverConfig = changeDomain . serverConfig ;
61
+ this . topology = changeDomain . serverConfig ;
62
62
} else if ( changeDomain instanceof MongoClient ) {
63
63
this . type = CHANGE_DOMAIN_TYPES . CLUSTER ;
64
64
this . namespace = { collection : '' , database : 'admin' } ;
65
65
this . cursorNamespace = this . namespace . database ;
66
- this . serverConfig = changeDomain . topology ;
66
+ this . topology = changeDomain . topology ;
67
67
} else {
68
68
throw new TypeError (
69
69
'changeDomain provided to ChangeStream constructor is not an instance of Collection, Db, or MongoClient'
@@ -76,9 +76,9 @@ class ChangeStream extends EventEmitter {
76
76
}
77
77
78
78
// We need to get the operationTime as early as possible
79
- const isMaster = this . serverConfig . lastIsMaster ( ) ;
79
+ const isMaster = this . topology . lastIsMaster ( ) ;
80
80
if ( ! isMaster ) {
81
- throw new MongoError ( 'ServerConfig does not have an ismaster yet.' ) ;
81
+ throw new MongoError ( 'Topology does not have an ismaster yet.' ) ;
82
82
}
83
83
84
84
this . operationTime = isMaster . operationTime ;
@@ -89,7 +89,9 @@ class ChangeStream extends EventEmitter {
89
89
// Listen for any `change` listeners being added to ChangeStream
90
90
this . on ( 'newListener' , eventName => {
91
91
if ( eventName === 'change' && this . cursor && this . cursor . listenerCount ( 'change' ) === 0 ) {
92
- this . cursor . on ( 'data' , change => processNewChange ( this , null , change ) ) ;
92
+ this . cursor . on ( 'data' , change =>
93
+ processNewChange ( { changeStream : this , change, eventEmitter : true } )
94
+ ) ;
93
95
}
94
96
} ) ;
95
97
@@ -125,14 +127,11 @@ class ChangeStream extends EventEmitter {
125
127
if ( callback ) return callback ( new Error ( 'Change Stream is not open.' ) , null ) ;
126
128
return self . promiseLibrary . reject ( new Error ( 'Change Stream is not open.' ) ) ;
127
129
}
130
+
128
131
return this . cursor
129
132
. next ( )
130
- . then ( function ( change ) {
131
- return processNewChange ( self , null , change , callback ) ;
132
- } )
133
- . catch ( function ( err ) {
134
- return processNewChange ( self , err , null , callback ) ;
135
- } ) ;
133
+ . then ( change => processNewChange ( { changeStream : self , change, callback } ) )
134
+ . catch ( error => processNewChange ( { changeStream : self , error, callback } ) ) ;
136
135
}
137
136
138
137
/**
@@ -230,14 +229,16 @@ var createChangeStreamCursor = function(self) {
230
229
var changeStreamCursor = buildChangeStreamAggregationCommand ( self ) ;
231
230
232
231
/**
233
- * Fired for each new matching change in the specified namespace. Attaching a `change` event listener to a Change Stream will switch the stream into flowing mode. Data will then be passed as soon as it is available.
232
+ * Fired for each new matching change in the specified namespace. Attaching a `change`
233
+ * event listener to a Change Stream will switch the stream into flowing mode. Data will
234
+ * then be passed as soon as it is available.
234
235
*
235
236
* @event ChangeStream#change
236
237
* @type {object }
237
238
*/
238
239
if ( self . listenerCount ( 'change' ) > 0 ) {
239
240
changeStreamCursor . on ( 'data' , function ( change ) {
240
- processNewChange ( self , null , change ) ;
241
+ processNewChange ( { changeStream : self , change , eventEmitter : true } ) ;
241
242
} ) ;
242
243
}
243
244
@@ -268,7 +269,7 @@ var createChangeStreamCursor = function(self) {
268
269
* @type {Error }
269
270
*/
270
271
changeStreamCursor . on ( 'error' , function ( error ) {
271
- self . emit ( 'error' , error ) ;
272
+ processNewChange ( { changeStream : self , error, eventEmitter : true } ) ;
272
273
} ) ;
273
274
274
275
if ( self . pipeDestinations ) {
@@ -286,14 +287,14 @@ function getResumeToken(self) {
286
287
}
287
288
288
289
function getStartAtOperationTime ( self ) {
289
- const isMaster = self . serverConfig . lastIsMaster ( ) || { } ;
290
+ const isMaster = self . topology . lastIsMaster ( ) || { } ;
290
291
return (
291
292
isMaster . maxWireVersion && isMaster . maxWireVersion >= 7 && self . options . startAtOperationTime
292
293
) ;
293
294
}
294
295
295
296
var buildChangeStreamAggregationCommand = function ( self ) {
296
- const serverConfig = self . serverConfig ;
297
+ const topology = self . topology ;
297
298
const namespace = self . namespace ;
298
299
const pipeline = self . pipeline ;
299
300
const options = self . options ;
@@ -339,62 +340,110 @@ var buildChangeStreamAggregationCommand = function(self) {
339
340
} ;
340
341
341
342
// Create and return the cursor
342
- return serverConfig . cursor ( cursorNamespace , command , cursorOptions ) ;
343
+ return topology . cursor ( cursorNamespace , command , cursorOptions ) ;
343
344
} ;
344
345
346
+ // This method performs a basic server selection loop, satisfying the requirements of
347
+ // ChangeStream resumability until the new SDAM layer can be used.
348
+ const SELECTION_TIMEOUT = 30000 ;
349
+ function waitForTopologyConnected ( topology , options , callback ) {
350
+ setTimeout ( ( ) => {
351
+ if ( options && options . start == null ) options . start = process . hrtime ( ) ;
352
+ const start = options . start || process . hrtime ( ) ;
353
+ const timeout = options . timeout || SELECTION_TIMEOUT ;
354
+ const readPreference = options . readPreference ;
355
+
356
+ if ( topology . isConnected ( { readPreference } ) ) return callback ( null , null ) ;
357
+ const hrElapsed = process . hrtime ( start ) ;
358
+ const elapsed = ( hrElapsed [ 0 ] * 1e9 + hrElapsed [ 1 ] ) / 1e6 ;
359
+ if ( elapsed > timeout ) return callback ( new MongoError ( 'Timed out waiting for connection' ) ) ;
360
+ waitForTopologyConnected ( topology , options , callback ) ;
361
+ } , 3000 ) ; // this is an arbitrary wait time to allow SDAM to transition
362
+ }
363
+
345
364
// Handle new change events. This method brings together the routes from the callback, event emitter, and promise ways of using ChangeStream.
346
- var processNewChange = function ( self , err , change , callback ) {
347
- // Handle errors
348
- if ( err ) {
349
- // Handle resumable MongoNetworkErrors
350
- if ( isResumableError ( err ) && ! self . attemptingResume ) {
351
- self . attemptingResume = true ;
352
-
353
- if ( ! ( getResumeToken ( self ) || getStartAtOperationTime ( self ) ) ) {
354
- const startAtOperationTime = self . cursor . cursorState . operationTime ;
355
- self . options = Object . assign ( { startAtOperationTime } , self . options ) ;
365
+ function processNewChange ( args ) {
366
+ const changeStream = args . changeStream ;
367
+ const error = args . error ;
368
+ const change = args . change ;
369
+ const callback = args . callback ;
370
+ const eventEmitter = args . eventEmitter || false ;
371
+ const topology = changeStream . topology ;
372
+ const options = changeStream . cursor . options ;
373
+
374
+ if ( error ) {
375
+ if ( isResumableError ( error ) && ! changeStream . attemptingResume ) {
376
+ changeStream . attemptingResume = true ;
377
+
378
+ if ( ! ( getResumeToken ( changeStream ) || getStartAtOperationTime ( changeStream ) ) ) {
379
+ const startAtOperationTime = changeStream . cursor . cursorState . operationTime ;
380
+ changeStream . options = Object . assign ( { startAtOperationTime } , changeStream . options ) ;
356
381
}
357
382
358
- if ( callback ) {
359
- return self . cursor . close ( function ( closeErr ) {
360
- if ( closeErr ) {
361
- return callback ( err , null ) ;
362
- }
383
+ // stop listening to all events from old cursor
384
+ [ 'data' , 'close' , 'end' , 'error' ] . forEach ( event =>
385
+ changeStream . cursor . removeAllListeners ( event )
386
+ ) ;
387
+
388
+ // close internal cursor, ignore errors
389
+ changeStream . cursor . close ( ) ;
390
+
391
+ // attempt recreating the cursor
392
+ if ( eventEmitter ) {
393
+ waitForTopologyConnected ( topology , { readPreference : options . readPreference } , err => {
394
+ if ( err ) return changeStream . emit ( 'error' , err ) ;
395
+ changeStream . cursor = createChangeStreamCursor ( changeStream ) ;
396
+ } ) ;
397
+
398
+ return ;
399
+ }
363
400
364
- self . cursor = createChangeStreamCursor ( self ) ;
401
+ if ( callback ) {
402
+ waitForTopologyConnected ( topology , { readPreference : options . readPreference } , err => {
403
+ if ( err ) return callback ( err , null ) ;
365
404
366
- return self . next ( callback ) ;
405
+ changeStream . cursor = createChangeStreamCursor ( changeStream ) ;
406
+ changeStream . next ( callback ) ;
367
407
} ) ;
408
+
409
+ return ;
368
410
}
369
411
370
- return self . cursor
371
- . close ( )
372
- . then ( ( ) => ( self . cursor = createChangeStreamCursor ( self ) ) )
373
- . then ( ( ) => self . next ( ) ) ;
412
+ return new Promise ( ( resolve , reject ) => {
413
+ waitForTopologyConnected ( topology , { readPreference : options . readPreference } , err => {
414
+ if ( err ) return reject ( err ) ;
415
+ resolve ( ) ;
416
+ } ) ;
417
+ } )
418
+ . then ( ( ) => ( changeStream . cursor = createChangeStreamCursor ( changeStream ) ) )
419
+ . then ( ( ) => changeStream . next ( ) ) ;
374
420
}
375
421
376
- if ( typeof callback === 'function' ) return callback ( err , null ) ;
377
- if ( self . listenerCount ( 'error' ) ) return self . emit ( ' error' , err ) ;
378
- return self . promiseLibrary . reject ( err ) ;
422
+ if ( eventEmitter ) return changeStream . emit ( 'error' , error ) ;
423
+ if ( typeof callback === 'function' ) return callback ( error , null ) ;
424
+ return changeStream . promiseLibrary . reject ( error ) ;
379
425
}
380
- self . attemptingResume = false ;
426
+
427
+ changeStream . attemptingResume = false ;
381
428
382
429
// Cache the resume token if it is present. If it is not present return an error.
383
430
if ( ! change || ! change . _id ) {
384
431
var noResumeTokenError = new Error (
385
432
'A change stream document has been received that lacks a resume token (_id).'
386
433
) ;
434
+
435
+ if ( eventEmitter ) return changeStream . emit ( 'error' , noResumeTokenError ) ;
387
436
if ( typeof callback === 'function' ) return callback ( noResumeTokenError , null ) ;
388
- if ( self . listenerCount ( 'error' ) ) return self . emit ( 'error' , noResumeTokenError ) ;
389
- return self . promiseLibrary . reject ( noResumeTokenError ) ;
437
+ return changeStream . promiseLibrary . reject ( noResumeTokenError ) ;
390
438
}
391
- self . resumeToken = change . _id ;
439
+
440
+ changeStream . resumeToken = change . _id ;
392
441
393
442
// Return the change
394
- if ( typeof callback === 'function' ) return callback ( err , change ) ;
395
- if ( self . listenerCount ( 'change' ) ) return self . emit ( 'change' , change ) ;
396
- return self . promiseLibrary . resolve ( change ) ;
397
- } ;
443
+ if ( eventEmitter ) return changeStream . emit ( 'change' , change ) ;
444
+ if ( typeof callback === 'function' ) return callback ( error , change ) ;
445
+ return changeStream . promiseLibrary . resolve ( change ) ;
446
+ }
398
447
399
448
/**
400
449
* The callback format for results
0 commit comments