@@ -21,6 +21,7 @@ import { ReadPreference, type ReadPreferenceLike } from '../read_preference';
21
21
import { type AsyncDisposable , configureResourceManagement } from '../resource_management' ;
22
22
import type { Server } from '../sdam/server' ;
23
23
import { ClientSession , maybeClearPinnedConnection } from '../sessions' ;
24
+ import { TimeoutContext } from '../timeout' ;
24
25
import { type MongoDBNamespace , squashError } from '../utils' ;
25
26
26
27
/**
@@ -60,6 +61,17 @@ export interface CursorStreamOptions {
60
61
/** @public */
61
62
export type CursorFlag = ( typeof CURSOR_FLAGS ) [ number ] ;
62
63
64
+ /** @public */
65
+ export const CursorTimeoutMode = Object . freeze ( {
66
+ ITERATION : 'iteration' ,
67
+ LIFETIME : 'cursorLifetime'
68
+ } as const ) ;
69
+
70
+ /** @public
71
+ * TODO(NODE-5688): Document and release
72
+ * */
73
+ export type CursorTimeoutMode = ( typeof CursorTimeoutMode ) [ keyof typeof CursorTimeoutMode ] ;
74
+
63
75
/** @public */
64
76
export interface AbstractCursorOptions extends BSONSerializeOptions {
65
77
session ?: ClientSession ;
@@ -105,6 +117,8 @@ export interface AbstractCursorOptions extends BSONSerializeOptions {
105
117
noCursorTimeout ?: boolean ;
106
118
/** @internal TODO(NODE-5688): make this public */
107
119
timeoutMS ?: number ;
120
+ /** @internal TODO(NODE-5688): make this public */
121
+ timeoutMode ?: CursorTimeoutMode ;
108
122
}
109
123
110
124
/** @internal */
@@ -117,6 +131,8 @@ export type InternalAbstractCursorOptions = Omit<AbstractCursorOptions, 'readPre
117
131
oplogReplay ?: boolean ;
118
132
exhaust ?: boolean ;
119
133
partial ?: boolean ;
134
+
135
+ omitMaxTimeMS ?: boolean ;
120
136
} ;
121
137
122
138
/** @public */
@@ -154,6 +170,8 @@ export abstract class AbstractCursor<
154
170
private isKilled : boolean ;
155
171
/** @internal */
156
172
protected readonly cursorOptions : InternalAbstractCursorOptions ;
173
+ /** @internal */
174
+ protected timeoutContext ?: TimeoutContext ;
157
175
158
176
/** @event */
159
177
static readonly CLOSE = 'close' as const ;
@@ -186,6 +204,30 @@ export abstract class AbstractCursor<
186
204
...pluckBSONSerializeOptions ( options )
187
205
} ;
188
206
this . cursorOptions . timeoutMS = options . timeoutMS ;
207
+ if ( this . cursorOptions . timeoutMS != null ) {
208
+ if ( options . timeoutMode == null ) {
209
+ if ( options . tailable ) {
210
+ this . cursorOptions . timeoutMode = CursorTimeoutMode . ITERATION ;
211
+ } else {
212
+ this . cursorOptions . timeoutMode = CursorTimeoutMode . LIFETIME ;
213
+ }
214
+ } else {
215
+ if ( options . tailable && this . cursorOptions . timeoutMode === CursorTimeoutMode . LIFETIME ) {
216
+ throw new MongoInvalidArgumentError (
217
+ "Cannot set tailable cursor's timeoutMode to LIFETIME"
218
+ ) ;
219
+ }
220
+ this . cursorOptions . timeoutMode = options . timeoutMode ;
221
+ }
222
+ } else {
223
+ if ( options . timeoutMode != null )
224
+ throw new MongoInvalidArgumentError ( 'Cannot set timeoutMode without setting timeoutMS' ) ;
225
+ }
226
+ this . cursorOptions . omitMaxTimeMS =
227
+ this . cursorOptions . timeoutMS != null &&
228
+ ( ( this . cursorOptions . timeoutMode === CursorTimeoutMode . ITERATION &&
229
+ ! this . cursorOptions . tailable ) ||
230
+ ( this . cursorOptions . tailable && ! this . cursorOptions . awaitData ) ) ;
189
231
190
232
const readConcern = ReadConcern . fromOptions ( options ) ;
191
233
if ( readConcern ) {
@@ -400,12 +442,21 @@ export abstract class AbstractCursor<
400
442
return false ;
401
443
}
402
444
403
- do {
404
- if ( ( this . documents ?. length ?? 0 ) !== 0 ) {
405
- return true ;
445
+ if ( this . cursorOptions . timeoutMode === CursorTimeoutMode . ITERATION && this . cursorId != null ) {
446
+ this . timeoutContext ?. refresh ( ) ;
447
+ }
448
+ try {
449
+ do {
450
+ if ( ( this . documents ?. length ?? 0 ) !== 0 ) {
451
+ return true ;
452
+ }
453
+ await this . fetchBatch ( ) ;
454
+ } while ( ! this . isDead || ( this . documents ?. length ?? 0 ) !== 0 ) ;
455
+ } finally {
456
+ if ( this . cursorOptions . timeoutMode === CursorTimeoutMode . ITERATION && this . cursorId != null ) {
457
+ this . timeoutContext ?. clear ( ) ;
406
458
}
407
- await this . fetchBatch ( ) ;
408
- } while ( ! this . isDead || ( this . documents ?. length ?? 0 ) !== 0 ) ;
459
+ }
409
460
410
461
return false ;
411
462
}
@@ -415,15 +466,24 @@ export abstract class AbstractCursor<
415
466
if ( this . cursorId === Long . ZERO ) {
416
467
throw new MongoCursorExhaustedError ( ) ;
417
468
}
469
+ if ( this . cursorOptions . timeoutMode === CursorTimeoutMode . ITERATION && this . cursorId != null ) {
470
+ this . timeoutContext ?. refresh ( ) ;
471
+ }
418
472
419
- do {
420
- const doc = this . documents ?. shift ( this . deserializationOptions ) ;
421
- if ( doc != null ) {
422
- if ( this . transform != null ) return await this . transformDocument ( doc ) ;
423
- return doc ;
473
+ try {
474
+ do {
475
+ const doc = this . documents ?. shift ( this . deserializationOptions ) ;
476
+ if ( doc != null ) {
477
+ if ( this . transform != null ) return await this . transformDocument ( doc ) ;
478
+ return doc ;
479
+ }
480
+ await this . fetchBatch ( ) ;
481
+ } while ( ! this . isDead || ( this . documents ?. length ?? 0 ) !== 0 ) ;
482
+ } finally {
483
+ if ( this . cursorOptions . timeoutMode === CursorTimeoutMode . ITERATION && this . cursorId != null ) {
484
+ this . timeoutContext ?. clear ( ) ;
424
485
}
425
- await this . fetchBatch ( ) ;
426
- } while ( ! this . isDead || ( this . documents ?. length ?? 0 ) !== 0 ) ;
486
+ }
427
487
428
488
return null ;
429
489
}
@@ -436,18 +496,27 @@ export abstract class AbstractCursor<
436
496
throw new MongoCursorExhaustedError ( ) ;
437
497
}
438
498
439
- let doc = this . documents ?. shift ( this . deserializationOptions ) ;
440
- if ( doc != null ) {
441
- if ( this . transform != null ) return await this . transformDocument ( doc ) ;
442
- return doc ;
499
+ if ( this . cursorOptions . timeoutMode === CursorTimeoutMode . ITERATION && this . cursorId != null ) {
500
+ this . timeoutContext ?. refresh ( ) ;
443
501
}
502
+ try {
503
+ let doc = this . documents ?. shift ( this . deserializationOptions ) ;
504
+ if ( doc != null ) {
505
+ if ( this . transform != null ) return await this . transformDocument ( doc ) ;
506
+ return doc ;
507
+ }
444
508
445
- await this . fetchBatch ( ) ;
509
+ await this . fetchBatch ( ) ;
446
510
447
- doc = this . documents ?. shift ( this . deserializationOptions ) ;
448
- if ( doc != null ) {
449
- if ( this . transform != null ) return await this . transformDocument ( doc ) ;
450
- return doc ;
511
+ doc = this . documents ?. shift ( this . deserializationOptions ) ;
512
+ if ( doc != null ) {
513
+ if ( this . transform != null ) return await this . transformDocument ( doc ) ;
514
+ return doc ;
515
+ }
516
+ } finally {
517
+ if ( this . cursorOptions . timeoutMode === CursorTimeoutMode . ITERATION && this . cursorId != null ) {
518
+ this . timeoutContext ?. clear ( ) ;
519
+ }
451
520
}
452
521
453
522
return null ;
@@ -476,8 +545,8 @@ export abstract class AbstractCursor<
476
545
/**
477
546
* Frees any client-side resources used by the cursor.
478
547
*/
479
- async close ( ) : Promise < void > {
480
- await this . cleanup ( ) ;
548
+ async close ( options ?: { timeoutMS ?: number } ) : Promise < void > {
549
+ await this . cleanup ( options ?. timeoutMS ) ;
481
550
}
482
551
483
552
/**
@@ -658,6 +727,8 @@ export abstract class AbstractCursor<
658
727
659
728
this . cursorId = null ;
660
729
this . documents ?. clear ( ) ;
730
+ this . timeoutContext ?. clear ( ) ;
731
+ this . timeoutContext = undefined ;
661
732
this . isClosed = false ;
662
733
this . isKilled = false ;
663
734
this . initialized = false ;
@@ -707,7 +778,7 @@ export abstract class AbstractCursor<
707
778
}
708
779
) ;
709
780
710
- return await executeOperation ( this . cursorClient , getMoreOperation ) ;
781
+ return await executeOperation ( this . cursorClient , getMoreOperation , this . timeoutContext ) ;
711
782
}
712
783
713
784
/**
@@ -718,6 +789,12 @@ export abstract class AbstractCursor<
718
789
* a significant refactor.
719
790
*/
720
791
private async cursorInit ( ) : Promise < void > {
792
+ if ( this . cursorOptions . timeoutMS != null ) {
793
+ this . timeoutContext = TimeoutContext . create ( {
794
+ serverSelectionTimeoutMS : this . client . options . serverSelectionTimeoutMS ,
795
+ timeoutMS : this . cursorOptions . timeoutMS
796
+ } ) ;
797
+ }
721
798
try {
722
799
const state = await this . _initialize ( this . cursorSession ) ;
723
800
const response = state . response ;
@@ -729,7 +806,7 @@ export abstract class AbstractCursor<
729
806
} catch ( error ) {
730
807
// the cursor is now initialized, even if an error occurred
731
808
this . initialized = true ;
732
- await this . cleanup ( error ) ;
809
+ await this . cleanup ( undefined , error ) ;
733
810
throw error ;
734
811
}
735
812
@@ -763,14 +840,15 @@ export abstract class AbstractCursor<
763
840
764
841
// otherwise need to call getMore
765
842
const batchSize = this . cursorOptions . batchSize || 1000 ;
843
+ this . cursorOptions . omitMaxTimeMS = this . cursorOptions . timeoutMS != null ;
766
844
767
845
try {
768
846
const response = await this . getMore ( batchSize ) ;
769
847
this . cursorId = response . id ;
770
848
this . documents = response ;
771
849
} catch ( error ) {
772
850
try {
773
- await this . cleanup ( error ) ;
851
+ await this . cleanup ( undefined , error ) ;
774
852
} catch ( error ) {
775
853
// `cleanupCursor` should never throw, squash and throw the original error
776
854
squashError ( error ) ;
@@ -791,7 +869,7 @@ export abstract class AbstractCursor<
791
869
}
792
870
793
871
/** @internal */
794
- private async cleanup ( error ?: Error ) {
872
+ private async cleanup ( timeoutMS ?: number , error ?: Error ) {
795
873
this . isClosed = true ;
796
874
const session = this . cursorSession ;
797
875
try {
@@ -806,11 +884,23 @@ export abstract class AbstractCursor<
806
884
this . isKilled = true ;
807
885
const cursorId = this . cursorId ;
808
886
this . cursorId = Long . ZERO ;
887
+ let timeoutContext : TimeoutContext | undefined ;
888
+ if ( timeoutMS != null ) {
889
+ this . timeoutContext ?. clear ( ) ;
890
+ timeoutContext = TimeoutContext . create ( {
891
+ serverSelectionTimeoutMS : this . client . options . serverSelectionTimeoutMS ,
892
+ timeoutMS
893
+ } ) ;
894
+ } else {
895
+ this . timeoutContext ?. refresh ( ) ;
896
+ timeoutContext = this . timeoutContext ;
897
+ }
809
898
await executeOperation (
810
899
this . cursorClient ,
811
900
new KillCursorsOperation ( cursorId , this . cursorNamespace , this . selectedServer , {
812
901
session
813
- } )
902
+ } ) ,
903
+ timeoutContext
814
904
) ;
815
905
}
816
906
} catch ( error ) {
0 commit comments