@@ -5,97 +5,20 @@ const isResumableError = require('./error').isResumableError;
5
5
const MongoError = require ( './core' ) . MongoError ;
6
6
const ReadConcern = require ( './read_concern' ) ;
7
7
const MongoDBNamespace = require ( './utils' ) . MongoDBNamespace ;
8
+ const Cursor = require ( './cursor' ) ;
9
+ const relayEvents = require ( './core/utils' ) . relayEvents ;
10
+ const maxWireVersion = require ( './core/utils' ) . maxWireVersion ;
8
11
9
- var cursorOptionNames = [ 'maxAwaitTimeMS' , 'collation' , 'readPreference' ] ;
12
+ const CHANGE_STREAM_OPTIONS = [ 'resumeAfter' , 'startAfter' , 'startAtOperationTime' , 'fullDocument' ] ;
13
+ const CURSOR_OPTIONS = [ 'batchSize' , 'maxAwaitTimeMS' , 'collation' , 'readPreference' ] . concat (
14
+ CHANGE_STREAM_OPTIONS
15
+ ) ;
10
16
11
17
const CHANGE_DOMAIN_TYPES = {
12
18
COLLECTION : Symbol ( 'Collection' ) ,
13
19
DATABASE : Symbol ( 'Database' ) ,
14
20
CLUSTER : Symbol ( 'Cluster' )
15
21
} ;
16
- class ResumeTokenTracker extends EventEmitter {
17
- constructor ( changeStream , options ) {
18
- super ( ) ;
19
- this . changeStream = changeStream ;
20
- this . options = options ;
21
- this . _postBatchResumeToken = undefined ;
22
- }
23
-
24
- set resumeToken ( token ) {
25
- this . _resumeToken = token ;
26
- // NOTE: Event is for internal use only, and is not part of public API
27
- this . emit ( 'tokenChange' ) ;
28
- }
29
-
30
- get resumeToken ( ) {
31
- return this . _resumeToken ;
32
- }
33
-
34
- init ( ) {
35
- this . _resumeToken = this . options . startAfter || this . options . resumeAfter ;
36
- this . _operationTime = this . options . startAtOperationTime ;
37
- this . _init = true ;
38
- }
39
-
40
- resumeInfo ( ) {
41
- const resumeInfo = { } ;
42
-
43
- if ( this . _init && this . resumeToken ) {
44
- resumeInfo . resumeAfter = this . resumeToken ;
45
- } else if ( this . _init && this . _operationTime ) {
46
- resumeInfo . startAtOperationTime = this . _operationTime ;
47
- } else {
48
- if ( this . options . startAfter ) {
49
- resumeInfo . startAfter = this . options . startAfter ;
50
- }
51
-
52
- if ( this . options . resumeAfter ) {
53
- resumeInfo . resumeAfter = this . options . resumeAfter ;
54
- }
55
-
56
- if ( this . options . startAtOperationTime ) {
57
- resumeInfo . startAtOperationTime = this . options . startAtOperationTime ;
58
- }
59
- }
60
-
61
- return resumeInfo ;
62
- }
63
-
64
- onResponse ( postBatchResumeToken , operationTime ) {
65
- if ( this . changeStream . isClosed ( ) ) {
66
- return ;
67
- }
68
- const cursor = this . changeStream . cursor ;
69
- if ( ! postBatchResumeToken ) {
70
- if (
71
- ! ( this . resumeToken || this . _operationTime || cursor . bufferedCount ( ) ) &&
72
- cursor . server &&
73
- cursor . server . ismaster . maxWireVersion >= 7
74
- ) {
75
- this . _operationTime = operationTime ;
76
- }
77
- } else {
78
- this . _postBatchResumeToken = postBatchResumeToken ;
79
- if ( cursor . cursorState . documents . length === 0 ) {
80
- this . resumeToken = this . _postBatchResumeToken ;
81
- }
82
- }
83
-
84
- // NOTE: Event is for internal use only, and is not part of public API
85
- this . emit ( 'response' ) ;
86
- }
87
-
88
- onNext ( doc ) {
89
- if ( this . changeStream . isClosed ( ) ) {
90
- return ;
91
- }
92
- if ( this . _postBatchResumeToken && this . changeStream . cursor . bufferedCount ( ) === 0 ) {
93
- this . resumeToken = this . _postBatchResumeToken ;
94
- } else {
95
- this . resumeToken = doc . _id ;
96
- }
97
- }
98
- }
99
22
100
23
/**
101
24
* @typedef ResumeToken
@@ -133,6 +56,7 @@ class ResumeTokenTracker extends EventEmitter {
133
56
* @fires ChangeStream#change
134
57
* @fires ChangeStream#end
135
58
* @fires ChangeStream#error
59
+ * @fires ChangeStream#resumeTokenChanged
136
60
* @return {ChangeStream } a ChangeStream instance.
137
61
*/
138
62
class ChangeStream extends EventEmitter {
@@ -170,12 +94,8 @@ class ChangeStream extends EventEmitter {
170
94
this . options . readPreference = changeDomain . s . readPreference ;
171
95
}
172
96
173
- this . _resumeTokenTracker = new ResumeTokenTracker ( this , options ) ;
174
-
175
97
// Create contained Change Stream cursor
176
- this . cursor = createChangeStreamCursor ( this ) ;
177
-
178
- this . _resumeTokenTracker . init ( ) ;
98
+ this . cursor = createChangeStreamCursor ( this , options ) ;
179
99
180
100
// Listen for any `change` listeners being added to ChangeStream
181
101
this . on ( 'newListener' , eventName => {
@@ -200,7 +120,7 @@ class ChangeStream extends EventEmitter {
200
120
* after the most recently returned change.
201
121
*/
202
122
get resumeToken ( ) {
203
- return this . _resumeTokenTracker . resumeToken ;
123
+ return this . cursor . resumeToken ;
204
124
}
205
125
206
126
/**
@@ -321,9 +241,93 @@ class ChangeStream extends EventEmitter {
321
241
}
322
242
}
323
243
244
+ class ChangeStreamCursor extends Cursor {
245
+ constructor ( bson , ns , cmd , options , topology , topologyOptions ) {
246
+ // TODO: spread will help a lot here
247
+ super ( bson , ns , cmd , options , topology , topologyOptions ) ;
248
+
249
+ options = options || { } ;
250
+ this . _resumeToken = null ;
251
+ this . startAtOperationTime = options . startAtOperationTime ;
252
+
253
+ if ( options . startAfter ) {
254
+ this . resumeToken = options . startAfter ;
255
+ } else if ( options . resumeAfter ) {
256
+ this . resumeToken = options . resumeAfter ;
257
+ }
258
+ }
259
+
260
+ set resumeToken ( token ) {
261
+ this . _resumeToken = token ;
262
+ this . emit ( 'resumeTokenChanged' , token ) ;
263
+ }
264
+
265
+ get resumeToken ( ) {
266
+ return this . _resumeToken ;
267
+ }
268
+
269
+ get resumeOptions ( ) {
270
+ if ( this . resumeToken ) {
271
+ return { resumeAfter : this . resumeToken } ;
272
+ }
273
+
274
+ if ( this . startAtOperationTime && maxWireVersion ( this . server ) >= 7 ) {
275
+ return { startAtOperationTime : this . startAtOperationTime } ;
276
+ }
277
+
278
+ return null ;
279
+ }
280
+
281
+ _initializeCursor ( callback ) {
282
+ super . _initializeCursor ( ( err , result ) => {
283
+ if ( err ) {
284
+ callback ( err , null ) ;
285
+ return ;
286
+ }
287
+
288
+ const response = result . documents [ 0 ] ;
289
+
290
+ if (
291
+ this . startAtOperationTime == null &&
292
+ this . resumeAfter == null &&
293
+ this . startAfter == null &&
294
+ maxWireVersion ( this . server ) >= 7
295
+ ) {
296
+ this . startAtOperationTime = response . operationTime ;
297
+ }
298
+
299
+ const cursor = response . cursor ;
300
+ if ( cursor . firstBatch . length === 0 && cursor . postBatchResumeToken ) {
301
+ this . resumeToken = cursor . postBatchResumeToken ;
302
+ }
303
+
304
+ this . emit ( 'response' ) ;
305
+ callback ( err , result ) ;
306
+ } ) ;
307
+ }
308
+
309
+ _getMore ( callback ) {
310
+ super . _getMore ( ( err , response ) => {
311
+ if ( err ) {
312
+ callback ( err , null ) ;
313
+ return ;
314
+ }
315
+
316
+ const cursor = response . cursor ;
317
+ if ( cursor . nextBatch . length === 0 && cursor . postBatchResumeToken ) {
318
+ this . resumeToken = cursor . postBatchResumeToken ;
319
+ }
320
+
321
+ this . emit ( 'response' ) ;
322
+ callback ( err , response ) ;
323
+ } ) ;
324
+ }
325
+ }
326
+
324
327
// Create a new change stream cursor based on self's configuration
325
- var createChangeStreamCursor = function ( self ) {
326
- var changeStreamCursor = buildChangeStreamAggregationCommand ( self ) ;
328
+ var createChangeStreamCursor = function ( self , options ) {
329
+ const changeStreamCursor = buildChangeStreamAggregationCommand ( self , options ) ;
330
+ relayEvents ( changeStreamCursor , self , [ 'resumeTokenChanged' , 'end' , 'close' ] ) ;
327
331
328
332
/**
329
333
* Fired for each new matching change in the specified namespace. Attaching a `change`
@@ -345,19 +349,13 @@ var createChangeStreamCursor = function(self) {
345
349
* @event ChangeStream#close
346
350
* @type {null }
347
351
*/
348
- changeStreamCursor . on ( 'close' , function ( ) {
349
- self . emit ( 'close' ) ;
350
- } ) ;
351
352
352
353
/**
353
354
* Change stream end event
354
355
*
355
356
* @event ChangeStream#end
356
357
* @type {null }
357
358
*/
358
- changeStreamCursor . on ( 'end' , function ( ) {
359
- self . emit ( 'end' ) ;
360
- } ) ;
361
359
362
360
/**
363
361
* Fired when the stream encounters an error.
@@ -379,25 +377,26 @@ var createChangeStreamCursor = function(self) {
379
377
return changeStreamCursor ;
380
378
} ;
381
379
382
- var buildChangeStreamAggregationCommand = function ( self ) {
380
+ function applyKnownOptions ( target , source , optionNames ) {
381
+ optionNames . forEach ( name => {
382
+ if ( source [ name ] ) {
383
+ target [ name ] = source [ name ] ;
384
+ }
385
+ } ) ;
386
+ }
387
+
388
+ var buildChangeStreamAggregationCommand = function ( self , options ) {
389
+ options = options || { } ;
383
390
const topology = self . topology ;
384
391
const namespace = self . namespace ;
385
392
const pipeline = self . pipeline ;
386
- const options = self . options ;
387
- const resumeTokenTracker = self . _resumeTokenTracker ;
388
393
389
- const changeStreamStageOptions = Object . assign (
390
- { fullDocument : options . fullDocument || 'default' } ,
391
- resumeTokenTracker . resumeInfo ( )
392
- ) ;
394
+ const changeStreamStageOptions = { fullDocument : options . fullDocument || 'default' } ;
395
+ applyKnownOptions ( changeStreamStageOptions , options , CHANGE_STREAM_OPTIONS ) ;
393
396
394
397
// Map cursor options
395
- var cursorOptions = { resumeTokenTracker } ;
396
- cursorOptionNames . forEach ( function ( optionName ) {
397
- if ( options [ optionName ] ) {
398
- cursorOptions [ optionName ] = options [ optionName ] ;
399
- }
400
- } ) ;
398
+ const cursorOptions = { cursorFactory : ChangeStreamCursor } ;
399
+ applyKnownOptions ( cursorOptions , options , CURSOR_OPTIONS ) ;
401
400
402
401
if ( self . type === CHANGE_DOMAIN_TYPES . CLUSTER ) {
403
402
changeStreamStageOptions . allChangesForCluster = true ;
@@ -460,6 +459,7 @@ function processNewChange(args) {
460
459
: changeStream . promiseLibrary . reject ( error ) ;
461
460
}
462
461
462
+ const cursor = changeStream . cursor ;
463
463
const topology = changeStream . topology ;
464
464
const options = changeStream . cursor . options ;
465
465
@@ -483,7 +483,7 @@ function processNewChange(args) {
483
483
changeStream . emit ( 'close' ) ;
484
484
return ;
485
485
}
486
- changeStream . cursor = createChangeStreamCursor ( changeStream ) ;
486
+ changeStream . cursor = createChangeStreamCursor ( changeStream , cursor . resumeOptions ) ;
487
487
} ) ;
488
488
489
489
return ;
@@ -493,7 +493,7 @@ function processNewChange(args) {
493
493
waitForTopologyConnected ( topology , { readPreference : options . readPreference } , err => {
494
494
if ( err ) return callback ( err , null ) ;
495
495
496
- changeStream . cursor = createChangeStreamCursor ( changeStream ) ;
496
+ changeStream . cursor = createChangeStreamCursor ( changeStream , cursor . resumeOptions ) ;
497
497
changeStream . next ( callback ) ;
498
498
} ) ;
499
499
@@ -506,7 +506,9 @@ function processNewChange(args) {
506
506
resolve ( ) ;
507
507
} ) ;
508
508
} )
509
- . then ( ( ) => ( changeStream . cursor = createChangeStreamCursor ( changeStream ) ) )
509
+ . then (
510
+ ( ) => ( changeStream . cursor = createChangeStreamCursor ( changeStream , cursor . resumeOptions ) )
511
+ )
510
512
. then ( ( ) => changeStream . next ( ) ) ;
511
513
}
512
514
@@ -517,9 +519,8 @@ function processNewChange(args) {
517
519
518
520
changeStream . attemptingResume = false ;
519
521
520
- // Cache the resume token if it is present. If it is not present return an error.
521
- if ( ! change || ! change . _id ) {
522
- var noResumeTokenError = new Error (
522
+ if ( change && ! change . _id ) {
523
+ const noResumeTokenError = new Error (
523
524
'A change stream document has been received that lacks a resume token (_id).'
524
525
) ;
525
526
@@ -528,7 +529,12 @@ function processNewChange(args) {
528
529
return changeStream . promiseLibrary . reject ( noResumeTokenError ) ;
529
530
}
530
531
531
- changeStream . _resumeTokenTracker . onNext ( change ) ;
532
+ // cache the resume token
533
+ if ( cursor . bufferedCount ( ) === 0 && cursor . cursorState . postBatchResumeToken ) {
534
+ cursor . resumeToken = cursor . cursorState . postBatchResumeToken ;
535
+ } else {
536
+ cursor . resumeToken = change . _id ;
537
+ }
532
538
533
539
// wipe the startAtOperationTime if there was one so that there won't be a conflict
534
540
// between resumeToken and startAtOperationTime if we need to reconnect the cursor
0 commit comments