Skip to content

Commit a5154fb

Browse files
authored
feat: reintroduce clone and rewind for cursors (#2647)
* feat: implement rewind support for AbstractCursor subclasses This reintroduces support for rewinding a cursor to its uninitialized state. NODE-2811 * feat: reimplement clone for find and aggregate cursors NODE-2811
1 parent 3e5ff57 commit a5154fb

8 files changed

+193
-9
lines changed

src/change_stream.ts

+6
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,12 @@ export class ChangeStreamCursor extends AbstractCursor {
425425
}
426426
}
427427

428+
clone(): ChangeStreamCursor {
429+
return new ChangeStreamCursor(this.topology, this.namespace, this.pipeline, {
430+
...this.cursorOptions
431+
});
432+
}
433+
428434
_initialize(session: ClientSession, callback: Callback<ExecutionResult>): void {
429435
const aggregateOperation = new AggregateOperation(
430436
{ s: { namespace: this.namespace } },

src/cursor/abstract_cursor.ts

+33-1
Original file line numberDiff line numberDiff line change
@@ -508,6 +508,38 @@ export abstract class AbstractCursor extends EventEmitter {
508508
return this;
509509
}
510510

511+
/**
512+
* Rewind this cursor to its uninitialized state. Any options that are present on the cursor will
513+
* remain in effect. Iterating this cursor will cause new queries to be sent to the server, even
514+
* if the resultant data has already been retrieved by this cursor.
515+
*/
516+
rewind(): void {
517+
if (!this[kInitialized]) {
518+
return;
519+
}
520+
521+
this[kId] = undefined;
522+
this[kDocuments] = [];
523+
this[kClosed] = false;
524+
this[kKilled] = false;
525+
this[kInitialized] = false;
526+
527+
const session = this[kSession];
528+
if (session) {
529+
// We only want to end this session if we created it, and it hasn't ended yet
530+
if (session.explicit === false && !session.hasEnded) {
531+
session.endSession();
532+
}
533+
534+
this[kSession] = undefined;
535+
}
536+
}
537+
538+
/**
539+
* Returns a new uninitialized copy of this cursor, with options matching those that have been set on the current instance
540+
*/
541+
abstract clone(): AbstractCursor;
542+
511543
/* @internal */
512544
abstract _initialize(
513545
session: ClientSession | undefined,
@@ -579,7 +611,7 @@ function next(
579611
if (cursorId == null) {
580612
// All cursors must operate within a session, one must be made implicitly if not explicitly provided
581613
if (cursor[kSession] == null && cursor[kTopology].hasSessionSupport()) {
582-
cursor[kSession] = cursor[kTopology].startSession({ owner: cursor, explicit: true });
614+
cursor[kSession] = cursor[kTopology].startSession({ owner: cursor, explicit: false });
583615
}
584616

585617
cursor._initialize(cursor[kSession], (err, state) => {

src/cursor/aggregation_cursor.ts

+7
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,13 @@ export class AggregationCursor extends AbstractCursor {
4848
return this[kPipeline];
4949
}
5050

51+
clone(): AggregationCursor {
52+
return new AggregationCursor(this[kParent], this.topology, this.namespace, this[kPipeline], {
53+
...this[kOptions],
54+
...this.cursorOptions
55+
});
56+
}
57+
5158
/** @internal */
5259
_initialize(session: ClientSession | undefined, callback: Callback<ExecutionResult>): void {
5360
const aggregateOperation = new AggregateOperation(this[kParent], this[kPipeline], {

src/cursor/find_cursor.ts

+7
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,13 @@ export class FindCursor extends AbstractCursor {
4949
}
5050
}
5151

52+
clone(): FindCursor {
53+
return new FindCursor(this.topology, this.namespace, this[kFilter], {
54+
...this[kBuiltOptions],
55+
...this.cursorOptions
56+
});
57+
}
58+
5259
/** @internal */
5360
_initialize(session: ClientSession | undefined, callback: Callback<ExecutionResult>): void {
5461
const findOperation = new FindOperation(undefined, this.namespace, this[kFilter], {

src/operations/indexes.ts

+7
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,13 @@ export class ListIndexesCursor extends AbstractCursor {
368368
this.options = options;
369369
}
370370

371+
clone(): ListIndexesCursor {
372+
return new ListIndexesCursor(this.parent, {
373+
...this.options,
374+
...this.cursorOptions
375+
});
376+
}
377+
371378
/** @internal */
372379
_initialize(session: ClientSession | undefined, callback: Callback<ExecutionResult>): void {
373380
const operation = new ListIndexesOperation(this.parent, {

src/operations/list_collections.ts

+7
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,13 @@ export class ListCollectionsCursor extends AbstractCursor {
115115
this.options = options;
116116
}
117117

118+
clone(): ListCollectionsCursor {
119+
return new ListCollectionsCursor(this.parent, this.filter, {
120+
...this.options,
121+
...this.cursorOptions
122+
});
123+
}
124+
118125
_initialize(session: ClientSession | undefined, callback: Callback<ExecutionResult>): void {
119126
const operation = new ListCollectionsOperation(this.parent, this.filter, {
120127
...this.cursorOptions,

test/functional/abstract_cursor.test.js

+125-6
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ describe('AbstractCursor', function () {
66
before(
77
withClientV2((client, done) => {
88
const docs = [{ a: 1 }, { a: 2 }, { a: 3 }, { a: 4 }, { a: 5 }, { a: 6 }];
9-
const coll = client.db().collection('find_cursor');
9+
const coll = client.db().collection('abstract_cursor');
1010
const tryNextColl = client.db().collection('try_next');
1111
coll.drop(() => tryNextColl.drop(() => coll.insertMany(docs, done)));
1212
})
@@ -19,7 +19,7 @@ describe('AbstractCursor', function () {
1919
const commands = [];
2020
client.on('commandStarted', filterForCommands(['getMore'], commands));
2121

22-
const coll = client.db().collection('find_cursor');
22+
const coll = client.db().collection('abstract_cursor');
2323
const cursor = coll.find({}, { batchSize: 2 });
2424
this.defer(() => cursor.close());
2525

@@ -40,7 +40,7 @@ describe('AbstractCursor', function () {
4040
const commands = [];
4141
client.on('commandStarted', filterForCommands(['killCursors'], commands));
4242

43-
const coll = client.db().collection('find_cursor');
43+
const coll = client.db().collection('abstract_cursor');
4444
const cursor = coll.find({}, { batchSize: 2 });
4545
cursor.next(err => {
4646
expect(err).to.not.exist;
@@ -59,7 +59,7 @@ describe('AbstractCursor', function () {
5959
const commands = [];
6060
client.on('commandStarted', filterForCommands(['killCursors'], commands));
6161

62-
const coll = client.db().collection('find_cursor');
62+
const coll = client.db().collection('abstract_cursor');
6363
const cursor = coll.find({}, { batchSize: 2 });
6464
cursor.toArray(err => {
6565
expect(err).to.not.exist;
@@ -79,7 +79,7 @@ describe('AbstractCursor', function () {
7979
const commands = [];
8080
client.on('commandStarted', filterForCommands(['killCursors'], commands));
8181

82-
const coll = client.db().collection('find_cursor');
82+
const coll = client.db().collection('abstract_cursor');
8383
const cursor = coll.find({}, { batchSize: 2 });
8484
cursor.close(err => {
8585
expect(err).to.not.exist;
@@ -94,7 +94,7 @@ describe('AbstractCursor', function () {
9494
it(
9595
'should iterate each document in a cursor',
9696
withClientV2(function (client, done) {
97-
const coll = client.db().collection('find_cursor');
97+
const coll = client.db().collection('abstract_cursor');
9898
const cursor = coll.find({}, { batchSize: 2 });
9999

100100
const bag = [];
@@ -143,4 +143,123 @@ describe('AbstractCursor', function () {
143143
})
144144
);
145145
});
146+
147+
context('#clone', function () {
148+
it(
149+
'should clone a find cursor',
150+
withClientV2(function (client, done) {
151+
const coll = client.db().collection('abstract_cursor');
152+
const cursor = coll.find({});
153+
this.defer(() => cursor.close());
154+
155+
cursor.toArray((err, docs) => {
156+
expect(err).to.not.exist;
157+
expect(docs).to.have.length(6);
158+
expect(cursor).property('closed').to.be.true;
159+
160+
const clonedCursor = cursor.clone();
161+
this.defer(() => clonedCursor.close());
162+
163+
clonedCursor.toArray((err, docs) => {
164+
expect(err).to.not.exist;
165+
expect(docs).to.have.length(6);
166+
expect(clonedCursor).property('closed').to.be.true;
167+
done();
168+
});
169+
});
170+
})
171+
);
172+
173+
it(
174+
'should clone an aggregate cursor',
175+
withClientV2(function (client, done) {
176+
const coll = client.db().collection('abstract_cursor');
177+
const cursor = coll.aggregate([{ $match: {} }]);
178+
this.defer(() => cursor.close());
179+
180+
cursor.toArray((err, docs) => {
181+
expect(err).to.not.exist;
182+
expect(docs).to.have.length(6);
183+
expect(cursor).property('closed').to.be.true;
184+
185+
const clonedCursor = cursor.clone();
186+
this.defer(() => clonedCursor.close());
187+
188+
clonedCursor.toArray((err, docs) => {
189+
expect(err).to.not.exist;
190+
expect(docs).to.have.length(6);
191+
expect(clonedCursor).property('closed').to.be.true;
192+
done();
193+
});
194+
});
195+
})
196+
);
197+
});
198+
199+
context('#rewind', function () {
200+
it(
201+
'should rewind a cursor',
202+
withClientV2(function (client, done) {
203+
const coll = client.db().collection('abstract_cursor');
204+
const cursor = coll.find({});
205+
this.defer(() => cursor.close());
206+
207+
cursor.toArray((err, docs) => {
208+
expect(err).to.not.exist;
209+
expect(docs).to.have.length(6);
210+
211+
cursor.rewind();
212+
cursor.toArray((err, docs) => {
213+
expect(err).to.not.exist;
214+
expect(docs).to.have.length(6);
215+
216+
done();
217+
});
218+
});
219+
})
220+
);
221+
222+
it('should end an implicit session on rewind', {
223+
metadata: { requires: { mongodb: '>=3.6' } },
224+
test: withClientV2(function (client, done) {
225+
const coll = client.db().collection('abstract_cursor');
226+
const cursor = coll.find({}, { batchSize: 1 });
227+
this.defer(() => cursor.close());
228+
229+
cursor.next((err, doc) => {
230+
expect(err).to.not.exist;
231+
expect(doc).to.exist;
232+
233+
const session = cursor.session;
234+
expect(session).property('hasEnded').to.be.false;
235+
cursor.rewind();
236+
expect(session).property('hasEnded').to.be.true;
237+
done();
238+
});
239+
})
240+
});
241+
242+
it('should not end an explicit session on rewind', {
243+
metadata: { requires: { mongodb: '>=3.6' } },
244+
test: withClientV2(function (client, done) {
245+
const coll = client.db().collection('abstract_cursor');
246+
const session = client.startSession();
247+
248+
const cursor = coll.find({}, { batchSize: 1, session });
249+
this.defer(() => cursor.close());
250+
251+
cursor.next((err, doc) => {
252+
expect(err).to.not.exist;
253+
expect(doc).to.exist;
254+
255+
const session = cursor.session;
256+
expect(session).property('hasEnded').to.be.false;
257+
cursor.rewind();
258+
expect(session).property('hasEnded').to.be.false;
259+
260+
session.endSession(done);
261+
});
262+
})
263+
});
264+
});
146265
});

test/functional/operation_example.test.js

+1-2
Original file line numberDiff line numberDiff line change
@@ -4908,8 +4908,7 @@ describe('Operation Examples', function () {
49084908
* @example-class Cursor
49094909
* @example-method rewind
49104910
*/
4911-
// NOTE: unclear whether we should continue to support `rewind`
4912-
it.skip('Should correctly rewind and restart cursor', {
4911+
it('Should correctly rewind and restart cursor', {
49134912
// Add a tag that our runner can trigger on
49144913
// in this case we are setting that node needs to be higher than 0.10.X to run
49154914
metadata: {

0 commit comments

Comments
 (0)