@@ -4,6 +4,7 @@ import { promisify } from 'util';
44import { BSONSerializeOptions , Document , Long , pluckBSONSerializeOptions } from '../bson' ;
55import {
66 AnyError ,
7+ MongoAPIError ,
78 MongoCursorExhaustedError ,
89 MongoCursorInUseError ,
910 MongoInvalidArgumentError ,
@@ -305,7 +306,18 @@ export abstract class AbstractCursor<
305306 while ( true ) {
306307 const document = await this . next ( ) ;
307308
308- if ( document == null ) {
309+ // Intentional strict null check, because users can map cursors to falsey values.
310+ // We allow mapping to all values except for null.
311+ // eslint-disable-next-line no-restricted-syntax
312+ if ( document === null ) {
313+ if ( ! this . closed ) {
314+ const message =
315+ 'Cursor returned a `null` document, but the cursor is not exhausted. Mapping documents to `null` is not supported in the cursor transform.' ;
316+
317+ await cleanupCursorAsync ( this , { needsToEmitClosed : true } ) . catch ( ( ) => null ) ;
318+
319+ throw new MongoAPIError ( message ) ;
320+ }
309321 break ;
310322 }
311323
@@ -504,6 +516,29 @@ export abstract class AbstractCursor<
504516 * this function's transform.
505517 *
506518 * @remarks
519+ *
520+ * **Note** Cursors use `null` internally to indicate that there are no more documents in the cursor. Providing a mapping
521+ * function that maps values to `null` will result in the cursor closing itself before it has finished iterating
522+ * all documents. This will **not** result in a memory leak, just surprising behavior. For example:
523+ *
524+ * ```typescript
525+ * const cursor = collection.find({});
526+ * cursor.map(() => null);
527+ *
528+ * const documents = await cursor.toArray();
529+ * // documents is always [], regardless of how many documents are in the collection.
530+ * ```
531+ *
532+ * Other falsey values are allowed:
533+ *
534+ * ```typescript
535+ * const cursor = collection.find({});
536+ * cursor.map(() => '');
537+ *
538+ * const documents = await cursor.toArray();
539+ * // documents is now an array of empty strings
540+ * ```
541+ *
507542 * **Note for Typescript Users:** adding a transform changes the return type of the iteration of this cursor,
508543 * it **does not** return a new instance of a cursor. This means when calling map,
509544 * you should always assign the result to a new variable in order to get a correctly typed cursor variable.
@@ -657,7 +692,7 @@ export abstract class AbstractCursor<
657692 * a significant refactor.
658693 */
659694 [ kInit ] ( callback : Callback < TSchema | null > ) : void {
660- this . _initialize ( this [ kSession ] , ( err , state ) => {
695+ this . _initialize ( this [ kSession ] , ( error , state ) => {
661696 if ( state ) {
662697 const response = state . response ;
663698 this [ kServer ] = state . server ;
@@ -689,8 +724,12 @@ export abstract class AbstractCursor<
689724 // the cursor is now initialized, even if an error occurred or it is dead
690725 this [ kInitialized ] = true ;
691726
692- if ( err || cursorIsDead ( this ) ) {
693- return cleanupCursor ( this , { error : err } , ( ) => callback ( err , nextDocument ( this ) ) ) ;
727+ if ( error ) {
728+ return cleanupCursor ( this , { error } , ( ) => callback ( error , undefined ) ) ;
729+ }
730+
731+ if ( cursorIsDead ( this ) ) {
732+ return cleanupCursor ( this , undefined , ( ) => callback ( ) ) ;
694733 }
695734
696735 callback ( ) ;
@@ -743,11 +782,8 @@ export function next<T>(
743782
744783 if ( cursorId == null ) {
745784 // All cursors must operate within a session, one must be made implicitly if not explicitly provided
746- cursor [ kInit ] ( ( err , value ) => {
785+ cursor [ kInit ] ( err => {
747786 if ( err ) return callback ( err ) ;
748- if ( value ) {
749- return callback ( undefined , value ) ;
750- }
751787 return next ( cursor , blocking , callback ) ;
752788 } ) ;
753789
0 commit comments