@@ -20,6 +20,7 @@ import { ReadPreference, type ReadPreferenceLike } from '../read_preference';
20
20
import { type AsyncDisposable , configureResourceManagement } from '../resource_management' ;
21
21
import type { Server } from '../sdam/server' ;
22
22
import { ClientSession , maybeClearPinnedConnection } from '../sessions' ;
23
+ import { TimeoutContext } from '../timeout' ;
23
24
import { type MongoDBNamespace , squashError } from '../utils' ;
24
25
25
26
/**
@@ -59,6 +60,17 @@ export interface CursorStreamOptions {
59
60
/** @public */
60
61
export type CursorFlag = ( typeof CURSOR_FLAGS ) [ number ] ;
61
62
63
+ /** @public */
64
+ export const CursorTimeoutMode = Object . freeze ( {
65
+ ITERATION : 'iteration' ,
66
+ LIFETIME : 'cursorLifetime'
67
+ } as const ) ;
68
+
69
+ /** @public
70
+ * TODO(NODE-5688): Document and release
71
+ * */
72
+ export type CursorTimeoutMode = ( typeof CursorTimeoutMode ) [ keyof typeof CursorTimeoutMode ] ;
73
+
62
74
/** @public */
63
75
export interface AbstractCursorOptions extends BSONSerializeOptions {
64
76
session ?: ClientSession ;
@@ -104,6 +116,8 @@ export interface AbstractCursorOptions extends BSONSerializeOptions {
104
116
noCursorTimeout ?: boolean ;
105
117
/** @internal TODO(NODE-5688): make this public */
106
118
timeoutMS ?: number ;
119
+ /** @internal TODO(NODE-5688): make this public */
120
+ timeoutMode ?: CursorTimeoutMode ;
107
121
}
108
122
109
123
/** @internal */
@@ -116,6 +130,8 @@ export type InternalAbstractCursorOptions = Omit<AbstractCursorOptions, 'readPre
116
130
oplogReplay ?: boolean ;
117
131
exhaust ?: boolean ;
118
132
partial ?: boolean ;
133
+
134
+ omitMaxTimeMS ?: boolean ;
119
135
} ;
120
136
121
137
/** @public */
@@ -153,6 +169,8 @@ export abstract class AbstractCursor<
153
169
private isKilled : boolean ;
154
170
/** @internal */
155
171
protected readonly cursorOptions : InternalAbstractCursorOptions ;
172
+ /** @internal */
173
+ protected timeoutContext ?: TimeoutContext ;
156
174
157
175
/** @event */
158
176
static readonly CLOSE = 'close' as const ;
@@ -182,6 +200,30 @@ export abstract class AbstractCursor<
182
200
...pluckBSONSerializeOptions ( options )
183
201
} ;
184
202
this . cursorOptions . timeoutMS = options . timeoutMS ;
203
+ if ( this . cursorOptions . timeoutMS != null ) {
204
+ if ( options . timeoutMode == null ) {
205
+ if ( options . tailable ) {
206
+ this . cursorOptions . timeoutMode = CursorTimeoutMode . ITERATION ;
207
+ } else {
208
+ this . cursorOptions . timeoutMode = CursorTimeoutMode . LIFETIME ;
209
+ }
210
+ } else {
211
+ if ( options . tailable && this . cursorOptions . timeoutMode === CursorTimeoutMode . LIFETIME ) {
212
+ throw new MongoInvalidArgumentError (
213
+ "Cannot set tailable cursor's timeoutMode to LIFETIME"
214
+ ) ;
215
+ }
216
+ this . cursorOptions . timeoutMode = options . timeoutMode ;
217
+ }
218
+ } else {
219
+ if ( options . timeoutMode != null )
220
+ throw new MongoInvalidArgumentError ( 'Cannot set timeoutMode without setting timeoutMS' ) ;
221
+ }
222
+ this . cursorOptions . omitMaxTimeMS =
223
+ this . cursorOptions . timeoutMS != null &&
224
+ ( ( this . cursorOptions . timeoutMode === CursorTimeoutMode . ITERATION &&
225
+ ! this . cursorOptions . tailable ) ||
226
+ ( this . cursorOptions . tailable && ! this . cursorOptions . awaitData ) ) ;
185
227
186
228
const readConcern = ReadConcern . fromOptions ( options ) ;
187
229
if ( readConcern ) {
@@ -389,12 +431,21 @@ export abstract class AbstractCursor<
389
431
return false ;
390
432
}
391
433
392
- do {
393
- if ( ( this . documents ?. length ?? 0 ) !== 0 ) {
394
- return true ;
434
+ if ( this . cursorOptions . timeoutMode === CursorTimeoutMode . ITERATION && this . cursorId != null ) {
435
+ this . timeoutContext ?. refresh ( ) ;
436
+ }
437
+ try {
438
+ do {
439
+ if ( ( this . documents ?. length ?? 0 ) !== 0 ) {
440
+ return true ;
441
+ }
442
+ await this . fetchBatch ( ) ;
443
+ } while ( ! this . isDead || ( this . documents ?. length ?? 0 ) !== 0 ) ;
444
+ } finally {
445
+ if ( this . cursorOptions . timeoutMode === CursorTimeoutMode . ITERATION && this . cursorId != null ) {
446
+ this . timeoutContext ?. clear ( ) ;
395
447
}
396
- await this . fetchBatch ( ) ;
397
- } while ( ! this . isDead || ( this . documents ?. length ?? 0 ) !== 0 ) ;
448
+ }
398
449
399
450
return false ;
400
451
}
@@ -404,15 +455,24 @@ export abstract class AbstractCursor<
404
455
if ( this . cursorId === Long . ZERO ) {
405
456
throw new MongoCursorExhaustedError ( ) ;
406
457
}
458
+ if ( this . cursorOptions . timeoutMode === CursorTimeoutMode . ITERATION && this . cursorId != null ) {
459
+ this . timeoutContext ?. refresh ( ) ;
460
+ }
407
461
408
- do {
409
- const doc = this . documents ?. shift ( this . cursorOptions ) ;
410
- if ( doc != null ) {
411
- if ( this . transform != null ) return await this . transformDocument ( doc ) ;
412
- return doc ;
462
+ try {
463
+ do {
464
+ const doc = this . documents ?. shift ( this . cursorOptions ) ;
465
+ if ( doc != null ) {
466
+ if ( this . transform != null ) return await this . transformDocument ( doc ) ;
467
+ return doc ;
468
+ }
469
+ await this . fetchBatch ( ) ;
470
+ } while ( ! this . isDead || ( this . documents ?. length ?? 0 ) !== 0 ) ;
471
+ } finally {
472
+ if ( this . cursorOptions . timeoutMode === CursorTimeoutMode . ITERATION && this . cursorId != null ) {
473
+ this . timeoutContext ?. clear ( ) ;
413
474
}
414
- await this . fetchBatch ( ) ;
415
- } while ( ! this . isDead || ( this . documents ?. length ?? 0 ) !== 0 ) ;
475
+ }
416
476
417
477
return null ;
418
478
}
@@ -425,18 +485,27 @@ export abstract class AbstractCursor<
425
485
throw new MongoCursorExhaustedError ( ) ;
426
486
}
427
487
428
- let doc = this . documents ?. shift ( this . cursorOptions ) ;
429
- if ( doc != null ) {
430
- if ( this . transform != null ) return await this . transformDocument ( doc ) ;
431
- return doc ;
488
+ if ( this . cursorOptions . timeoutMode === CursorTimeoutMode . ITERATION && this . cursorId != null ) {
489
+ this . timeoutContext ?. refresh ( ) ;
432
490
}
491
+ try {
492
+ let doc = this . documents ?. shift ( this . cursorOptions ) ;
493
+ if ( doc != null ) {
494
+ if ( this . transform != null ) return await this . transformDocument ( doc ) ;
495
+ return doc ;
496
+ }
433
497
434
- await this . fetchBatch ( ) ;
498
+ await this . fetchBatch ( ) ;
435
499
436
- doc = this . documents ?. shift ( this . cursorOptions ) ;
437
- if ( doc != null ) {
438
- if ( this . transform != null ) return await this . transformDocument ( doc ) ;
439
- return doc ;
500
+ doc = this . documents ?. shift ( this . cursorOptions ) ;
501
+ if ( doc != null ) {
502
+ if ( this . transform != null ) return await this . transformDocument ( doc ) ;
503
+ return doc ;
504
+ }
505
+ } finally {
506
+ if ( this . cursorOptions . timeoutMode === CursorTimeoutMode . ITERATION && this . cursorId != null ) {
507
+ this . timeoutContext ?. clear ( ) ;
508
+ }
440
509
}
441
510
442
511
return null ;
@@ -465,8 +534,8 @@ export abstract class AbstractCursor<
465
534
/**
466
535
* Frees any client-side resources used by the cursor.
467
536
*/
468
- async close ( ) : Promise < void > {
469
- await this . cleanup ( ) ;
537
+ async close ( options ?: { timeoutMS ?: number } ) : Promise < void > {
538
+ await this . cleanup ( options ?. timeoutMS ) ;
470
539
}
471
540
472
541
/**
@@ -647,6 +716,8 @@ export abstract class AbstractCursor<
647
716
648
717
this . cursorId = null ;
649
718
this . documents ?. clear ( ) ;
719
+ this . timeoutContext ?. clear ( ) ;
720
+ this . timeoutContext = undefined ;
650
721
this . isClosed = false ;
651
722
this . isKilled = false ;
652
723
this . initialized = false ;
@@ -697,7 +768,7 @@ export abstract class AbstractCursor<
697
768
}
698
769
) ;
699
770
700
- return await executeOperation ( this . cursorClient , getMoreOperation ) ;
771
+ return await executeOperation ( this . cursorClient , getMoreOperation , this . timeoutContext ) ;
701
772
}
702
773
703
774
/**
@@ -708,6 +779,12 @@ export abstract class AbstractCursor<
708
779
* a significant refactor.
709
780
*/
710
781
private async cursorInit ( ) : Promise < void > {
782
+ if ( this . cursorOptions . timeoutMS != null ) {
783
+ this . timeoutContext = TimeoutContext . create ( {
784
+ serverSelectionTimeoutMS : this . client . options . serverSelectionTimeoutMS ,
785
+ timeoutMS : this . cursorOptions . timeoutMS
786
+ } ) ;
787
+ }
711
788
try {
712
789
const state = await this . _initialize ( this . cursorSession ) ;
713
790
const response = state . response ;
@@ -719,7 +796,7 @@ export abstract class AbstractCursor<
719
796
} catch ( error ) {
720
797
// the cursor is now initialized, even if an error occurred
721
798
this . initialized = true ;
722
- await this . cleanup ( error ) ;
799
+ await this . cleanup ( undefined , error ) ;
723
800
throw error ;
724
801
}
725
802
@@ -753,14 +830,15 @@ export abstract class AbstractCursor<
753
830
754
831
// otherwise need to call getMore
755
832
const batchSize = this . cursorOptions . batchSize || 1000 ;
833
+ this . cursorOptions . omitMaxTimeMS = this . cursorOptions . timeoutMS != null ;
756
834
757
835
try {
758
836
const response = await this . getMore ( batchSize ) ;
759
837
this . cursorId = response . id ;
760
838
this . documents = response ;
761
839
} catch ( error ) {
762
840
try {
763
- await this . cleanup ( error ) ;
841
+ await this . cleanup ( undefined , error ) ;
764
842
} catch ( error ) {
765
843
// `cleanupCursor` should never throw, squash and throw the original error
766
844
squashError ( error ) ;
@@ -781,7 +859,7 @@ export abstract class AbstractCursor<
781
859
}
782
860
783
861
/** @internal */
784
- private async cleanup ( error ?: Error ) {
862
+ private async cleanup ( timeoutMS ?: number , error ?: Error ) {
785
863
this . isClosed = true ;
786
864
const session = this . cursorSession ;
787
865
try {
@@ -796,11 +874,23 @@ export abstract class AbstractCursor<
796
874
this . isKilled = true ;
797
875
const cursorId = this . cursorId ;
798
876
this . cursorId = Long . ZERO ;
877
+ let timeoutContext : TimeoutContext | undefined ;
878
+ if ( timeoutMS != null ) {
879
+ this . timeoutContext ?. clear ( ) ;
880
+ timeoutContext = TimeoutContext . create ( {
881
+ serverSelectionTimeoutMS : this . client . options . serverSelectionTimeoutMS ,
882
+ timeoutMS
883
+ } ) ;
884
+ } else {
885
+ this . timeoutContext ?. refresh ( ) ;
886
+ timeoutContext = this . timeoutContext ;
887
+ }
799
888
await executeOperation (
800
889
this . cursorClient ,
801
890
new KillCursorsOperation ( cursorId , this . cursorNamespace , this . selectedServer , {
802
891
session
803
- } )
892
+ } ) ,
893
+ timeoutContext
804
894
) ;
805
895
}
806
896
} catch ( error ) {
0 commit comments