@@ -3,14 +3,15 @@ import type { Readable } from 'stream';
3
3
import type { Binary , Document , Timestamp } from './bson' ;
4
4
import { Collection } from './collection' ;
5
5
import { CHANGE , CLOSE , END , ERROR , INIT , MORE , RESPONSE , RESUME_TOKEN_CHANGED } from './constants' ;
6
- import type { AbstractCursorEvents , CursorStreamOptions } from './cursor/abstract_cursor' ;
6
+ import { type CursorStreamOptions , CursorTimeoutContext } from './cursor/abstract_cursor' ;
7
7
import { ChangeStreamCursor , type ChangeStreamCursorOptions } from './cursor/change_stream_cursor' ;
8
8
import { Db } from './db' ;
9
9
import {
10
10
type AnyError ,
11
11
isResumableError ,
12
12
MongoAPIError ,
13
13
MongoChangeStreamError ,
14
+ MongoOperationTimeoutError ,
14
15
MongoRuntimeError
15
16
} from './error' ;
16
17
import { MongoClient } from './mongo_client' ;
@@ -20,6 +21,7 @@ import type { CollationOptions, OperationParent } from './operations/command';
20
21
import type { ReadPreference } from './read_preference' ;
21
22
import { type AsyncDisposable , configureResourceManagement } from './resource_management' ;
22
23
import type { ServerSessionId } from './sessions' ;
24
+ import { CSOTTimeoutContext , type TimeoutContext } from './timeout' ;
23
25
import { filterOptions , getTopology , type MongoDBNamespace , squashError } from './utils' ;
24
26
25
27
/** @internal */
@@ -538,7 +540,12 @@ export type ChangeStreamEvents<
538
540
end ( ) : void ;
539
541
error ( error : Error ) : void ;
540
542
change ( change : TChange ) : void ;
541
- } & AbstractCursorEvents ;
543
+ /**
544
+ * @remarks Note that the `close` event is currently emitted whenever the internal `ChangeStreamCursor`
545
+ * instance is closed, which can occur multiple times for a given `ChangeStream` instance.
546
+ */
547
+ close ( ) : void ;
548
+ } ;
542
549
543
550
/**
544
551
* Creates a new Change Stream instance. Normally created using {@link Collection#watch|Collection.watch()}.
@@ -609,6 +616,13 @@ export class ChangeStream<
609
616
*/
610
617
static readonly RESUME_TOKEN_CHANGED = RESUME_TOKEN_CHANGED ;
611
618
619
+ private timeoutContext ?: TimeoutContext ;
620
+ /**
621
+ * Note that this property is here to uniquely identify a ChangeStream instance as the owner of
622
+ * the {@link CursorTimeoutContext} instance (see {@link ChangeStream._createChangeStreamCursor}) to ensure
623
+ * that {@link AbstractCursor.close} does not mutate the timeoutContext.
624
+ */
625
+ private contextOwner : symbol ;
612
626
/**
613
627
* @internal
614
628
*
@@ -624,20 +638,25 @@ export class ChangeStream<
624
638
625
639
this . pipeline = pipeline ;
626
640
this . options = { ...options } ;
641
+ let serverSelectionTimeoutMS : number ;
627
642
delete this . options . writeConcern ;
628
643
629
644
if ( parent instanceof Collection ) {
630
645
this . type = CHANGE_DOMAIN_TYPES . COLLECTION ;
646
+ serverSelectionTimeoutMS = parent . s . db . client . options . serverSelectionTimeoutMS ;
631
647
} else if ( parent instanceof Db ) {
632
648
this . type = CHANGE_DOMAIN_TYPES . DATABASE ;
649
+ serverSelectionTimeoutMS = parent . client . options . serverSelectionTimeoutMS ;
633
650
} else if ( parent instanceof MongoClient ) {
634
651
this . type = CHANGE_DOMAIN_TYPES . CLUSTER ;
652
+ serverSelectionTimeoutMS = parent . options . serverSelectionTimeoutMS ;
635
653
} else {
636
654
throw new MongoChangeStreamError (
637
655
'Parent provided to ChangeStream constructor must be an instance of Collection, Db, or MongoClient'
638
656
) ;
639
657
}
640
658
659
+ this . contextOwner = Symbol ( ) ;
641
660
this . parent = parent ;
642
661
this . namespace = parent . s . namespace ;
643
662
if ( ! this . options . readPreference && parent . readPreference ) {
@@ -662,6 +681,13 @@ export class ChangeStream<
662
681
this [ kCursorStream ] ?. removeAllListeners ( 'data' ) ;
663
682
}
664
683
} ) ;
684
+
685
+ if ( this . options . timeoutMS != null ) {
686
+ this . timeoutContext = new CSOTTimeoutContext ( {
687
+ timeoutMS : this . options . timeoutMS ,
688
+ serverSelectionTimeoutMS
689
+ } ) ;
690
+ }
665
691
}
666
692
667
693
/** @internal */
@@ -681,22 +707,30 @@ export class ChangeStream<
681
707
// This loop continues until either a change event is received or until a resume attempt
682
708
// fails.
683
709
684
- while ( true ) {
685
- try {
686
- const hasNext = await this . cursor . hasNext ( ) ;
687
- return hasNext ;
688
- } catch ( error ) {
710
+ this . timeoutContext ?. refresh ( ) ;
711
+ try {
712
+ while ( true ) {
689
713
try {
690
- await this . _processErrorIteratorMode ( error ) ;
714
+ const hasNext = await this . cursor . hasNext ( ) ;
715
+ return hasNext ;
691
716
} catch ( error ) {
692
717
try {
693
- await this . close ( ) ;
718
+ await this . _processErrorIteratorMode ( error , this . cursor . id != null ) ;
694
719
} catch ( error ) {
695
- squashError ( error ) ;
720
+ if ( error instanceof MongoOperationTimeoutError && this . cursor . id == null ) {
721
+ throw error ;
722
+ }
723
+ try {
724
+ await this . close ( ) ;
725
+ } catch ( error ) {
726
+ squashError ( error ) ;
727
+ }
728
+ throw error ;
696
729
}
697
- throw error ;
698
730
}
699
731
}
732
+ } finally {
733
+ this . timeoutContext ?. clear ( ) ;
700
734
}
701
735
}
702
736
@@ -706,24 +740,32 @@ export class ChangeStream<
706
740
// Change streams must resume indefinitely while each resume event succeeds.
707
741
// This loop continues until either a change event is received or until a resume attempt
708
742
// fails.
743
+ this . timeoutContext ?. refresh ( ) ;
709
744
710
- while ( true ) {
711
- try {
712
- const change = await this . cursor . next ( ) ;
713
- const processedChange = this . _processChange ( change ?? null ) ;
714
- return processedChange ;
715
- } catch ( error ) {
745
+ try {
746
+ while ( true ) {
716
747
try {
717
- await this . _processErrorIteratorMode ( error ) ;
748
+ const change = await this . cursor . next ( ) ;
749
+ const processedChange = this . _processChange ( change ?? null ) ;
750
+ return processedChange ;
718
751
} catch ( error ) {
719
752
try {
720
- await this . close ( ) ;
753
+ await this . _processErrorIteratorMode ( error , this . cursor . id != null ) ;
721
754
} catch ( error ) {
722
- squashError ( error ) ;
755
+ if ( error instanceof MongoOperationTimeoutError && this . cursor . id == null ) {
756
+ throw error ;
757
+ }
758
+ try {
759
+ await this . close ( ) ;
760
+ } catch ( error ) {
761
+ squashError ( error ) ;
762
+ }
763
+ throw error ;
723
764
}
724
- throw error ;
725
765
}
726
766
}
767
+ } finally {
768
+ this . timeoutContext ?. clear ( ) ;
727
769
}
728
770
}
729
771
@@ -735,23 +777,29 @@ export class ChangeStream<
735
777
// Change streams must resume indefinitely while each resume event succeeds.
736
778
// This loop continues until either a change event is received or until a resume attempt
737
779
// fails.
780
+ this . timeoutContext ?. refresh ( ) ;
738
781
739
- while ( true ) {
740
- try {
741
- const change = await this . cursor . tryNext ( ) ;
742
- return change ?? null ;
743
- } catch ( error ) {
782
+ try {
783
+ while ( true ) {
744
784
try {
745
- await this . _processErrorIteratorMode ( error ) ;
785
+ const change = await this . cursor . tryNext ( ) ;
786
+ return change ?? null ;
746
787
} catch ( error ) {
747
788
try {
748
- await this . close ( ) ;
789
+ await this . _processErrorIteratorMode ( error , this . cursor . id != null ) ;
749
790
} catch ( error ) {
750
- squashError ( error ) ;
791
+ if ( error instanceof MongoOperationTimeoutError && this . cursor . id == null ) throw error ;
792
+ try {
793
+ await this . close ( ) ;
794
+ } catch ( error ) {
795
+ squashError ( error ) ;
796
+ }
797
+ throw error ;
751
798
}
752
- throw error ;
753
799
}
754
800
}
801
+ } finally {
802
+ this . timeoutContext ?. clear ( ) ;
755
803
}
756
804
}
757
805
@@ -784,6 +832,8 @@ export class ChangeStream<
784
832
* Frees the internal resources used by the change stream.
785
833
*/
786
834
async close ( ) : Promise < void > {
835
+ this . timeoutContext ?. clear ( ) ;
836
+ this . timeoutContext = undefined ;
787
837
this [ kClosed ] = true ;
788
838
789
839
const cursor = this . cursor ;
@@ -866,7 +916,12 @@ export class ChangeStream<
866
916
client ,
867
917
this . namespace ,
868
918
pipeline ,
869
- options
919
+ {
920
+ ...options ,
921
+ timeoutContext : this . timeoutContext
922
+ ? new CursorTimeoutContext ( this . timeoutContext , this . contextOwner )
923
+ : undefined
924
+ }
870
925
) ;
871
926
872
927
for ( const event of CHANGE_STREAM_EVENTS ) {
@@ -899,8 +954,9 @@ export class ChangeStream<
899
954
} catch ( error ) {
900
955
this . emit ( ChangeStream . ERROR , error ) ;
901
956
}
957
+ this . timeoutContext ?. refresh ( ) ;
902
958
} ) ;
903
- stream . on ( 'error' , error => this . _processErrorStreamMode ( error ) ) ;
959
+ stream . on ( 'error' , error => this . _processErrorStreamMode ( error , this . cursor . id != null ) ) ;
904
960
}
905
961
906
962
/** @internal */
@@ -942,24 +998,30 @@ export class ChangeStream<
942
998
}
943
999
944
1000
/** @internal */
945
- private _processErrorStreamMode ( changeStreamError : AnyError ) {
1001
+ private _processErrorStreamMode ( changeStreamError : AnyError , cursorInitialized : boolean ) {
946
1002
// If the change stream has been closed explicitly, do not process error.
947
1003
if ( this [ kClosed ] ) return ;
948
1004
949
- if ( this . cursor . id != null && isResumableError ( changeStreamError , this . cursor . maxWireVersion ) ) {
1005
+ if (
1006
+ cursorInitialized &&
1007
+ ( isResumableError ( changeStreamError , this . cursor . maxWireVersion ) ||
1008
+ changeStreamError instanceof MongoOperationTimeoutError )
1009
+ ) {
950
1010
this . _endStream ( ) ;
951
1011
952
- this . cursor . close ( ) . then ( undefined , squashError ) ;
953
-
954
- const topology = getTopology ( this . parent ) ;
955
- topology
956
- . selectServer ( this . cursor . readPreference , {
957
- operationName : 'reconnect topology in change stream'
958
- } )
959
-
1012
+ this . cursor
1013
+ . close ( )
1014
+ . then (
1015
+ ( ) => this . _resume ( changeStreamError ) ,
1016
+ e => {
1017
+ squashError ( e ) ;
1018
+ return this . _resume ( changeStreamError ) ;
1019
+ }
1020
+ )
960
1021
. then (
961
1022
( ) => {
962
- this . cursor = this . _createChangeStreamCursor ( this . cursor . resumeOptions ) ;
1023
+ if ( changeStreamError instanceof MongoOperationTimeoutError )
1024
+ this . emit ( ChangeStream . ERROR , changeStreamError ) ;
963
1025
} ,
964
1026
( ) => this . _closeEmitterModeWithError ( changeStreamError )
965
1027
) ;
@@ -969,33 +1031,44 @@ export class ChangeStream<
969
1031
}
970
1032
971
1033
/** @internal */
972
- private async _processErrorIteratorMode ( changeStreamError : AnyError ) {
1034
+ private async _processErrorIteratorMode ( changeStreamError : AnyError , cursorInitialized : boolean ) {
973
1035
if ( this [ kClosed ] ) {
974
1036
// TODO(NODE-3485): Replace with MongoChangeStreamClosedError
975
1037
throw new MongoAPIError ( CHANGESTREAM_CLOSED_ERROR ) ;
976
1038
}
977
1039
978
1040
if (
979
- this . cursor . id == null ||
980
- ! isResumableError ( changeStreamError , this . cursor . maxWireVersion )
1041
+ cursorInitialized &&
1042
+ ( isResumableError ( changeStreamError , this . cursor . maxWireVersion ) ||
1043
+ changeStreamError instanceof MongoOperationTimeoutError )
981
1044
) {
1045
+ try {
1046
+ await this . cursor . close ( ) ;
1047
+ } catch ( error ) {
1048
+ squashError ( error ) ;
1049
+ }
1050
+
1051
+ await this . _resume ( changeStreamError ) ;
1052
+
1053
+ if ( changeStreamError instanceof MongoOperationTimeoutError ) throw changeStreamError ;
1054
+ } else {
982
1055
try {
983
1056
await this . close ( ) ;
984
1057
} catch ( error ) {
985
1058
squashError ( error ) ;
986
1059
}
1060
+
987
1061
throw changeStreamError ;
988
1062
}
1063
+ }
989
1064
990
- try {
991
- await this . cursor . close ( ) ;
992
- } catch ( error ) {
993
- squashError ( error ) ;
994
- }
1065
+ private async _resume ( changeStreamError : AnyError ) {
1066
+ this . timeoutContext ?. refresh ( ) ;
995
1067
const topology = getTopology ( this . parent ) ;
996
1068
try {
997
1069
await topology . selectServer ( this . cursor . readPreference , {
998
- operationName : 'reconnect topology in change stream'
1070
+ operationName : 'reconnect topology in change stream' ,
1071
+ timeoutContext : this . timeoutContext
999
1072
} ) ;
1000
1073
this . cursor = this . _createChangeStreamCursor ( this . cursor . resumeOptions ) ;
1001
1074
} catch {
0 commit comments