@@ -34,6 +34,84 @@ const CHANGE_DOMAIN_TYPES = {
34
34
* @return {ChangeStream } a ChangeStream instance.
35
35
*/
36
36
37
+ class ResumeTokenTracker extends EventEmitter {
38
+ constructor ( changeStream , options ) {
39
+ super ( ) ;
40
+ this . changeStream = changeStream ;
41
+ this . options = options ;
42
+ this . _postBatchResumeToken = undefined ;
43
+ }
44
+
45
+ get resumeToken ( ) {
46
+ return this . _resumeToken ;
47
+ }
48
+
49
+ init ( ) {
50
+ this . _resumeToken = this . options . startAfter || this . options . resumeAfter ;
51
+ this . _operationTime = this . options . startAtOperationTime ;
52
+ this . _init = true ;
53
+ }
54
+
55
+ resumeInfo ( ) {
56
+ const resumeInfo = { } ;
57
+
58
+ if ( this . _init && this . _resumeToken ) {
59
+ resumeInfo . resumeAfter = this . _resumeToken ;
60
+ } else if ( this . _init && this . _operationTime ) {
61
+ resumeInfo . startAtOperationTime = this . _operationTime ;
62
+ } else {
63
+ if ( this . options . startAfter ) {
64
+ resumeInfo . startAfter = this . options . startAfter ;
65
+ }
66
+
67
+ if ( this . options . resumeAfter ) {
68
+ resumeInfo . resumeAfter = this . options . resumeAfter ;
69
+ }
70
+
71
+ if ( this . options . startAtOperationTime ) {
72
+ resumeInfo . startAtOperationTime = this . options . startAtOperationTime ;
73
+ }
74
+ }
75
+
76
+ return resumeInfo ;
77
+ }
78
+
79
+ onResponse ( postBatchResumeToken , operationTime ) {
80
+ if ( this . changeStream . isClosed ( ) ) {
81
+ return ;
82
+ }
83
+ const cursor = this . changeStream . cursor ;
84
+ if ( ! postBatchResumeToken ) {
85
+ if (
86
+ ! ( this . _resumeToken || this . _operationTime || cursor . bufferedCount ( ) ) &&
87
+ cursor . server &&
88
+ cursor . server . ismaster . maxWireVersion >= 7
89
+ ) {
90
+ this . _operationTime = operationTime ;
91
+ }
92
+ return ;
93
+ } else {
94
+ this . _postBatchResumeToken = postBatchResumeToken ;
95
+ if ( cursor . cursorState . documents . length === 0 ) {
96
+ this . _resumeToken = this . _postBatchResumeToken ;
97
+ }
98
+ }
99
+
100
+ this . emit ( 'response' ) ;
101
+ }
102
+
103
+ onNext ( doc ) {
104
+ if ( this . changeStream . isClosed ( ) ) {
105
+ return ;
106
+ }
107
+ if ( this . _postBatchResumeToken && this . changeStream . cursor . bufferedCount ( ) === 0 ) {
108
+ this . _resumeToken = this . _postBatchResumeToken ;
109
+ } else {
110
+ this . _resumeToken = doc . _id ;
111
+ }
112
+ }
113
+ }
114
+
37
115
class ChangeStream extends EventEmitter {
38
116
constructor ( changeDomain , pipeline , options ) {
39
117
super ( ) ;
@@ -69,17 +147,13 @@ class ChangeStream extends EventEmitter {
69
147
this . options . readPreference = changeDomain . s . readPreference ;
70
148
}
71
149
72
- // We need to get the operationTime as early as possible
73
- const isMaster = this . topology . lastIsMaster ( ) ;
74
- if ( ! isMaster ) {
75
- throw new MongoError ( 'Topology does not have an ismaster yet.' ) ;
76
- }
77
-
78
- this . operationTime = isMaster . operationTime ;
150
+ this . _resumeTokenTracker = new ResumeTokenTracker ( this , options ) ;
79
151
80
152
// Create contained Change Stream cursor
81
153
this . cursor = createChangeStreamCursor ( this ) ;
82
154
155
+ this . _resumeTokenTracker . init ( ) ;
156
+
83
157
// Listen for any `change` listeners being added to ChangeStream
84
158
this . on ( 'newListener' , eventName => {
85
159
if ( eventName === 'change' && this . cursor && this . listenerCount ( 'change' ) === 0 ) {
@@ -97,6 +171,14 @@ class ChangeStream extends EventEmitter {
97
171
} ) ;
98
172
}
99
173
174
+ /**
175
+ * The cached resume token that will be used to resume
176
+ * after the most recently returned change.
177
+ */
178
+ get resumeToken ( ) {
179
+ return this . _resumeTokenTracker . resumeToken ;
180
+ }
181
+
100
182
/**
101
183
* Check if there is any document still available in the Change Stream
102
184
* @function ChangeStream.prototype.hasNext
@@ -217,10 +299,6 @@ class ChangeStream extends EventEmitter {
217
299
218
300
// Create a new change stream cursor based on self's configuration
219
301
var createChangeStreamCursor = function ( self ) {
220
- if ( self . resumeToken ) {
221
- self . options . resumeAfter = self . resumeToken ;
222
- }
223
-
224
302
var changeStreamCursor = buildChangeStreamAggregationCommand ( self ) ;
225
303
226
304
/**
@@ -277,39 +355,20 @@ var createChangeStreamCursor = function(self) {
277
355
return changeStreamCursor ;
278
356
} ;
279
357
280
- function getResumeToken ( self ) {
281
- return self . resumeToken || self . options . resumeAfter ;
282
- }
283
-
284
- function getStartAtOperationTime ( self ) {
285
- const isMaster = self . topology . lastIsMaster ( ) || { } ;
286
- return (
287
- isMaster . maxWireVersion && isMaster . maxWireVersion >= 7 && self . options . startAtOperationTime
288
- ) ;
289
- }
290
-
291
358
var buildChangeStreamAggregationCommand = function ( self ) {
292
359
const topology = self . topology ;
293
360
const namespace = self . namespace ;
294
361
const pipeline = self . pipeline ;
295
362
const options = self . options ;
363
+ const resumeTokenTracker = self . _resumeTokenTracker ;
296
364
297
- var changeStreamStageOptions = {
298
- fullDocument : options . fullDocument || 'default'
299
- } ;
300
-
301
- const resumeToken = getResumeToken ( self ) ;
302
- const startAtOperationTime = getStartAtOperationTime ( self ) ;
303
- if ( resumeToken ) {
304
- changeStreamStageOptions . resumeAfter = resumeToken ;
305
- }
306
-
307
- if ( startAtOperationTime ) {
308
- changeStreamStageOptions . startAtOperationTime = startAtOperationTime ;
309
- }
365
+ const changeStreamStageOptions = Object . assign (
366
+ { fullDocument : options . fullDocument || 'default' } ,
367
+ resumeTokenTracker . resumeInfo ( )
368
+ ) ;
310
369
311
370
// Map cursor options
312
- var cursorOptions = { } ;
371
+ var cursorOptions = { resumeTokenTracker } ;
313
372
cursorOptionNames . forEach ( function ( optionName ) {
314
373
if ( options [ optionName ] ) {
315
374
cursorOptions [ optionName ] = options [ optionName ] ;
@@ -384,11 +443,6 @@ function processNewChange(args) {
384
443
if ( isResumableError ( error ) && ! changeStream . attemptingResume ) {
385
444
changeStream . attemptingResume = true ;
386
445
387
- if ( ! ( getResumeToken ( changeStream ) || getStartAtOperationTime ( changeStream ) ) ) {
388
- const startAtOperationTime = changeStream . cursor . cursorState . operationTime ;
389
- changeStream . options = Object . assign ( { startAtOperationTime } , changeStream . options ) ;
390
- }
391
-
392
446
// stop listening to all events from old cursor
393
447
[ 'data' , 'close' , 'end' , 'error' ] . forEach ( event =>
394
448
changeStream . cursor . removeAllListeners ( event )
@@ -450,7 +504,7 @@ function processNewChange(args) {
450
504
return changeStream . promiseLibrary . reject ( noResumeTokenError ) ;
451
505
}
452
506
453
- changeStream . resumeToken = change . _id ;
507
+ changeStream . _resumeTokenTracker . onNext ( change ) ;
454
508
455
509
// wipe the startAtOperationTime if there was one so that there won't be a conflict
456
510
// between resumeToken and startAtOperationTime if we need to reconnect the cursor
0 commit comments