From d63bf6fae303ec921335c363865e86c912286d6c Mon Sep 17 00:00:00 2001 From: Warren James Date: Thu, 12 Sep 2024 11:52:46 -0400 Subject: [PATCH] fix(NODE-6367): enable mixed use of iteration APIs (#4234) --- src/cursor/abstract_cursor.ts | 2 +- .../crud/find_cursor_methods.test.js | 344 ++++++++++++++++++ 2 files changed, 345 insertions(+), 1 deletion(-) diff --git a/src/cursor/abstract_cursor.ts b/src/cursor/abstract_cursor.ts index 54447fe70e..eb2e6e7aeb 100644 --- a/src/cursor/abstract_cursor.ts +++ b/src/cursor/abstract_cursor.ts @@ -309,7 +309,7 @@ export abstract class AbstractCursor< return bufferedDocs; } async *[Symbol.asyncIterator](): AsyncGenerator { - if (this.isClosed) { + if (this.closed) { return; } diff --git a/test/integration/crud/find_cursor_methods.test.js b/test/integration/crud/find_cursor_methods.test.js index 6e7066d5fa..42eeda3e81 100644 --- a/test/integration/crud/find_cursor_methods.test.js +++ b/test/integration/crud/find_cursor_methods.test.js @@ -1,6 +1,7 @@ 'use strict'; const { expect } = require('chai'); const { filterForCommands } = require('../shared'); +const { promiseWithResolvers, MongoCursorExhaustedError } = require('../../mongodb'); describe('Find Cursor', function () { let client; @@ -361,4 +362,347 @@ describe('Find Cursor', function () { } }); }); + + describe('mixing iteration APIs', function () { + let client; + let collection; + let cursor; + + beforeEach(async function () { + client = this.configuration.newClient(); + await client.connect(); + collection = client.db('next-symbolasynciterator').collection('bar'); + await collection.deleteMany({}, { writeConcern: { w: 'majority' } }); + await collection.insertMany([{ a: 1 }, { a: 2 }], { writeConcern: { w: 'majority' } }); + }); + + afterEach(async function () { + await cursor.close(); + await client.close(); + }); + + context('when all documents are retrieved in the first batch', function () { + it('allows combining iteration modes', async function () { + let count = 0; + cursor = collection.find().map(doc => { + count++; + return doc; + }); + + await cursor.next(); + // eslint-disable-next-line no-unused-vars + for await (const _ of cursor) { + /* empty */ + } + + expect(count).to.equal(2); + }); + + it('works with next + next() loop', async function () { + let count = 0; + cursor = collection.find().map(doc => { + count++; + return doc; + }); + + await cursor.next(); + + let doc; + while ((doc = (await cursor.next()) && doc != null)) { + /** empty */ + } + + expect(count).to.equal(2); + }); + + context('when next() is called in a loop after a single invocation', function () { + it('iterates over all documents', async function () { + let count = 0; + cursor = collection.find({}).map(doc => { + count++; + return doc; + }); + + await cursor.next(); + + let doc; + while ((doc = (await cursor.next()) && doc != null)) { + /** empty */ + } + + expect(count).to.equal(2); + }); + }); + + context( + 'when cursor.next() is called after cursor.stream() is partially iterated', + function () { + it('returns null', async function () { + cursor = collection.find({}); + + const stream = cursor.stream(); + const { promise, resolve, reject } = promiseWithResolvers(); + + stream.once('data', v => { + resolve(v); + }); + + stream.once('error', v => { + reject(v); + }); + await promise; + + expect(await cursor.next()).to.be.null; + }); + } + ); + + context('when cursor.tryNext() is called after cursor.stream()', function () { + it('returns null', async function () { + cursor = collection.find({}); + + const stream = cursor.stream(); + const { promise, resolve, reject } = promiseWithResolvers(); + + stream.once('data', v => { + resolve(v); + }); + + stream.once('error', v => { + reject(v); + }); + await promise; + + expect(await cursor.tryNext()).to.be.null; + }); + }); + + context( + 'when cursor.[Symbol.asyncIterator] is called after cursor.stream() is partly iterated', + function () { + it('returns an empty iterator', async function () { + cursor = collection.find({}); + + const stream = cursor.stream(); + const { promise, resolve, reject } = promiseWithResolvers(); + + stream.once('data', v => { + resolve(v); + }); + + stream.once('error', v => { + reject(v); + }); + await promise; + + let count = 0; + // eslint-disable-next-line no-unused-vars + for await (const _ of cursor) { + count++; + } + + expect(count).to.equal(0); + }); + } + ); + + context('when cursor.readBufferedDocuments() is called after cursor.next()', function () { + it('returns an array with remaining buffered documents', async function () { + cursor = collection.find({}); + + await cursor.next(); + const docs = cursor.readBufferedDocuments(); + + expect(docs).to.have.lengthOf(1); + }); + }); + + context('when cursor.next() is called after cursor.toArray()', function () { + it('returns null', async function () { + cursor = collection.find({}); + + await cursor.toArray(); + expect(await cursor.next()).to.be.null; + }); + }); + + context('when cursor.tryNext is called after cursor.toArray()', function () { + it('returns null', async function () { + cursor = collection.find({}); + + await cursor.toArray(); + expect(await cursor.tryNext()).to.be.null; + }); + }); + + context('when cursor.[Symbol.asyncIterator] is called after cursor.toArray()', function () { + it('should not iterate', async function () { + cursor = collection.find({}); + + await cursor.toArray(); + // eslint-disable-next-line no-unused-vars + for await (const _ of cursor) { + expect.fail('should not iterate'); + } + }); + }); + + context('when cursor.readBufferedDocuments() is called after cursor.toArray()', function () { + it('return and empty array', async function () { + cursor = collection.find({}); + + await cursor.toArray(); + expect(cursor.readBufferedDocuments()).to.have.lengthOf(0); + }); + }); + + context('when cursor.stream() is called after cursor.toArray()', function () { + it('returns an empty stream', async function () { + cursor = collection.find({}); + await cursor.toArray(); + + const s = cursor.stream(); + const { promise, resolve, reject } = promiseWithResolvers(); + + s.once('data', d => { + reject(d); + }); + + s.once('end', d => { + resolve(d); + }); + + expect(await promise).to.be.undefined; + }); + }); + }); + + context('when there are documents that are not retrieved in the first batch', function () { + it('allows combining next() and for await syntax', async function () { + let count = 0; + cursor = collection.find({}, { batchSize: 1 }).map(doc => { + count++; + return doc; + }); + + await cursor.next(); + // eslint-disable-next-line no-unused-vars + for await (const _ of cursor) { + /* empty */ + } + + expect(count).to.equal(2); + }); + + context( + 'when a cursor is partially iterated with for await and then .next() is called', + function () { + it('throws a MongoCursorExhaustedError', async function () { + cursor = collection.find({}, { batchSize: 1 }); + + // eslint-disable-next-line no-unused-vars + for await (const _ of cursor) { + /* empty */ + break; + } + + const maybeError = await cursor.next().then( + () => null, + e => e + ); + expect(maybeError).to.be.instanceof(MongoCursorExhaustedError); + }); + } + ); + + context('when next() is called in a loop after a single invocation', function () { + it('iterates over all documents', async function () { + let count = 0; + cursor = collection.find({}, { batchSize: 1 }).map(doc => { + count++; + return doc; + }); + + await cursor.next(); + + let doc; + while ((doc = (await cursor.next()) && doc != null)) { + /** empty */ + } + + expect(count).to.equal(2); + }); + }); + + context('when cursor.readBufferedDocuments() is called after cursor.next()', function () { + it('returns an empty array', async function () { + cursor = collection.find({}, { batchSize: 1 }); + + await cursor.next(); + const docs = cursor.readBufferedDocuments(); + + expect(docs).to.have.lengthOf(0); + }); + }); + + context('when cursor.next() is called after cursor.toArray()', function () { + it('returns null', async function () { + cursor = collection.find({}, { batchSize: 1 }); + + await cursor.toArray(); + expect(await cursor.next()).to.be.null; + }); + }); + + context('when cursor.tryNext is called after cursor.toArray()', function () { + it('returns null', async function () { + cursor = collection.find({}, { batchSize: 1 }); + + await cursor.toArray(); + expect(await cursor.tryNext()).to.be.null; + }); + }); + + context('when cursor.[Symbol.asyncIterator] is called after cursor.toArray()', function () { + it('should not iterate', async function () { + cursor = collection.find({}, { batchSize: 1 }); + + await cursor.toArray(); + // eslint-disable-next-line no-unused-vars + for await (const _ of cursor) { + expect.fail('should not iterate'); + } + }); + }); + + context('when cursor.readBufferedDocuments() is called after cursor.toArray()', function () { + it('return and empty array', async function () { + cursor = collection.find({}, { batchSize: 1 }); + + await cursor.toArray(); + expect(cursor.readBufferedDocuments()).to.have.lengthOf(0); + }); + }); + + context('when cursor.stream() is called after cursor.toArray()', function () { + it('returns an empty stream', async function () { + cursor = collection.find({}, { batchSize: 1 }); + await cursor.toArray(); + + const s = cursor.stream(); + const { promise, resolve, reject } = promiseWithResolvers(); + + s.once('data', d => { + reject(d); + }); + + s.once('end', d => { + resolve(d); + }); + + expect(await promise).to.be.undefined; + }); + }); + }); + }); });