@@ -361,7 +361,7 @@ export abstract class AbstractCursor<
361
361
return true ;
362
362
}
363
363
364
- const doc = await nextAsync < TSchema > ( this , true ) ;
364
+ const doc = await next < TSchema > ( this , { blocking : true , transform : false } ) ;
365
365
366
366
if ( doc ) {
367
367
this [ kDocuments ] . unshift ( doc ) ;
@@ -377,7 +377,7 @@ export abstract class AbstractCursor<
377
377
throw new MongoCursorExhaustedError ( ) ;
378
378
}
379
379
380
- return nextAsync ( this , true ) ;
380
+ return next ( this , { blocking : true , transform : true } ) ;
381
381
}
382
382
383
383
/**
@@ -388,7 +388,7 @@ export abstract class AbstractCursor<
388
388
throw new MongoCursorExhaustedError ( ) ;
389
389
}
390
390
391
- return nextAsync ( this , false ) ;
391
+ return next ( this , { blocking : false , transform : true } ) ;
392
392
}
393
393
394
394
/**
@@ -680,88 +680,112 @@ export abstract class AbstractCursor<
680
680
}
681
681
}
682
682
683
- function nextDocument < T > ( cursor : AbstractCursor < T > ) : T | null {
684
- const doc = cursor [ kDocuments ] . shift ( ) ;
685
-
686
- if ( doc && cursor [ kTransform ] ) {
687
- return cursor [ kTransform ] ( doc ) as T ;
688
- }
689
-
690
- return doc ;
691
- }
692
-
693
- const nextAsync = promisify (
694
- next as < T > (
695
- cursor : AbstractCursor < T > ,
696
- blocking : boolean ,
697
- callback : ( e : Error , r : T | null ) => void
698
- ) => void
699
- ) ;
700
-
701
683
/**
702
684
* @param cursor - the cursor on which to call `next`
703
685
* @param blocking - a boolean indicating whether or not the cursor should `block` until data
704
686
* is available. Generally, this flag is set to `false` because if the getMore returns no documents,
705
687
* the cursor has been exhausted. In certain scenarios (ChangeStreams, tailable await cursors and
706
688
* `tryNext`, for example) blocking is necessary because a getMore returning no documents does
707
689
* not indicate the end of the cursor.
708
- * @param callback - callback to return the result to the caller
709
- * @returns
690
+ * @param transform - if true, the cursor's transform function is applied to the result document (if the transform exists)
691
+ * @returns the next document in the cursor, or `null`. When `blocking` is `true`, a `null` document means
692
+ * the cursor has been exhausted. Otherwise, it means that there is no document available in the cursor's buffer.
710
693
*/
711
- export function next < T > (
694
+ async function next < T > (
712
695
cursor : AbstractCursor < T > ,
713
- blocking : boolean ,
714
- callback : Callback < T | null >
715
- ) : void {
696
+ {
697
+ blocking,
698
+ transform
699
+ } : {
700
+ blocking : boolean ;
701
+ transform : boolean ;
702
+ }
703
+ ) : Promise < T | null > {
716
704
const cursorId = cursor [ kId ] ;
717
705
if ( cursor . closed ) {
718
- return callback ( undefined , null ) ;
706
+ return null ;
719
707
}
720
708
721
709
if ( cursor [ kDocuments ] . length !== 0 ) {
722
- callback ( undefined , nextDocument < T > ( cursor ) ) ;
723
- return ;
710
+ const doc = cursor [ kDocuments ] . shift ( ) ;
711
+
712
+ if ( doc != null && transform && cursor [ kTransform ] ) {
713
+ try {
714
+ return cursor [ kTransform ] ( doc ) ;
715
+ } catch ( error ) {
716
+ await cleanupCursorAsync ( cursor , { error, needsToEmitClosed : true } ) . catch ( ( ) => {
717
+ // `cleanupCursorAsync` should never throw, but if it does we want to throw the original
718
+ // error instead.
719
+ } ) ;
720
+ throw error ;
721
+ }
722
+ }
723
+
724
+ return doc ;
724
725
}
725
726
726
727
if ( cursorId == null ) {
727
728
// All cursors must operate within a session, one must be made implicitly if not explicitly provided
728
- cursor [ kInit ] ( err => {
729
- if ( err ) return callback ( err ) ;
730
- return next ( cursor , blocking , callback ) ;
731
- } ) ;
732
-
733
- return ;
729
+ const init = promisify ( cb => cursor [ kInit ] ( cb ) ) ;
730
+ await init ( ) ;
731
+ return next ( cursor , { blocking, transform } ) ;
734
732
}
735
733
736
734
if ( cursorIsDead ( cursor ) ) {
737
- return cleanupCursor ( cursor , undefined , ( ) => callback ( undefined , null ) ) ;
735
+ // if the cursor is dead, we clean it up
736
+ // cleanupCursorAsync should never throw, but if it does it indicates a bug in the driver
737
+ // and we should surface the error
738
+ await cleanupCursorAsync ( cursor , { } ) ;
739
+ return null ;
738
740
}
739
741
740
742
// otherwise need to call getMore
741
743
const batchSize = cursor [ kOptions ] . batchSize || 1000 ;
742
- cursor . _getMore ( batchSize , ( error , response ) => {
743
- if ( response ) {
744
- const cursorId =
745
- typeof response . cursor . id === 'number'
746
- ? Long . fromNumber ( response . cursor . id )
747
- : typeof response . cursor . id === 'bigint'
748
- ? Long . fromBigInt ( response . cursor . id )
749
- : response . cursor . id ;
744
+ const getMore = promisify ( ( batchSize : number , cb : Callback < Document | undefined > ) =>
745
+ cursor . _getMore ( batchSize , cb )
746
+ ) ;
750
747
751
- cursor [ kDocuments ] . pushMany ( response . cursor . nextBatch ) ;
752
- cursor [ kId ] = cursorId ;
748
+ let response : Document | undefined ;
749
+ try {
750
+ response = await getMore ( batchSize ) ;
751
+ } catch ( error ) {
752
+ if ( error ) {
753
+ await cleanupCursorAsync ( cursor , { error } ) . catch ( ( ) => {
754
+ // `cleanupCursorAsync` should never throw, but if it does we want to throw the original
755
+ // error instead.
756
+ } ) ;
757
+ throw error ;
753
758
}
759
+ }
754
760
755
- if ( error || cursorIsDead ( cursor ) ) {
756
- return cleanupCursor ( cursor , { error } , ( ) => callback ( error , nextDocument < T > ( cursor ) ) ) ;
757
- }
761
+ if ( response ) {
762
+ const cursorId =
763
+ typeof response . cursor . id === 'number'
764
+ ? Long . fromNumber ( response . cursor . id )
765
+ : typeof response . cursor . id === 'bigint'
766
+ ? Long . fromBigInt ( response . cursor . id )
767
+ : response . cursor . id ;
758
768
759
- if ( cursor [ kDocuments ] . length === 0 && blocking === false ) {
760
- return callback ( undefined , null ) ;
761
- }
769
+ cursor [ kDocuments ] . pushMany ( response . cursor . nextBatch ) ;
770
+ cursor [ kId ] = cursorId ;
771
+ }
762
772
763
- next ( cursor , blocking , callback ) ;
764
- } ) ;
773
+ if ( cursorIsDead ( cursor ) ) {
774
+ // If we successfully received a response from a cursor BUT the cursor indicates that it is exhausted,
775
+ // we intentionally clean up the cursor to release its session back into the pool before the cursor
776
+ // is iterated. This prevents a cursor that is exhausted on the server from holding
777
+ // onto a session indefinitely until the AbstractCursor is iterated.
778
+ //
779
+ // cleanupCursorAsync should never throw, but if it does it indicates a bug in the driver
780
+ // and we should surface the error
781
+ await cleanupCursorAsync ( cursor , { } ) ;
782
+ }
783
+
784
+ if ( cursor [ kDocuments ] . length === 0 && blocking === false ) {
785
+ return null ;
786
+ }
787
+
788
+ return next ( cursor , { blocking, transform } ) ;
765
789
}
766
790
767
791
function cursorIsDead ( cursor : AbstractCursor ) : boolean {
@@ -781,6 +805,10 @@ function cleanupCursor(
781
805
const server = cursor [ kServer ] ;
782
806
const session = cursor [ kSession ] ;
783
807
const error = options ?. error ;
808
+
809
+ // Cursors only emit closed events once the client-side cursor has been exhausted fully or there
810
+ // was an error. Notably, when the server returns a cursor id of 0 and a non-empty batch, we
811
+ // cleanup the cursor but don't emit a `close` event.
784
812
const needsToEmitClosed = options ?. needsToEmitClosed ?? cursor [ kDocuments ] . length === 0 ;
785
813
786
814
if ( error ) {
@@ -881,8 +909,21 @@ class ReadableCursorStream extends Readable {
881
909
}
882
910
883
911
private _readNext ( ) {
884
- next ( this . _cursor , true , ( err , result ) => {
885
- if ( err ) {
912
+ next ( this . _cursor , { blocking : true , transform : true } ) . then (
913
+ result => {
914
+ if ( result == null ) {
915
+ this . push ( null ) ;
916
+ } else if ( this . destroyed ) {
917
+ this . _cursor . close ( ) . catch ( ( ) => null ) ;
918
+ } else {
919
+ if ( this . push ( result ) ) {
920
+ return this . _readNext ( ) ;
921
+ }
922
+
923
+ this . _readInProgress = false ;
924
+ }
925
+ } ,
926
+ err => {
886
927
// NOTE: This is questionable, but we have a test backing the behavior. It seems the
887
928
// desired behavior is that a stream ends cleanly when a user explicitly closes
888
929
// a client during iteration. Alternatively, we could do the "right" thing and
@@ -911,18 +952,6 @@ class ReadableCursorStream extends Readable {
911
952
// See NODE-4475.
912
953
return this . destroy ( err ) ;
913
954
}
914
-
915
- if ( result == null ) {
916
- this . push ( null ) ;
917
- } else if ( this . destroyed ) {
918
- this . _cursor . close ( ) . catch ( ( ) => null ) ;
919
- } else {
920
- if ( this . push ( result ) ) {
921
- return this . _readNext ( ) ;
922
- }
923
-
924
- this . _readInProgress = false ;
925
- }
926
- } ) ;
955
+ ) ;
927
956
}
928
957
}
0 commit comments