Skip to content

Commit 43c94b6

Browse files
authored
feat: add an internal tryNext method (#2638)
The default cursor iteration behavior is to block until either a cursor is dead (has a cursor id of 0), or a non-empty batch is returned from the server for a `getMore` command. `tryNext` allows users to iterate a cursor optionally returning `null` if the `getMore` returns an empty batch (likely on a tailable + awaitData cursor). NODE-2917
1 parent ca0124d commit 43c94b6

File tree

2 files changed

+68
-9
lines changed

2 files changed

+68
-9
lines changed

src/cursor/abstract_cursor.ts

+32-8
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ export abstract class AbstractCursor extends EventEmitter {
213213
return done(undefined, true);
214214
}
215215

216-
next(this, (err, doc) => {
216+
next(this, true, (err, doc) => {
217217
if (err) return done(err);
218218

219219
if (doc) {
@@ -236,7 +236,23 @@ export abstract class AbstractCursor extends EventEmitter {
236236
return done(new MongoError('Cursor is exhausted'));
237237
}
238238

239-
next(this, done);
239+
next(this, true, done);
240+
});
241+
}
242+
243+
/**
244+
* Try to get the next available document from the cursor or `null` if an empty batch is returned
245+
* @internal
246+
*/
247+
tryNext(): Promise<Document | null>;
248+
tryNext(callback: Callback<Document | null>): void;
249+
tryNext(callback?: Callback<Document | null>): Promise<Document | null> | void {
250+
return maybePromise(callback, done => {
251+
if (this[kId] === Long.ZERO) {
252+
return done(new MongoError('Cursor is exhausted'));
253+
}
254+
255+
next(this, false, done);
240256
});
241257
}
242258

@@ -259,7 +275,7 @@ export abstract class AbstractCursor extends EventEmitter {
259275
return maybePromise(callback, done => {
260276
const transform = this[kTransform];
261277
const fetchDocs = () => {
262-
next(this, (err, doc) => {
278+
next(this, true, (err, doc) => {
263279
if (err || doc == null) return done(err);
264280
if (doc == null) return done();
265281

@@ -350,7 +366,7 @@ export abstract class AbstractCursor extends EventEmitter {
350366
const transform = this[kTransform];
351367
const fetchDocs = () => {
352368
// NOTE: if we add a `nextBatch` then we should use it here
353-
next(this, (err, doc) => {
369+
next(this, true, (err, doc) => {
354370
if (err) return done(err);
355371
if (doc == null) return done(undefined, docs);
356372

@@ -518,7 +534,11 @@ function nextDocument(cursor: AbstractCursor): Document | null | undefined {
518534
return null;
519535
}
520536

521-
function next(cursor: AbstractCursor, callback: Callback<Document | null>): void {
537+
function next(
538+
cursor: AbstractCursor,
539+
blocking: boolean,
540+
callback: Callback<Document | null>
541+
): void {
522542
const cursorId = cursor[kId];
523543
if (cursor.closed) {
524544
return callback(undefined, null);
@@ -577,7 +597,7 @@ function next(cursor: AbstractCursor, callback: Callback<Document | null>): void
577597
return cleanupCursor(cursor, () => callback(err, nextDocument(cursor)));
578598
}
579599

580-
next(cursor, callback);
600+
next(cursor, blocking, callback);
581601
});
582602

583603
return;
@@ -604,7 +624,11 @@ function next(cursor: AbstractCursor, callback: Callback<Document | null>): void
604624
return cleanupCursor(cursor, () => callback(err, nextDocument(cursor)));
605625
}
606626

607-
next(cursor, callback);
627+
if (cursor[kDocuments].length === 0 && blocking === false) {
628+
return callback(undefined, null);
629+
}
630+
631+
next(cursor, blocking, callback);
608632
});
609633
}
610634

@@ -666,7 +690,7 @@ function makeCursorStream(cursor: AbstractCursor) {
666690

667691
function readNext() {
668692
needToClose = false;
669-
next(cursor, (err, result) => {
693+
next(cursor, true, (err, result) => {
670694
needToClose = err ? !cursor.closed : result !== null;
671695

672696
if (err) {

test/functional/abstract_cursor.test.js

+36-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ describe('AbstractCursor', function () {
2323
withClientV2((client, done) => {
2424
const docs = [{ a: 1 }, { a: 2 }, { a: 3 }, { a: 4 }, { a: 5 }, { a: 6 }];
2525
const coll = client.db().collection('find_cursor');
26-
coll.drop(() => coll.insertMany(docs, done));
26+
const tryNextColl = client.db().collection('try_next');
27+
coll.drop(() => tryNextColl.drop(() => coll.insertMany(docs, done)));
2728
})
2829
);
2930

@@ -124,4 +125,38 @@ describe('AbstractCursor', function () {
124125
})
125126
);
126127
});
128+
129+
context('#tryNext', function () {
130+
it(
131+
'should return control to the user if an empty batch is returned',
132+
withClientV2(function (client, done) {
133+
const db = client.db();
134+
db.createCollection('try_next', { capped: true, size: 10000000 }, () => {
135+
const coll = db.collection('try_next');
136+
coll.insertMany([{}, {}], err => {
137+
expect(err).to.not.exist;
138+
139+
const cursor = coll.find({}, { tailable: true, awaitData: true });
140+
this.defer(() => cursor.close());
141+
142+
cursor.tryNext((err, doc) => {
143+
expect(err).to.not.exist;
144+
expect(doc).to.exist;
145+
146+
cursor.tryNext((err, doc) => {
147+
expect(err).to.not.exist;
148+
expect(doc).to.exist;
149+
150+
cursor.tryNext((err, doc) => {
151+
expect(err).to.not.exist;
152+
expect(doc).to.be.null;
153+
done();
154+
});
155+
});
156+
});
157+
});
158+
});
159+
})
160+
);
161+
});
127162
});

0 commit comments

Comments
 (0)