Skip to content

Commit b972c1e

Browse files
authored
feat(Cursor): adds support for AsyncIterator in cursors
Adds support for async iterators in cursors, allowing users to use for..await..of blocks on cursors. Note: this is only loaded if Symbol.asyncIterator is supported (v10.x). Fixes NODE-1684 test: adds single file responsible for loading esnext tests Now use a single file to load tests that cannot run in current env
1 parent 92e2580 commit b972c1e

File tree

9 files changed

+169
-27
lines changed

9 files changed

+169
-27
lines changed

lib/aggregation_cursor.js

+5
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,11 @@ inherits(AggregationCursor, Readable);
130130
for (var name in CoreCursor.prototype) {
131131
AggregationCursor.prototype[name] = CoreCursor.prototype[name];
132132
}
133+
if (Symbol.asyncIterator) {
134+
AggregationCursor.prototype[
135+
Symbol.asyncIterator
136+
] = require('./async/async_iterator').asyncIterator;
137+
}
133138

134139
/**
135140
* Set the batch size for the cursor.

lib/async/.eslintrc

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"parserOptions": {
3+
"ecmaVersion": 2018
4+
}
5+
}

lib/async/async_iterator.js

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
'use strict';
2+
3+
async function* asyncIterator() {
4+
while (true) {
5+
const value = await this.next();
6+
if (!value) {
7+
await this.close();
8+
return;
9+
}
10+
11+
yield value;
12+
}
13+
}
14+
15+
exports.asyncIterator = asyncIterator;

lib/command_cursor.js

+4
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,10 @@ for (var i = 0; i < methodsToInherit.length; i++) {
156156
CommandCursor.prototype[methodsToInherit[i]] = CoreCursor.prototype[methodsToInherit[i]];
157157
}
158158

159+
if (Symbol.asyncIterator) {
160+
CommandCursor.prototype[Symbol.asyncIterator] = require('./async/async_iterator').asyncIterator;
161+
}
162+
159163
/**
160164
* Set the ReadPreference for the cursor.
161165
* @method

lib/cursor.js

+4
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,10 @@ function Cursor(bson, ns, cmd, options, topology, topologyOptions) {
203203
// Inherit from Readable
204204
inherits(Cursor, Readable);
205205

206+
if (Symbol.asyncIterator) {
207+
Cursor.prototype[Symbol.asyncIterator] = require('./async/async_iterator').asyncIterator;
208+
}
209+
206210
// Map core cursor _next method so we can apply mapping
207211
Cursor.prototype._next = function() {
208212
if (this._initImplicitSession) {

test/functional/load_esnext.js

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
'use strict';
2+
3+
function loadTests() {
4+
const fs = require('fs');
5+
const path = require('path');
6+
7+
const directory = path.resolve.apply(path, arguments);
8+
fs
9+
.readdirSync(directory)
10+
.filter(filePath => filePath.match(/.*\.js$/))
11+
.map(filePath => path.resolve(directory, filePath))
12+
.forEach(x => require(x));
13+
}
14+
15+
describe('ES2017', function() {
16+
let supportES2017 = false;
17+
try {
18+
new Function('return (async function foo() {})();')();
19+
supportES2017 = true;
20+
} catch (e) {
21+
supportES2017 = false;
22+
}
23+
24+
if (supportES2017) {
25+
loadTests(__dirname, '..', 'examples');
26+
} else {
27+
it.skip('skipping ES2017 tests due to insufficient node version', function() {});
28+
}
29+
});
30+
31+
describe('ES2018', function() {
32+
const supportES2018 = !!Symbol.asyncIterator;
33+
34+
if (supportES2018) {
35+
loadTests(__dirname, '..', 'node-next', 'es2018');
36+
} else {
37+
it.skip('skipping ES2018 tests due to insufficient node version', function() {});
38+
}
39+
});

test/functional/load_examples.js

-27
This file was deleted.

test/node-next/es2018/.eslintrc

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"parserOptions": {
3+
"ecmaVersion": 2018
4+
}
5+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
'use strict';
2+
3+
const { expect } = require('chai');
4+
const { MongoError } = require('../../../index');
5+
6+
describe('Cursor Async Iterator Tests', function() {
7+
let client, collection;
8+
before(async function() {
9+
client = this.configuration.newClient();
10+
11+
await client.connect();
12+
const docs = Array.from({ length: 1000 }).map((_, index) => ({ foo: index, bar: 1 }));
13+
14+
collection = client.db(this.configuration.db).collection('async_cursor_tests');
15+
16+
await collection.deleteMany({});
17+
await collection.insertMany(docs);
18+
await client.close();
19+
});
20+
21+
beforeEach(async function() {
22+
client = this.configuration.newClient();
23+
await client.connect();
24+
collection = client.db(this.configuration.db).collection('async_cursor_tests');
25+
});
26+
27+
afterEach(() => client.close());
28+
29+
it('should be able to use a for-await loop on a find command cursor', {
30+
metadata: { requires: { node: '>=10.5.0' } },
31+
test: async function() {
32+
const cursor = collection.find({ bar: 1 });
33+
34+
let counter = 0;
35+
for await (const doc of cursor) {
36+
expect(doc).to.have.property('bar', 1);
37+
counter += 1;
38+
}
39+
40+
expect(counter).to.equal(1000);
41+
}
42+
});
43+
44+
it('should be able to use a for-await loop on an aggregation cursor', {
45+
metadata: { requires: { node: '>=10.5.0' } },
46+
test: async function() {
47+
const cursor = collection.aggregate([{ $match: { bar: 1 } }]);
48+
49+
let counter = 0;
50+
for await (const doc of cursor) {
51+
expect(doc).to.have.property('bar', 1);
52+
counter += 1;
53+
}
54+
55+
expect(counter).to.equal(1000);
56+
}
57+
});
58+
59+
it('should be able to use a for-await loop on a command cursor', {
60+
metadata: { requires: { node: '>=10.5.0', mongodb: '>=3.0.0' } },
61+
test: async function() {
62+
const cursor1 = collection.listIndexes();
63+
const cursor2 = collection.listIndexes();
64+
65+
const indexes = await cursor1.toArray();
66+
let counter = 0;
67+
for await (const doc of cursor2) {
68+
expect(doc).to.exist;
69+
counter += 1;
70+
}
71+
72+
expect(counter).to.equal(indexes.length);
73+
}
74+
});
75+
76+
it('should properly error when cursor is closed', {
77+
metadata: { requires: { node: '>=10.5.0' } },
78+
test: async function() {
79+
const cursor = collection.find();
80+
81+
try {
82+
for await (const doc of cursor) {
83+
expect(doc).to.exist;
84+
cursor.close();
85+
}
86+
throw new Error('expected closing the cursor to break iteration');
87+
} catch (e) {
88+
expect(e).to.be.an.instanceOf(MongoError);
89+
}
90+
}
91+
});
92+
});

0 commit comments

Comments
 (0)