Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add an internal tryNext method #2638

Merged
merged 1 commit into from
Nov 30, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 32 additions & 8 deletions src/cursor/abstract_cursor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ export abstract class AbstractCursor extends EventEmitter {
return done(undefined, true);
}

next(this, (err, doc) => {
next(this, true, (err, doc) => {
if (err) return done(err);

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

next(this, done);
next(this, true, done);
});
}

/**
* Try to get the next available document from the cursor or `null` if an empty batch is returned
* @internal
*/
tryNext(): Promise<Document | null>;
tryNext(callback: Callback<Document | null>): void;
tryNext(callback?: Callback<Document | null>): Promise<Document | null> | void {
return maybePromise(callback, done => {
if (this[kId] === Long.ZERO) {
return done(new MongoError('Cursor is exhausted'));
}

next(this, false, done);
});
}

Expand All @@ -259,7 +275,7 @@ export abstract class AbstractCursor extends EventEmitter {
return maybePromise(callback, done => {
const transform = this[kTransform];
const fetchDocs = () => {
next(this, (err, doc) => {
next(this, true, (err, doc) => {
if (err || doc == null) return done(err);
if (doc == null) return done();

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

Expand Down Expand Up @@ -518,7 +534,11 @@ function nextDocument(cursor: AbstractCursor): Document | null | undefined {
return null;
}

function next(cursor: AbstractCursor, callback: Callback<Document | null>): void {
function next(
cursor: AbstractCursor,
blocking: boolean,
callback: Callback<Document | null>
): void {
const cursorId = cursor[kId];
if (cursor.closed) {
return callback(undefined, null);
Expand Down Expand Up @@ -577,7 +597,7 @@ function next(cursor: AbstractCursor, callback: Callback<Document | null>): void
return cleanupCursor(cursor, () => callback(err, nextDocument(cursor)));
}

next(cursor, callback);
next(cursor, blocking, callback);
});

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

next(cursor, callback);
if (cursor[kDocuments].length === 0 && blocking === false) {
return callback(undefined, null);
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there a specific reason to use null over undefined? I suppose it makes more sense to communicate a non-existent object, so I'm not taking issue with it just wondering if there was some thinking behind it

Although maybe one reason to use undefined is so the result doesn't respond true to: typeof result === 'object'.

Copy link
Member Author

Choose a reason for hiding this comment

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

I used null here for symmetry with our semantics around a "dead" cursor. When a cursor returns a zero cursor id, we return an explicit null to the user. I think this might have been left over from days when a Cursor inherited Readable since ending a stream is accomplished by readable.push(null), but either way the convention exists so I stuck with it

}

next(cursor, blocking, callback);
});
}

Expand Down Expand Up @@ -666,7 +690,7 @@ function makeCursorStream(cursor: AbstractCursor) {

function readNext() {
needToClose = false;
next(cursor, (err, result) => {
next(cursor, true, (err, result) => {
needToClose = err ? !cursor.closed : result !== null;

if (err) {
Expand Down
37 changes: 36 additions & 1 deletion test/functional/abstract_cursor.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ describe('AbstractCursor', function () {
withClientV2((client, done) => {
const docs = [{ a: 1 }, { a: 2 }, { a: 3 }, { a: 4 }, { a: 5 }, { a: 6 }];
const coll = client.db().collection('find_cursor');
coll.drop(() => coll.insertMany(docs, done));
const tryNextColl = client.db().collection('try_next');
coll.drop(() => tryNextColl.drop(() => coll.insertMany(docs, done)));
})
);

Expand Down Expand Up @@ -124,4 +125,38 @@ describe('AbstractCursor', function () {
})
);
});

context('#tryNext', function () {
it(
'should return control to the user if an empty batch is returned',
withClientV2(function (client, done) {
const db = client.db();
db.createCollection('try_next', { capped: true, size: 10000000 }, () => {
const coll = db.collection('try_next');
coll.insertMany([{}, {}], err => {
expect(err).to.not.exist;

const cursor = coll.find({}, { tailable: true, awaitData: true });
this.defer(() => cursor.close());

cursor.tryNext((err, doc) => {
expect(err).to.not.exist;
expect(doc).to.exist;

cursor.tryNext((err, doc) => {
expect(err).to.not.exist;
expect(doc).to.exist;

cursor.tryNext((err, doc) => {
expect(err).to.not.exist;
expect(doc).to.be.null;
done();
});
});
});
});
});
})
);
});
});