- Reference
- Cursors
Cursors
When a driver executes a read that returns mutliple documents, the server does not immediately return all values that match the query. Instead, the driver creates a Cursor object which fetches the documents in batches.
Where are cursors used in the driver?
The following functions directly return cursors to the user:
Collection.prototype.find
Collection.prototype.aggregate
Collection.prototype.listIndexes
Db.prototype.aggregate
Db.prototype.listCollections
In addition, many other methods, like Collection.prototype.findOne
and Collection.prototype.watch
, use cursors in order to return results.
How do I use a cursor?
There are multiple ways to consume a cursor:
Stream API
All cursors in the Node Driver are Node Readable Streams operating in Object Mode. Cursors will work with most Node stream APIs.
// Using the stream API
const cursor = collection.find({});
cursor.pipe(new stream.Writable({
write: function(doc, _, callback) {
console.log(doc);
callback();
}
}));
Event API
As Readable Streams, Cursors also support an Event API.
// Using the event API
const cursor = collection.find({});
cursor.on('data', data => console.log(data));
Get all documents at once
To get all documents at once, users can use the toArray
method.
// Get all values
const cursor = collection.find({});
const allValues = await cursor.toArray();
Async Iterator
Cursors also implement the AsyncIterator interface, allowing them to be used in for
…await
loops.
const cursor = collection.find({});
for await(const doc of cursor) {
console.log(doc);
}
For Each
If you want to perform an operation on each document, you can use the forEach
method.
// apply a callback to every item in a cursor
const cursor = collection.find({});
await cursor.forEach(doc => console.log(doc));
Get a single document
If you wish to only get a single document, use next
and hasNext
.
// get only one value
const cursor = collection.find({});
let firstValue, secondValue;
if (await cursor.hasNext()) {
firstValue = await cursor.next();
if (await cursor.hasNext()) {
secondValue = await cursor.next();
}
}
Count
If you would like an estimated count of the number of documents the cursor will return, use count
// Get an estimate of the number of documents in a cursor
const cursor = collection.find({});
const count = await cursor.count();
Important design considerations
Only use one API at a time
The cursor is designed with the assumption that users will only use one of the above methods to get data. Therefore, when interacting with a cursor, it is very important to only use one method of interaction at a time. Using more than one method of interacting with a cursor will lead to undefined behavior.
For example, the following:
const cursor = collection.find({});
const countOfDocuments = await cursor.count();
const allDocument = await cursor.toArray();
is very likely to trigger an error, as it is attempting both count
and a toArray
on the same cursor.
Similarly, the following:
const cursor = collection.find({});
cursor.on('data', data => callSomeLoggingFunction(data));
while (await cursor.hasNext()) {
doSomethingWithDocument(await cursor.next());
}
is very likely to cause an error as it attempts to use both the next
/hasNext
API and the stream event API.
Do not attempt multiple simultaneous cursor operations
The cursor is designed with the assumption that it will only be performing one asynchronous operation at a time. Because of this, attempting multiple async operations in parallel on a cursor can result in undefined behavior.
For example, the following loop is likely to produce an error:
const promises = [];
const cursor = collection.find({});
for (let i = 0; i < 100; i++) {
promises.push(cursor.next);
}
const results = await Promise.all(promises);