Skip to content

Commit

Permalink
feat(Cursor): adds support for AsyncIterator in cursors
Browse files Browse the repository at this point in the history
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
  • Loading branch information
daprahamian committed Mar 19, 2019
1 parent 92e2580 commit b972c1e
Show file tree
Hide file tree
Showing 9 changed files with 169 additions and 27 deletions.
5 changes: 5 additions & 0 deletions lib/aggregation_cursor.js
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,11 @@ inherits(AggregationCursor, Readable);
for (var name in CoreCursor.prototype) {
AggregationCursor.prototype[name] = CoreCursor.prototype[name];
}
if (Symbol.asyncIterator) {
AggregationCursor.prototype[
Symbol.asyncIterator
] = require('./async/async_iterator').asyncIterator;
}

/**
* Set the batch size for the cursor.
Expand Down
5 changes: 5 additions & 0 deletions lib/async/.eslintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"parserOptions": {
"ecmaVersion": 2018
}
}
15 changes: 15 additions & 0 deletions lib/async/async_iterator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
'use strict';

async function* asyncIterator() {
while (true) {
const value = await this.next();
if (!value) {
await this.close();
return;
}

yield value;
}
}

exports.asyncIterator = asyncIterator;
4 changes: 4 additions & 0 deletions lib/command_cursor.js
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,10 @@ for (var i = 0; i < methodsToInherit.length; i++) {
CommandCursor.prototype[methodsToInherit[i]] = CoreCursor.prototype[methodsToInherit[i]];
}

if (Symbol.asyncIterator) {
CommandCursor.prototype[Symbol.asyncIterator] = require('./async/async_iterator').asyncIterator;
}

/**
* Set the ReadPreference for the cursor.
* @method
Expand Down
4 changes: 4 additions & 0 deletions lib/cursor.js
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,10 @@ function Cursor(bson, ns, cmd, options, topology, topologyOptions) {
// Inherit from Readable
inherits(Cursor, Readable);

if (Symbol.asyncIterator) {
Cursor.prototype[Symbol.asyncIterator] = require('./async/async_iterator').asyncIterator;
}

// Map core cursor _next method so we can apply mapping
Cursor.prototype._next = function() {
if (this._initImplicitSession) {
Expand Down
39 changes: 39 additions & 0 deletions test/functional/load_esnext.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
'use strict';

function loadTests() {
const fs = require('fs');
const path = require('path');

const directory = path.resolve.apply(path, arguments);
fs
.readdirSync(directory)
.filter(filePath => filePath.match(/.*\.js$/))
.map(filePath => path.resolve(directory, filePath))
.forEach(x => require(x));
}

describe('ES2017', function() {
let supportES2017 = false;
try {
new Function('return (async function foo() {})();')();
supportES2017 = true;
} catch (e) {
supportES2017 = false;
}

if (supportES2017) {
loadTests(__dirname, '..', 'examples');
} else {
it.skip('skipping ES2017 tests due to insufficient node version', function() {});
}
});

describe('ES2018', function() {
const supportES2018 = !!Symbol.asyncIterator;

if (supportES2018) {
loadTests(__dirname, '..', 'node-next', 'es2018');
} else {
it.skip('skipping ES2018 tests due to insufficient node version', function() {});
}
});
27 changes: 0 additions & 27 deletions test/functional/load_examples.js

This file was deleted.

5 changes: 5 additions & 0 deletions test/node-next/es2018/.eslintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"parserOptions": {
"ecmaVersion": 2018
}
}
92 changes: 92 additions & 0 deletions test/node-next/es2018/cursor_async_iterator_tests.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
'use strict';

const { expect } = require('chai');
const { MongoError } = require('../../../index');

describe('Cursor Async Iterator Tests', function() {
let client, collection;
before(async function() {
client = this.configuration.newClient();

await client.connect();
const docs = Array.from({ length: 1000 }).map((_, index) => ({ foo: index, bar: 1 }));

collection = client.db(this.configuration.db).collection('async_cursor_tests');

await collection.deleteMany({});
await collection.insertMany(docs);
await client.close();
});

beforeEach(async function() {
client = this.configuration.newClient();
await client.connect();
collection = client.db(this.configuration.db).collection('async_cursor_tests');
});

afterEach(() => client.close());

it('should be able to use a for-await loop on a find command cursor', {
metadata: { requires: { node: '>=10.5.0' } },
test: async function() {
const cursor = collection.find({ bar: 1 });

let counter = 0;
for await (const doc of cursor) {
expect(doc).to.have.property('bar', 1);
counter += 1;
}

expect(counter).to.equal(1000);
}
});

it('should be able to use a for-await loop on an aggregation cursor', {
metadata: { requires: { node: '>=10.5.0' } },
test: async function() {
const cursor = collection.aggregate([{ $match: { bar: 1 } }]);

let counter = 0;
for await (const doc of cursor) {
expect(doc).to.have.property('bar', 1);
counter += 1;
}

expect(counter).to.equal(1000);
}
});

it('should be able to use a for-await loop on a command cursor', {
metadata: { requires: { node: '>=10.5.0', mongodb: '>=3.0.0' } },
test: async function() {
const cursor1 = collection.listIndexes();
const cursor2 = collection.listIndexes();

const indexes = await cursor1.toArray();
let counter = 0;
for await (const doc of cursor2) {
expect(doc).to.exist;
counter += 1;
}

expect(counter).to.equal(indexes.length);
}
});

it('should properly error when cursor is closed', {
metadata: { requires: { node: '>=10.5.0' } },
test: async function() {
const cursor = collection.find();

try {
for await (const doc of cursor) {
expect(doc).to.exist;
cursor.close();
}
throw new Error('expected closing the cursor to break iteration');
} catch (e) {
expect(e).to.be.an.instanceOf(MongoError);
}
}
});
});

3 comments on commit b972c1e

@simplecommerce
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi, since the update to 3.2.1, I get this error when I try to start node and it seems related to this code specifically, I do not know how to fix it, any help would be appreciated.

using npm@6.9.0
using node@v8.10.0

/root/294-cms-section-blog-listing-components/node_modules/mongodb/lib/async/async_iterator.js:3
async function* asyncIterator() {
              ^

SyntaxError: Unexpected token *
    at createScript (vm.js:80:10)
    at Object.runInThisContext (vm.js:139:10)
    at Module._compile (module.js:616:28)
    at Module._compile (/root/294-cms-section-blog-listing-components/node_modules/pirates/lib/index.js:99:24)
    at Module._extensions..js (module.js:663:10)
    at Object.newLoader [as .js] (/root/294-cms-section-blog-listing-components/node_modules/pirates/lib/index.js:104:7)
    at Module.load (module.js:565:32)
    at tryModuleLoad (module.js:505:12)
    at Function.Module._load (module.js:497:3)
    at Module.require (module.js:596:17)
    at require (internal/module.js:11:18)
    at Object.<anonymous> (/root/294-cms-section-blog-listing-components/node_modules/mongodb/lib/cursor.js:207:44)
    at Module._compile (module.js:652:30)
    at Module._compile (/root/294-cms-section-blog-listing-components/node_modules/pirates/lib/index.js:99:24)
    at Module._extensions..js (module.js:663:10)
    at Object.newLoader [as .js] (/root/294-cms-section-blog-listing-components/node_modules/pirates/lib/index.js:104:7)
    at Module.load (module.js:565:32)
    at tryModuleLoad (module.js:505:12)
    at Function.Module._load (module.js:497:3)
    at Module.require (module.js:596:17)
    at require (internal/module.js:11:18)
    at Object.<anonymous> (/root/294-cms-section-blog-listing-components/node_modules/mongodb/lib/command_cursor.js:7:20)
npm ERR! code ELIFECYCLE
npm ERR! errno 1
npm ERR! graphql-test-server@1.0.0 start: `babel-node index.js --presets @babel/preset-env --plugins @babel/plugin-transform-destructuring,@babel/plugin-syntax-throw-expressions,@babel/plugin-proposal-async-generator-functions,@babel/plugin-transform-async-to-generator`
npm ERR! Exit status 1
npm ERR!
npm ERR! Failed at the graphql-test-server@1.0.0 start script.
npm ERR! This is probably not a problem with npm. There is likely additional logging output above.

npm ERR! A complete log of this run can be found in:
npm ERR!     /root/.npm/_logs/2019-03-22T10_51_29_931Z-debug.log

@simplecommerce
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@wangrenyn If you we're trying to reply, I think your comment was cut off?

@mbroadst
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@simplecommerce would you mind opening a JIRA ticket for this? It's difficult to have comment threads on individual commits. In the ticket, could you please tell us if there is anything special about your application (are you webpacking for instance)? We run integration tests against node carbon on both travis and evergreen, and I just did a test run locally with v8.10.0 specifically and it's running without this issue.

Please sign in to comment.