Skip to content

Commit 30c0aee

Browse files
authored
feat(NODE-4598)!: close cursor on early loop break (#3502)
1 parent 556812f commit 30c0aee

File tree

3 files changed

+74
-17
lines changed

3 files changed

+74
-17
lines changed

etc/notes/CHANGES_5.0.0.md

+14
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,17 @@ The new minimum supported Node.js version is now 14.20.1.
4747

4848
The MongoClient option `promiseLibrary` along with the `Promise.set` export that allows specifying a custom promise library has been removed.
4949
This allows the driver to adopt async/await syntax which has [performance benefits](https://v8.dev/blog/fast-async) over manual promise construction.
50+
51+
### Cursor closes on exit of for await of loops
52+
53+
Cursors will now automatically close when exiting a for await of loop on the cursor itself.
54+
55+
```js
56+
const cursor = collection.find({});
57+
for await (const doc of cursor) {
58+
console.log(doc);
59+
break;
60+
}
61+
62+
cursor.closed // true
63+
```

src/cursor/abstract_cursor.ts

+25-17
Original file line numberDiff line numberDiff line change
@@ -301,29 +301,37 @@ export abstract class AbstractCursor<
301301
return;
302302
}
303303

304-
while (true) {
305-
const document = await this.next();
304+
try {
305+
while (true) {
306+
const document = await this.next();
306307

307-
// Intentional strict null check, because users can map cursors to falsey values.
308-
// We allow mapping to all values except for null.
309-
// eslint-disable-next-line no-restricted-syntax
310-
if (document === null) {
311-
if (!this.closed) {
312-
const message =
313-
'Cursor returned a `null` document, but the cursor is not exhausted. Mapping documents to `null` is not supported in the cursor transform.';
308+
// Intentional strict null check, because users can map cursors to falsey values.
309+
// We allow mapping to all values except for null.
310+
// eslint-disable-next-line no-restricted-syntax
311+
if (document === null) {
312+
if (!this.closed) {
313+
const message =
314+
'Cursor returned a `null` document, but the cursor is not exhausted. Mapping documents to `null` is not supported in the cursor transform.';
314315

315-
await cleanupCursorAsync(this, { needsToEmitClosed: true }).catch(() => null);
316+
await cleanupCursorAsync(this, { needsToEmitClosed: true }).catch(() => null);
316317

317-
throw new MongoAPIError(message);
318+
throw new MongoAPIError(message);
319+
}
320+
break;
318321
}
319-
break;
320-
}
321322

322-
yield document;
323+
yield document;
323324

324-
if (this[kId] === Long.ZERO) {
325-
// Cursor exhausted
326-
break;
325+
if (this[kId] === Long.ZERO) {
326+
// Cursor exhausted
327+
break;
328+
}
329+
}
330+
} finally {
331+
// Only close the cursor if it has not already been closed. This finally clause handles
332+
// the case when a user would break out of a for await of loop early.
333+
if (!this.closed) {
334+
await this.close().catch(() => null);
327335
}
328336
}
329337
}

test/integration/node-specific/cursor_async_iterator.test.js

+35
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
'use strict';
22

33
const { expect } = require('chai');
4+
const sinon = require('sinon');
45

56
describe('Cursor Async Iterator Tests', function () {
67
context('default promise library', function () {
@@ -36,6 +37,7 @@ describe('Cursor Async Iterator Tests', function () {
3637
}
3738

3839
expect(counter).to.equal(1000);
40+
expect(cursor.closed).to.be.true;
3941
});
4042

4143
it('should be able to use a for-await loop on an aggregation cursor', async function () {
@@ -48,6 +50,7 @@ describe('Cursor Async Iterator Tests', function () {
4850
}
4951

5052
expect(counter).to.equal(1000);
53+
expect(cursor.closed).to.be.true;
5154
});
5255

5356
it('should be able to use a for-await loop on a command cursor', {
@@ -64,6 +67,8 @@ describe('Cursor Async Iterator Tests', function () {
6467
}
6568

6669
expect(counter).to.equal(indexes.length);
70+
expect(cursor1.closed).to.be.true;
71+
expect(cursor2.closed).to.be.true;
6772
}
6873
});
6974

@@ -78,6 +83,36 @@ describe('Cursor Async Iterator Tests', function () {
7883
}
7984

8085
expect(count).to.equal(1);
86+
expect(cursor.closed).to.be.true;
87+
});
88+
89+
it('cleans up cursor when breaking out of for await of loops', async function () {
90+
const cursor = collection.find();
91+
92+
for await (const doc of cursor) {
93+
expect(doc).to.exist;
94+
break;
95+
}
96+
97+
expect(cursor.closed).to.be.true;
98+
});
99+
100+
it('returns when attempting to reuse the cursor after a break', async function () {
101+
const cursor = collection.find();
102+
const spy = sinon.spy(cursor);
103+
104+
for await (const doc of cursor) {
105+
expect(doc).to.exist;
106+
break;
107+
}
108+
109+
expect(cursor.closed).to.be.true;
110+
111+
for await (const doc of cursor) {
112+
expect.fail('Async generator returns immediately if cursor is closed', doc);
113+
}
114+
// cursor.close() should only be called once.
115+
expect(spy.close.calledOnce).to.be.true;
81116
});
82117
});
83118
});

0 commit comments

Comments
 (0)