@@ -21,7 +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
+ import { type CSOTTimeoutContext , type Timeout , TimeoutContext } from '../timeout' ;
25
25
import { type MongoDBNamespace , squashError } from '../utils' ;
26
26
27
27
/**
@@ -119,6 +119,14 @@ export interface AbstractCursorOptions extends BSONSerializeOptions {
119
119
timeoutMS ?: number ;
120
120
/** @internal TODO(NODE-5688): make this public */
121
121
timeoutMode ?: CursorTimeoutMode ;
122
+
123
+ /**
124
+ * @internal
125
+ *
126
+ * A timeout context to govern the total time the cursor can live. If provided, the cursor
127
+ * cannot be used in ITERATION mode.
128
+ */
129
+ timeoutContext ?: CursorTimeoutContext ;
122
130
}
123
131
124
132
/** @internal */
@@ -171,7 +179,7 @@ export abstract class AbstractCursor<
171
179
/** @internal */
172
180
protected readonly cursorOptions : InternalAbstractCursorOptions ;
173
181
/** @internal */
174
- protected timeoutContext ?: TimeoutContext ;
182
+ protected timeoutContext ?: CursorTimeoutContext ;
175
183
176
184
/** @event */
177
185
static readonly CLOSE = 'close' as const ;
@@ -205,20 +213,12 @@ export abstract class AbstractCursor<
205
213
} ;
206
214
this . cursorOptions . timeoutMS = options . timeoutMS ;
207
215
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 ;
216
+ if ( options . tailable && this . cursorOptions . timeoutMode === CursorTimeoutMode . LIFETIME ) {
217
+ throw new MongoInvalidArgumentError ( "Cannot set tailable cursor's timeoutMode to LIFETIME" ) ;
221
218
}
219
+ this . cursorOptions . timeoutMode =
220
+ options . timeoutMode ??
221
+ ( options . tailable ? CursorTimeoutMode . ITERATION : CursorTimeoutMode . LIFETIME ) ;
222
222
} else {
223
223
if ( options . timeoutMode != null )
224
224
throw new MongoInvalidArgumentError ( 'Cannot set timeoutMode without setting timeoutMS' ) ;
@@ -264,6 +264,17 @@ export abstract class AbstractCursor<
264
264
utf8 : options ?. enableUtf8Validation === false ? false : true
265
265
}
266
266
} ;
267
+
268
+ if (
269
+ options . timeoutContext != null &&
270
+ options . timeoutMS != null &&
271
+ this . cursorOptions . timeoutMode !== CursorTimeoutMode . LIFETIME
272
+ ) {
273
+ throw new MongoAPIError (
274
+ `cannot create a cursor with an externally provided timeout context that doesn't use timeoutMode=CURSOR_LIFETIME.`
275
+ ) ;
276
+ }
277
+ this . timeoutContext = options . timeoutContext ;
267
278
}
268
279
269
280
/**
@@ -721,6 +732,9 @@ export abstract class AbstractCursor<
721
732
* if the resultant data has already been retrieved by this cursor.
722
733
*/
723
734
rewind ( ) : void {
735
+ if ( this . timeoutContext && this . timeoutContext . owner !== this ) {
736
+ throw new MongoAPIError ( `Cannot rewind cursor that does not own its timeout context.` ) ;
737
+ }
724
738
if ( ! this . initialized ) {
725
739
return ;
726
740
}
@@ -790,10 +804,13 @@ export abstract class AbstractCursor<
790
804
*/
791
805
private async cursorInit ( ) : Promise < void > {
792
806
if ( this . cursorOptions . timeoutMS != null ) {
793
- this . timeoutContext = TimeoutContext . create ( {
794
- serverSelectionTimeoutMS : this . client . options . serverSelectionTimeoutMS ,
795
- timeoutMS : this . cursorOptions . timeoutMS
796
- } ) ;
807
+ this . timeoutContext ??= new CursorTimeoutContext (
808
+ TimeoutContext . create ( {
809
+ serverSelectionTimeoutMS : this . client . options . serverSelectionTimeoutMS ,
810
+ timeoutMS : this . cursorOptions . timeoutMS
811
+ } ) ,
812
+ this
813
+ ) ;
797
814
}
798
815
try {
799
816
const state = await this . _initialize ( this . cursorSession ) ;
@@ -872,6 +889,20 @@ export abstract class AbstractCursor<
872
889
private async cleanup ( timeoutMS ?: number , error ?: Error ) {
873
890
this . isClosed = true ;
874
891
const session = this . cursorSession ;
892
+ const timeoutContextForKillCursors = ( ) : CursorTimeoutContext | undefined => {
893
+ if ( timeoutMS != null ) {
894
+ this . timeoutContext ?. clear ( ) ;
895
+ return new CursorTimeoutContext (
896
+ TimeoutContext . create ( {
897
+ serverSelectionTimeoutMS : this . client . options . serverSelectionTimeoutMS ,
898
+ timeoutMS
899
+ } ) ,
900
+ this
901
+ ) ;
902
+ } else {
903
+ return this . timeoutContext ?. refreshed ( ) ;
904
+ }
905
+ } ;
875
906
try {
876
907
if (
877
908
! this . isKilled &&
@@ -884,23 +915,13 @@ export abstract class AbstractCursor<
884
915
this . isKilled = true ;
885
916
const cursorId = this . cursorId ;
886
917
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
- }
918
+
898
919
await executeOperation (
899
920
this . cursorClient ,
900
921
new KillCursorsOperation ( cursorId , this . cursorNamespace , this . selectedServer , {
901
922
session
902
923
} ) ,
903
- timeoutContext
924
+ timeoutContextForKillCursors ( )
904
925
) ;
905
926
}
906
927
} catch ( error ) {
@@ -1042,3 +1063,54 @@ class ReadableCursorStream extends Readable {
1042
1063
}
1043
1064
1044
1065
configureResourceManagement ( AbstractCursor . prototype ) ;
1066
+
1067
+ /**
1068
+ * @internal
1069
+ * The cursor timeout context is a wrapper around a timeout context
1070
+ * that keeps track of the "owner" of the cursor. For timeout contexts
1071
+ * instantiated inside a cursor, the owner will be the cursor.
1072
+ *
1073
+ * All timeout behavior is exactly the same as the wrapped timeout context's.
1074
+ */
1075
+ export class CursorTimeoutContext extends TimeoutContext {
1076
+ constructor (
1077
+ public timeoutContext : TimeoutContext ,
1078
+ public owner : symbol | AbstractCursor
1079
+ ) {
1080
+ super ( ) ;
1081
+ }
1082
+ override get serverSelectionTimeout ( ) : Timeout | null {
1083
+ return this . timeoutContext . serverSelectionTimeout ;
1084
+ }
1085
+ override get connectionCheckoutTimeout ( ) : Timeout | null {
1086
+ return this . timeoutContext . connectionCheckoutTimeout ;
1087
+ }
1088
+ override get clearServerSelectionTimeout ( ) : boolean {
1089
+ return this . timeoutContext . clearServerSelectionTimeout ;
1090
+ }
1091
+ override get clearConnectionCheckoutTimeout ( ) : boolean {
1092
+ return this . timeoutContext . clearConnectionCheckoutTimeout ;
1093
+ }
1094
+ override get timeoutForSocketWrite ( ) : Timeout | null {
1095
+ return this . timeoutContext . timeoutForSocketWrite ;
1096
+ }
1097
+ override get timeoutForSocketRead ( ) : Timeout | null {
1098
+ return this . timeoutContext . timeoutForSocketRead ;
1099
+ }
1100
+ override csotEnabled ( ) : this is CSOTTimeoutContext {
1101
+ return this . timeoutContext . csotEnabled ( ) ;
1102
+ }
1103
+ override refresh ( ) : void {
1104
+ return this . timeoutContext . refresh ( ) ;
1105
+ }
1106
+ override clear ( ) : void {
1107
+ return this . timeoutContext . clear ( ) ;
1108
+ }
1109
+ override get maxTimeMS ( ) : number | null {
1110
+ return this . timeoutContext . maxTimeMS ;
1111
+ }
1112
+
1113
+ override refreshed ( ) : CursorTimeoutContext {
1114
+ return new CursorTimeoutContext ( this . timeoutContext . refreshed ( ) , this . owner ) ;
1115
+ }
1116
+ }
0 commit comments