Skip to content

Commit 94d5d62

Browse files
committed
fix(NODE-6367): enable mixed use of iteration APIs (#4231)
1 parent 0c6dbad commit 94d5d62

File tree

2 files changed

+345
-1
lines changed

2 files changed

+345
-1
lines changed

Diff for: src/cursor/abstract_cursor.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -309,7 +309,7 @@ export abstract class AbstractCursor<
309309
return bufferedDocs;
310310
}
311311
async *[Symbol.asyncIterator](): AsyncGenerator<TSchema, void, void> {
312-
if (this.isClosed) {
312+
if (this.closed) {
313313
return;
314314
}
315315

Diff for: test/integration/crud/find_cursor_methods.test.js

+344
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
'use strict';
22
const { expect } = require('chai');
33
const { filterForCommands } = require('../shared');
4+
const { promiseWithResolvers, MongoCursorExhaustedError } = require('../../mongodb');
45

56
describe('Find Cursor', function () {
67
let client;
@@ -361,4 +362,347 @@ describe('Find Cursor', function () {
361362
}
362363
});
363364
});
365+
366+
describe('mixing iteration APIs', function () {
367+
let client;
368+
let collection;
369+
let cursor;
370+
371+
beforeEach(async function () {
372+
client = this.configuration.newClient();
373+
await client.connect();
374+
collection = client.db('next-symbolasynciterator').collection('bar');
375+
await collection.deleteMany({}, { writeConcern: { w: 'majority' } });
376+
await collection.insertMany([{ a: 1 }, { a: 2 }], { writeConcern: { w: 'majority' } });
377+
});
378+
379+
afterEach(async function () {
380+
await cursor.close();
381+
await client.close();
382+
});
383+
384+
context('when all documents are retrieved in the first batch', function () {
385+
it('allows combining iteration modes', async function () {
386+
let count = 0;
387+
cursor = collection.find().map(doc => {
388+
count++;
389+
return doc;
390+
});
391+
392+
await cursor.next();
393+
// eslint-disable-next-line no-unused-vars
394+
for await (const _ of cursor) {
395+
/* empty */
396+
}
397+
398+
expect(count).to.equal(2);
399+
});
400+
401+
it('works with next + next() loop', async function () {
402+
let count = 0;
403+
cursor = collection.find().map(doc => {
404+
count++;
405+
return doc;
406+
});
407+
408+
await cursor.next();
409+
410+
let doc;
411+
while ((doc = (await cursor.next()) && doc != null)) {
412+
/** empty */
413+
}
414+
415+
expect(count).to.equal(2);
416+
});
417+
418+
context('when next() is called in a loop after a single invocation', function () {
419+
it('iterates over all documents', async function () {
420+
let count = 0;
421+
cursor = collection.find({}).map(doc => {
422+
count++;
423+
return doc;
424+
});
425+
426+
await cursor.next();
427+
428+
let doc;
429+
while ((doc = (await cursor.next()) && doc != null)) {
430+
/** empty */
431+
}
432+
433+
expect(count).to.equal(2);
434+
});
435+
});
436+
437+
context(
438+
'when cursor.next() is called after cursor.stream() is partially iterated',
439+
function () {
440+
it('returns null', async function () {
441+
cursor = collection.find({});
442+
443+
const stream = cursor.stream();
444+
const { promise, resolve, reject } = promiseWithResolvers();
445+
446+
stream.once('data', v => {
447+
resolve(v);
448+
});
449+
450+
stream.once('error', v => {
451+
reject(v);
452+
});
453+
await promise;
454+
455+
expect(await cursor.next()).to.be.null;
456+
});
457+
}
458+
);
459+
460+
context('when cursor.tryNext() is called after cursor.stream()', function () {
461+
it('returns null', async function () {
462+
cursor = collection.find({});
463+
464+
const stream = cursor.stream();
465+
const { promise, resolve, reject } = promiseWithResolvers();
466+
467+
stream.once('data', v => {
468+
resolve(v);
469+
});
470+
471+
stream.once('error', v => {
472+
reject(v);
473+
});
474+
await promise;
475+
476+
expect(await cursor.tryNext()).to.be.null;
477+
});
478+
});
479+
480+
context(
481+
'when cursor.[Symbol.asyncIterator] is called after cursor.stream() is partly iterated',
482+
function () {
483+
it('returns an empty iterator', async function () {
484+
cursor = collection.find({});
485+
486+
const stream = cursor.stream();
487+
const { promise, resolve, reject } = promiseWithResolvers();
488+
489+
stream.once('data', v => {
490+
resolve(v);
491+
});
492+
493+
stream.once('error', v => {
494+
reject(v);
495+
});
496+
await promise;
497+
498+
let count = 0;
499+
// eslint-disable-next-line no-unused-vars
500+
for await (const _ of cursor) {
501+
count++;
502+
}
503+
504+
expect(count).to.equal(0);
505+
});
506+
}
507+
);
508+
509+
context('when cursor.readBufferedDocuments() is called after cursor.next()', function () {
510+
it('returns an array with remaining buffered documents', async function () {
511+
cursor = collection.find({});
512+
513+
await cursor.next();
514+
const docs = cursor.readBufferedDocuments();
515+
516+
expect(docs).to.have.lengthOf(1);
517+
});
518+
});
519+
520+
context('when cursor.next() is called after cursor.toArray()', function () {
521+
it('returns null', async function () {
522+
cursor = collection.find({});
523+
524+
await cursor.toArray();
525+
expect(await cursor.next()).to.be.null;
526+
});
527+
});
528+
529+
context('when cursor.tryNext is called after cursor.toArray()', function () {
530+
it('returns null', async function () {
531+
cursor = collection.find({});
532+
533+
await cursor.toArray();
534+
expect(await cursor.tryNext()).to.be.null;
535+
});
536+
});
537+
538+
context('when cursor.[Symbol.asyncIterator] is called after cursor.toArray()', function () {
539+
it('should not iterate', async function () {
540+
cursor = collection.find({});
541+
542+
await cursor.toArray();
543+
// eslint-disable-next-line no-unused-vars
544+
for await (const _ of cursor) {
545+
expect.fail('should not iterate');
546+
}
547+
});
548+
});
549+
550+
context('when cursor.readBufferedDocuments() is called after cursor.toArray()', function () {
551+
it('return and empty array', async function () {
552+
cursor = collection.find({});
553+
554+
await cursor.toArray();
555+
expect(cursor.readBufferedDocuments()).to.have.lengthOf(0);
556+
});
557+
});
558+
559+
context('when cursor.stream() is called after cursor.toArray()', function () {
560+
it('returns an empty stream', async function () {
561+
cursor = collection.find({});
562+
await cursor.toArray();
563+
564+
const s = cursor.stream();
565+
const { promise, resolve, reject } = promiseWithResolvers();
566+
567+
s.once('data', d => {
568+
reject(d);
569+
});
570+
571+
s.once('end', d => {
572+
resolve(d);
573+
});
574+
575+
expect(await promise).to.be.undefined;
576+
});
577+
});
578+
});
579+
580+
context('when there are documents that are not retrieved in the first batch', function () {
581+
it('allows combining next() and for await syntax', async function () {
582+
let count = 0;
583+
cursor = collection.find({}, { batchSize: 1 }).map(doc => {
584+
count++;
585+
return doc;
586+
});
587+
588+
await cursor.next();
589+
// eslint-disable-next-line no-unused-vars
590+
for await (const _ of cursor) {
591+
/* empty */
592+
}
593+
594+
expect(count).to.equal(2);
595+
});
596+
597+
context(
598+
'when a cursor is partially iterated with for await and then .next() is called',
599+
function () {
600+
it('throws a MongoCursorExhaustedError', async function () {
601+
cursor = collection.find({}, { batchSize: 1 });
602+
603+
// eslint-disable-next-line no-unused-vars
604+
for await (const _ of cursor) {
605+
/* empty */
606+
break;
607+
}
608+
609+
const maybeError = await cursor.next().then(
610+
() => null,
611+
e => e
612+
);
613+
expect(maybeError).to.be.instanceof(MongoCursorExhaustedError);
614+
});
615+
}
616+
);
617+
618+
context('when next() is called in a loop after a single invocation', function () {
619+
it('iterates over all documents', async function () {
620+
let count = 0;
621+
cursor = collection.find({}, { batchSize: 1 }).map(doc => {
622+
count++;
623+
return doc;
624+
});
625+
626+
await cursor.next();
627+
628+
let doc;
629+
while ((doc = (await cursor.next()) && doc != null)) {
630+
/** empty */
631+
}
632+
633+
expect(count).to.equal(2);
634+
});
635+
});
636+
637+
context('when cursor.readBufferedDocuments() is called after cursor.next()', function () {
638+
it('returns an empty array', async function () {
639+
cursor = collection.find({}, { batchSize: 1 });
640+
641+
await cursor.next();
642+
const docs = cursor.readBufferedDocuments();
643+
644+
expect(docs).to.have.lengthOf(0);
645+
});
646+
});
647+
648+
context('when cursor.next() is called after cursor.toArray()', function () {
649+
it('returns null', async function () {
650+
cursor = collection.find({}, { batchSize: 1 });
651+
652+
await cursor.toArray();
653+
expect(await cursor.next()).to.be.null;
654+
});
655+
});
656+
657+
context('when cursor.tryNext is called after cursor.toArray()', function () {
658+
it('returns null', async function () {
659+
cursor = collection.find({}, { batchSize: 1 });
660+
661+
await cursor.toArray();
662+
expect(await cursor.tryNext()).to.be.null;
663+
});
664+
});
665+
666+
context('when cursor.[Symbol.asyncIterator] is called after cursor.toArray()', function () {
667+
it('should not iterate', async function () {
668+
cursor = collection.find({}, { batchSize: 1 });
669+
670+
await cursor.toArray();
671+
// eslint-disable-next-line no-unused-vars
672+
for await (const _ of cursor) {
673+
expect.fail('should not iterate');
674+
}
675+
});
676+
});
677+
678+
context('when cursor.readBufferedDocuments() is called after cursor.toArray()', function () {
679+
it('return and empty array', async function () {
680+
cursor = collection.find({}, { batchSize: 1 });
681+
682+
await cursor.toArray();
683+
expect(cursor.readBufferedDocuments()).to.have.lengthOf(0);
684+
});
685+
});
686+
687+
context('when cursor.stream() is called after cursor.toArray()', function () {
688+
it('returns an empty stream', async function () {
689+
cursor = collection.find({}, { batchSize: 1 });
690+
await cursor.toArray();
691+
692+
const s = cursor.stream();
693+
const { promise, resolve, reject } = promiseWithResolvers();
694+
695+
s.once('data', d => {
696+
reject(d);
697+
});
698+
699+
s.once('end', d => {
700+
resolve(d);
701+
});
702+
703+
expect(await promise).to.be.undefined;
704+
});
705+
});
706+
});
707+
});
364708
});

0 commit comments

Comments
 (0)