mongodb 6.7.0-dev.20240607.sha.aa429f8c → 6.7.0-dev.20240608.sha.0655c730

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,12 +3,10 @@ import { Readable, Transform } from 'stream';
3
3
  import { type BSONSerializeOptions, type Document, Long, pluckBSONSerializeOptions } from '../bson';
4
4
  import { CursorResponse } from '../cmap/wire_protocol/responses';
5
5
  import {
6
- type AnyError,
7
6
  MongoAPIError,
8
7
  MongoCursorExhaustedError,
9
8
  MongoCursorInUseError,
10
9
  MongoInvalidArgumentError,
11
- MongoNetworkError,
12
10
  MongoRuntimeError,
13
11
  MongoTailableCursorError
14
12
  } from '../error';
@@ -23,31 +21,6 @@ import type { Server } from '../sdam/server';
23
21
  import { ClientSession, maybeClearPinnedConnection } from '../sessions';
24
22
  import { List, type MongoDBNamespace, ns, squashError } from '../utils';
25
23
 
26
- /** @internal */
27
- const kId = Symbol('id');
28
- /** @internal */
29
- const kDocuments = Symbol('documents');
30
- /** @internal */
31
- const kServer = Symbol('server');
32
- /** @internal */
33
- const kNamespace = Symbol('namespace');
34
- /** @internal */
35
- const kClient = Symbol('client');
36
- /** @internal */
37
- const kSession = Symbol('session');
38
- /** @internal */
39
- const kOptions = Symbol('options');
40
- /** @internal */
41
- const kTransform = Symbol('transform');
42
- /** @internal */
43
- const kInitialized = Symbol('initialized');
44
- /** @internal */
45
- const kClosed = Symbol('closed');
46
- /** @internal */
47
- const kKilled = Symbol('killed');
48
- /** @internal */
49
- const kInit = Symbol('kInit');
50
-
51
24
  /** @public */
52
25
  export const CURSOR_FLAGS = [
53
26
  'tailable',
@@ -137,15 +110,15 @@ export abstract class AbstractCursor<
137
110
  CursorEvents extends AbstractCursorEvents = AbstractCursorEvents
138
111
  > extends TypedEventEmitter<CursorEvents> {
139
112
  /** @internal */
140
- [kId]: Long | null;
113
+ private cursorId: Long | null;
141
114
  /** @internal */
142
- [kSession]: ClientSession;
115
+ private cursorSession: ClientSession;
143
116
  /** @internal */
144
- [kServer]?: Server;
117
+ private selectedServer?: Server;
145
118
  /** @internal */
146
- [kNamespace]: MongoDBNamespace;
119
+ private cursorNamespace: MongoDBNamespace;
147
120
  /** @internal */
148
- [kDocuments]: {
121
+ private documents: {
149
122
  length: number;
150
123
  shift(bsonOptions?: any): TSchema | null;
151
124
  clear(): void;
@@ -153,23 +126,23 @@ export abstract class AbstractCursor<
153
126
  push(item: TSchema): void;
154
127
  };
155
128
  /** @internal */
156
- [kClient]: MongoClient;
129
+ private cursorClient: MongoClient;
157
130
  /** @internal */
158
- [kTransform]?: (doc: TSchema) => any;
131
+ private transform?: (doc: TSchema) => any;
159
132
  /** @internal */
160
- [kInitialized]: boolean;
133
+ private initialized: boolean;
161
134
  /** @internal */
162
- [kClosed]: boolean;
135
+ private isClosed: boolean;
163
136
  /** @internal */
164
- [kKilled]: boolean;
137
+ private isKilled: boolean;
165
138
  /** @internal */
166
- [kOptions]: InternalAbstractCursorOptions;
139
+ protected readonly cursorOptions: InternalAbstractCursorOptions;
167
140
 
168
141
  /** @event */
169
142
  static readonly CLOSE = 'close' as const;
170
143
 
171
144
  /** @internal */
172
- constructor(
145
+ protected constructor(
173
146
  client: MongoClient,
174
147
  namespace: MongoDBNamespace,
175
148
  options: AbstractCursorOptions = {}
@@ -179,121 +152,116 @@ export abstract class AbstractCursor<
179
152
  if (!client.s.isMongoClient) {
180
153
  throw new MongoRuntimeError('Cursor must be constructed with MongoClient');
181
154
  }
182
- this[kClient] = client;
183
- this[kNamespace] = namespace;
184
- this[kId] = null;
185
- this[kDocuments] = new List();
186
- this[kInitialized] = false;
187
- this[kClosed] = false;
188
- this[kKilled] = false;
189
- this[kOptions] = {
155
+ this.cursorClient = client;
156
+ this.cursorNamespace = namespace;
157
+ this.cursorId = null;
158
+ this.documents = new List();
159
+ this.initialized = false;
160
+ this.isClosed = false;
161
+ this.isKilled = false;
162
+ this.cursorOptions = {
190
163
  readPreference:
191
164
  options.readPreference && options.readPreference instanceof ReadPreference
192
165
  ? options.readPreference
193
166
  : ReadPreference.primary,
194
167
  ...pluckBSONSerializeOptions(options)
195
168
  };
196
- this[kOptions].timeoutMS = options.timeoutMS;
169
+ this.cursorOptions.timeoutMS = options.timeoutMS;
197
170
 
198
171
  const readConcern = ReadConcern.fromOptions(options);
199
172
  if (readConcern) {
200
- this[kOptions].readConcern = readConcern;
173
+ this.cursorOptions.readConcern = readConcern;
201
174
  }
202
175
 
203
176
  if (typeof options.batchSize === 'number') {
204
- this[kOptions].batchSize = options.batchSize;
177
+ this.cursorOptions.batchSize = options.batchSize;
205
178
  }
206
179
 
207
180
  // we check for undefined specifically here to allow falsy values
208
181
  // eslint-disable-next-line no-restricted-syntax
209
182
  if (options.comment !== undefined) {
210
- this[kOptions].comment = options.comment;
183
+ this.cursorOptions.comment = options.comment;
211
184
  }
212
185
 
213
186
  if (typeof options.maxTimeMS === 'number') {
214
- this[kOptions].maxTimeMS = options.maxTimeMS;
187
+ this.cursorOptions.maxTimeMS = options.maxTimeMS;
215
188
  }
216
189
 
217
190
  if (typeof options.maxAwaitTimeMS === 'number') {
218
- this[kOptions].maxAwaitTimeMS = options.maxAwaitTimeMS;
191
+ this.cursorOptions.maxAwaitTimeMS = options.maxAwaitTimeMS;
219
192
  }
220
193
 
221
194
  if (options.session instanceof ClientSession) {
222
- this[kSession] = options.session;
195
+ this.cursorSession = options.session;
223
196
  } else {
224
- this[kSession] = this[kClient].startSession({ owner: this, explicit: false });
197
+ this.cursorSession = this.cursorClient.startSession({ owner: this, explicit: false });
225
198
  }
226
199
  }
227
200
 
228
201
  get id(): Long | undefined {
229
- return this[kId] ?? undefined;
202
+ return this.cursorId ?? undefined;
230
203
  }
231
204
 
232
205
  /** @internal */
233
206
  get isDead() {
234
- return (this[kId]?.isZero() ?? false) || this[kClosed] || this[kKilled];
207
+ return (this.cursorId?.isZero() ?? false) || this.isClosed || this.isKilled;
235
208
  }
236
209
 
237
210
  /** @internal */
238
211
  get client(): MongoClient {
239
- return this[kClient];
212
+ return this.cursorClient;
240
213
  }
241
214
 
242
215
  /** @internal */
243
216
  get server(): Server | undefined {
244
- return this[kServer];
217
+ return this.selectedServer;
245
218
  }
246
219
 
247
220
  get namespace(): MongoDBNamespace {
248
- return this[kNamespace];
221
+ return this.cursorNamespace;
249
222
  }
250
223
 
251
224
  get readPreference(): ReadPreference {
252
- return this[kOptions].readPreference;
225
+ return this.cursorOptions.readPreference;
253
226
  }
254
227
 
255
228
  get readConcern(): ReadConcern | undefined {
256
- return this[kOptions].readConcern;
229
+ return this.cursorOptions.readConcern;
257
230
  }
258
231
 
259
232
  /** @internal */
260
233
  get session(): ClientSession {
261
- return this[kSession];
234
+ return this.cursorSession;
262
235
  }
263
236
 
264
237
  set session(clientSession: ClientSession) {
265
- this[kSession] = clientSession;
266
- }
267
-
268
- /** @internal */
269
- get cursorOptions(): InternalAbstractCursorOptions {
270
- return this[kOptions];
238
+ this.cursorSession = clientSession;
271
239
  }
272
240
 
273
241
  get closed(): boolean {
274
- return this[kClosed];
242
+ return this.isClosed;
275
243
  }
276
244
 
277
245
  get killed(): boolean {
278
- return this[kKilled];
246
+ return this.isKilled;
279
247
  }
280
248
 
281
249
  get loadBalanced(): boolean {
282
- return !!this[kClient].topology?.loadBalanced;
250
+ return !!this.cursorClient.topology?.loadBalanced;
283
251
  }
284
252
 
285
253
  /** Returns current buffered documents length */
286
254
  bufferedCount(): number {
287
- return this[kDocuments].length;
255
+ return this.documents.length;
288
256
  }
289
257
 
290
258
  /** Returns current buffered documents */
291
259
  readBufferedDocuments(number?: number): TSchema[] {
292
260
  const bufferedDocs: TSchema[] = [];
293
- const documentsToRead = Math.min(number ?? this[kDocuments].length, this[kDocuments].length);
261
+ const documentsToRead = Math.min(number ?? this.documents.length, this.documents.length);
294
262
 
295
263
  for (let count = 0; count < documentsToRead; count++) {
296
- const document = this[kDocuments].shift(this[kOptions]);
264
+ const document = this.documents.shift(this.cursorOptions);
297
265
  if (document != null) {
298
266
  bufferedDocs.push(document);
299
267
  }
@@ -303,44 +271,37 @@ export abstract class AbstractCursor<
303
271
  }
304
272
 
305
273
  async *[Symbol.asyncIterator](): AsyncGenerator<TSchema, void, void> {
306
- if (this.closed) {
274
+ if (this.isClosed) {
307
275
  return;
308
276
  }
309
277
 
310
278
  try {
311
279
  while (true) {
280
+ if (this.isKilled) {
281
+ return;
282
+ }
283
+
284
+ if (this.isClosed && this.documents.length === 0) {
285
+ return;
286
+ }
287
+
288
+ if (this.cursorId != null && this.isDead && this.documents.length === 0) {
289
+ return;
290
+ }
291
+
312
292
  const document = await this.next();
313
293
 
314
- // Intentional strict null check, because users can map cursors to falsey values.
315
- // We allow mapping to all values except for null.
316
294
  // eslint-disable-next-line no-restricted-syntax
317
295
  if (document === null) {
318
- if (!this.closed) {
319
- const message =
320
- 'Cursor returned a `null` document, but the cursor is not exhausted. Mapping documents to `null` is not supported in the cursor transform.';
321
-
322
- try {
323
- await cleanupCursor(this, { needsToEmitClosed: true });
324
- } catch (error) {
325
- squashError(error);
326
- }
327
-
328
- throw new MongoAPIError(message);
329
- }
330
- break;
296
+ return;
331
297
  }
332
298
 
333
299
  yield document;
334
-
335
- if (this[kId] === Long.ZERO) {
336
- // Cursor exhausted
337
- break;
338
- }
339
300
  }
340
301
  } finally {
341
302
  // Only close the cursor if it has not already been closed. This finally clause handles
342
303
  // the case when a user would break out of a for await of loop early.
343
- if (!this.closed) {
304
+ if (!this.isClosed) {
344
305
  try {
345
306
  await this.close();
346
307
  } catch (error) {
@@ -381,35 +342,61 @@ export abstract class AbstractCursor<
381
342
  }
382
343
 
383
344
  async hasNext(): Promise<boolean> {
384
- if (this[kId] === Long.ZERO) {
345
+ if (this.cursorId === Long.ZERO) {
385
346
  return false;
386
347
  }
387
348
 
388
- if (this[kDocuments].length !== 0) {
389
- return true;
390
- }
349
+ do {
350
+ if (this.documents.length !== 0) {
351
+ return true;
352
+ }
353
+ await this.fetchBatch();
354
+ } while (!this.isDead || this.documents.length !== 0);
391
355
 
392
- return await next(this, { blocking: true, transform: false, shift: false });
356
+ return false;
393
357
  }
394
358
 
395
359
  /** Get the next available document from the cursor, returns null if no more documents are available. */
396
360
  async next(): Promise<TSchema | null> {
397
- if (this[kId] === Long.ZERO) {
361
+ if (this.cursorId === Long.ZERO) {
398
362
  throw new MongoCursorExhaustedError();
399
363
  }
400
364
 
401
- return await next(this, { blocking: true, transform: true, shift: true });
365
+ do {
366
+ const doc = this.documents.shift();
367
+ if (doc != null) {
368
+ if (this.transform != null) return await this.transformDocument(doc);
369
+ return doc;
370
+ }
371
+ await this.fetchBatch();
372
+ } while (!this.isDead || this.documents.length !== 0);
373
+
374
+ return null;
402
375
  }
403
376
 
404
377
  /**
405
378
  * Try to get the next available document from the cursor or `null` if an empty batch is returned
406
379
  */
407
380
  async tryNext(): Promise<TSchema | null> {
408
- if (this[kId] === Long.ZERO) {
381
+ if (this.cursorId === Long.ZERO) {
409
382
  throw new MongoCursorExhaustedError();
410
383
  }
411
384
 
412
- return await next(this, { blocking: false, transform: true, shift: true });
385
+ let doc = this.documents.shift();
386
+ if (doc != null) {
387
+ if (this.transform != null) return await this.transformDocument(doc);
388
+ return doc;
389
+ }
390
+
391
+ await this.fetchBatch();
392
+
393
+ doc = this.documents.shift();
394
+ if (doc != null) {
395
+ if (this.transform != null) return await this.transformDocument(doc);
396
+ return doc;
397
+ }
398
+
399
+ return null;
413
400
  }
414
401
 
415
402
  /**
@@ -433,9 +420,7 @@ export abstract class AbstractCursor<
433
420
  }
434
421
 
435
422
  async close(): Promise<void> {
436
- const needsToEmitClosed = !this[kClosed];
437
- this[kClosed] = true;
438
- await cleanupCursor(this, { needsToEmitClosed });
423
+ await this.cleanup();
439
424
  }
440
425
 
441
426
  /**
@@ -459,7 +444,7 @@ export abstract class AbstractCursor<
459
444
  * @param value - The flag boolean value.
460
445
  */
461
446
  addCursorFlag(flag: CursorFlag, value: boolean): this {
462
- assertUninitialized(this);
447
+ this.throwIfInitialized();
463
448
  if (!CURSOR_FLAGS.includes(flag)) {
464
449
  throw new MongoInvalidArgumentError(`Flag ${flag} is not one of ${CURSOR_FLAGS}`);
465
450
  }
@@ -468,7 +453,7 @@ export abstract class AbstractCursor<
468
453
  throw new MongoInvalidArgumentError(`Flag ${flag} must be a boolean value`);
469
454
  }
470
455
 
471
- this[kOptions][flag] = value;
456
+ this.cursorOptions[flag] = value;
472
457
  return this;
473
458
  }
474
459
 
@@ -515,14 +500,14 @@ export abstract class AbstractCursor<
515
500
  * @param transform - The mapping transformation method.
516
501
  */
517
502
  map<T = any>(transform: (doc: TSchema) => T): AbstractCursor<T> {
518
- assertUninitialized(this);
519
- const oldTransform = this[kTransform] as (doc: TSchema) => TSchema; // TODO(NODE-3283): Improve transform typing
503
+ this.throwIfInitialized();
504
+ const oldTransform = this.transform;
520
505
  if (oldTransform) {
521
- this[kTransform] = doc => {
506
+ this.transform = doc => {
522
507
  return transform(oldTransform(doc));
523
508
  };
524
509
  } else {
525
- this[kTransform] = transform;
510
+ this.transform = transform;
526
511
  }
527
512
 
528
513
  return this as unknown as AbstractCursor<T>;
@@ -534,11 +519,11 @@ export abstract class AbstractCursor<
534
519
  * @param readPreference - The new read preference for the cursor.
535
520
  */
536
521
  withReadPreference(readPreference: ReadPreferenceLike): this {
537
- assertUninitialized(this);
522
+ this.throwIfInitialized();
538
523
  if (readPreference instanceof ReadPreference) {
539
- this[kOptions].readPreference = readPreference;
524
+ this.cursorOptions.readPreference = readPreference;
540
525
  } else if (typeof readPreference === 'string') {
541
- this[kOptions].readPreference = ReadPreference.fromString(readPreference);
526
+ this.cursorOptions.readPreference = ReadPreference.fromString(readPreference);
542
527
  } else {
543
528
  throw new MongoInvalidArgumentError(`Invalid read preference: ${readPreference}`);
544
529
  }
@@ -552,10 +537,10 @@ export abstract class AbstractCursor<
552
537
  * @param readPreference - The new read preference for the cursor.
553
538
  */
554
539
  withReadConcern(readConcern: ReadConcernLike): this {
555
- assertUninitialized(this);
540
+ this.throwIfInitialized();
556
541
  const resolvedReadConcern = ReadConcern.fromOptions({ readConcern });
557
542
  if (resolvedReadConcern) {
558
- this[kOptions].readConcern = resolvedReadConcern;
543
+ this.cursorOptions.readConcern = resolvedReadConcern;
559
544
  }
560
545
 
561
546
  return this;
@@ -567,12 +552,12 @@ export abstract class AbstractCursor<
567
552
  * @param value - Number of milliseconds to wait before aborting the query.
568
553
  */
569
554
  maxTimeMS(value: number): this {
570
- assertUninitialized(this);
555
+ this.throwIfInitialized();
571
556
  if (typeof value !== 'number') {
572
557
  throw new MongoInvalidArgumentError('Argument for maxTimeMS must be a number');
573
558
  }
574
559
 
575
- this[kOptions].maxTimeMS = value;
560
+ this.cursorOptions.maxTimeMS = value;
576
561
  return this;
577
562
  }
578
563
 
@@ -582,8 +567,8 @@ export abstract class AbstractCursor<
582
567
  * @param value - The number of documents to return per batch. See {@link https://www.mongodb.com/docs/manual/reference/command/find/|find command documentation}.
583
568
  */
584
569
  batchSize(value: number): this {
585
- assertUninitialized(this);
586
- if (this[kOptions].tailable) {
570
+ this.throwIfInitialized();
571
+ if (this.cursorOptions.tailable) {
587
572
  throw new MongoTailableCursorError('Tailable cursor does not support batchSize');
588
573
  }
589
574
 
@@ -591,7 +576,7 @@ export abstract class AbstractCursor<
591
576
  throw new MongoInvalidArgumentError('Operation "batchSize" requires an integer');
592
577
  }
593
578
 
594
- this[kOptions].batchSize = value;
579
+ this.cursorOptions.batchSize = value;
595
580
  return this;
596
581
  }
597
582
 
@@ -601,17 +586,17 @@ export abstract class AbstractCursor<
601
586
  * if the resultant data has already been retrieved by this cursor.
602
587
  */
603
588
  rewind(): void {
604
- if (!this[kInitialized]) {
589
+ if (!this.initialized) {
605
590
  return;
606
591
  }
607
592
 
608
- this[kId] = null;
609
- this[kDocuments].clear();
610
- this[kClosed] = false;
611
- this[kKilled] = false;
612
- this[kInitialized] = false;
593
+ this.cursorId = null;
594
+ this.documents.clear();
595
+ this.isClosed = false;
596
+ this.isKilled = false;
597
+ this.initialized = false;
613
598
 
614
- const session = this[kSession];
599
+ const session = this.cursorSession;
615
600
  if (session) {
616
601
  // We only want to end this session if we created it, and it hasn't ended yet
617
602
  if (session.explicit === false) {
@@ -619,7 +604,7 @@ export abstract class AbstractCursor<
619
604
  // eslint-disable-next-line github/no-then
620
605
  session.endSession().then(undefined, squashError);
621
606
  }
622
- this[kSession] = this.client.startSession({ owner: this, explicit: false });
607
+ this.cursorSession = this.cursorClient.startSession({ owner: this, explicit: false });
623
608
  }
624
609
  }
625
610
  }
@@ -634,15 +619,29 @@ export abstract class AbstractCursor<
634
619
 
635
620
  /** @internal */
636
621
  async getMore(batchSize: number, useCursorResponse = false): Promise<Document | null> {
637
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
638
- const getMoreOperation = new GetMoreOperation(this[kNamespace], this[kId]!, this[kServer]!, {
639
- ...this[kOptions],
640
- session: this[kSession],
641
- batchSize,
642
- useCursorResponse
643
- });
622
+ if (this.cursorId == null) {
623
+ throw new MongoRuntimeError(
624
+ 'Unexpected null cursor id. A cursor creating command should have set this'
625
+ );
626
+ }
627
+ if (this.selectedServer == null) {
628
+ throw new MongoRuntimeError(
629
+ 'Unexpected null selectedServer. A cursor creating command should have set this'
630
+ );
631
+ }
632
+ const getMoreOperation = new GetMoreOperation(
633
+ this.cursorNamespace,
634
+ this.cursorId,
635
+ this.selectedServer,
636
+ {
637
+ ...this.cursorOptions,
638
+ session: this.cursorSession,
639
+ batchSize,
640
+ useCursorResponse
641
+ }
642
+ );
644
643
 
645
- return await executeOperation(this[kClient], getMoreOperation);
644
+ return await executeOperation(this.cursorClient, getMoreOperation);
646
645
  }
647
646
 
648
647
  /**
@@ -652,169 +651,85 @@ export abstract class AbstractCursor<
652
651
  * operation. We cannot refactor to use the abstract _initialize method without
653
652
  * a significant refactor.
654
653
  */
655
- async [kInit](): Promise<void> {
654
+ private async cursorInit(): Promise<void> {
656
655
  try {
657
- const state = await this._initialize(this[kSession]);
656
+ const state = await this._initialize(this.cursorSession);
658
657
  const response = state.response;
659
- this[kServer] = state.server;
658
+ this.selectedServer = state.server;
660
659
  if (CursorResponse.is(response)) {
661
- this[kId] = response.id;
662
- if (response.ns) this[kNamespace] = response.ns;
663
- this[kDocuments] = response;
660
+ this.cursorId = response.id;
661
+ if (response.ns) this.cursorNamespace = response.ns;
662
+ this.documents = response;
664
663
  } else if (response.cursor) {
665
664
  // TODO(NODE-2674): Preserve int64 sent from MongoDB
666
- this[kId] =
667
- typeof response.cursor.id === 'number'
668
- ? Long.fromNumber(response.cursor.id)
669
- : typeof response.cursor.id === 'bigint'
670
- ? Long.fromBigInt(response.cursor.id)
671
- : response.cursor.id;
672
-
673
- if (response.cursor.ns) {
674
- this[kNamespace] = ns(response.cursor.ns);
675
- }
676
-
677
- this[kDocuments].pushMany(response.cursor.firstBatch);
665
+ this.cursorId = getCursorId(response);
666
+ if (response.cursor.ns) this.cursorNamespace = ns(response.cursor.ns);
667
+ this.documents.pushMany(response.cursor.firstBatch);
678
668
  }
679
669
 
680
- // When server responses return without a cursor document, we close this cursor
681
- // and return the raw server response. This is often the case for explain commands
682
- // for example
683
- if (this[kId] == null) {
684
- this[kId] = Long.ZERO;
670
+ if (this.cursorId == null) {
671
+ // When server responses return without a cursor document, we close this cursor
672
+ // and return the raw server response. This is the case for explain commands
673
+ this.cursorId = Long.ZERO;
685
674
  // TODO(NODE-3286): ExecutionResult needs to accept a generic parameter
686
- this[kDocuments].push(state.response as TODO_NODE_3286);
675
+ this.documents.push(state.response as TODO_NODE_3286);
687
676
  }
688
677
 
689
678
  // the cursor is now initialized, even if it is dead
690
- this[kInitialized] = true;
679
+ this.initialized = true;
691
680
  } catch (error) {
692
681
  // the cursor is now initialized, even if an error occurred
693
- this[kInitialized] = true;
694
- await cleanupCursor(this, { error });
682
+ this.initialized = true;
683
+ await this.cleanup(error);
695
684
  throw error;
696
685
  }
697
686
 
698
687
  if (this.isDead) {
699
- await cleanupCursor(this, undefined);
688
+ await this.cleanup();
700
689
  }
701
690
 
702
691
  return;
703
692
  }
704
- }
705
-
706
- /**
707
- * @param cursor - the cursor on which to call `next`
708
- * @param blocking - a boolean indicating whether or not the cursor should `block` until data
709
- * is available. Generally, this flag is set to `false` because if the getMore returns no documents,
710
- * the cursor has been exhausted. In certain scenarios (ChangeStreams, tailable await cursors and
711
- * `tryNext`, for example) blocking is necessary because a getMore returning no documents does
712
- * not indicate the end of the cursor.
713
- * @param transform - if true, the cursor's transform function is applied to the result document (if the transform exists)
714
- * @returns the next document in the cursor, or `null`. When `blocking` is `true`, a `null` document means
715
- * the cursor has been exhausted. Otherwise, it means that there is no document available in the cursor's buffer.
716
- */
717
- async function next<T>(
718
- cursor: AbstractCursor<T>,
719
- {
720
- blocking,
721
- transform,
722
- shift
723
- }: {
724
- blocking: boolean;
725
- transform: boolean;
726
- shift: false;
727
- }
728
- ): Promise<boolean>;
729
-
730
- async function next<T>(
731
- cursor: AbstractCursor<T>,
732
- {
733
- blocking,
734
- transform,
735
- shift
736
- }: {
737
- blocking: boolean;
738
- transform: boolean;
739
- shift: true;
740
- }
741
- ): Promise<T | null>;
742
-
743
- async function next<T>(
744
- cursor: AbstractCursor<T>,
745
- {
746
- blocking,
747
- transform,
748
- shift
749
- }: {
750
- blocking: boolean;
751
- transform: boolean;
752
- shift: boolean;
753
- }
754
- ): Promise<boolean | T | null> {
755
- if (cursor.closed) {
756
- if (!shift) return false;
757
- return null;
758
- }
759
-
760
- do {
761
- if (cursor[kId] == null) {
762
- // All cursors must operate within a session, one must be made implicitly if not explicitly provided
763
- await cursor[kInit]();
764
- }
765
-
766
- if (cursor[kDocuments].length !== 0) {
767
- if (!shift) return true;
768
- const doc = cursor[kDocuments].shift(cursor[kOptions]);
769
693
 
770
- if (doc != null && transform && cursor[kTransform]) {
771
- try {
772
- return cursor[kTransform](doc);
773
- } catch (error) {
774
- try {
775
- await cleanupCursor(cursor, { error, needsToEmitClosed: true });
776
- } catch (error) {
777
- // `cleanupCursor` should never throw, squash and throw the original error
778
- squashError(error);
779
- }
780
- throw error;
781
- }
782
- }
783
-
784
- return doc;
694
+ /** @internal Attempt to obtain more documents */
695
+ private async fetchBatch(): Promise<void> {
696
+ if (this.isClosed) {
697
+ return;
785
698
  }
786
699
 
787
- if (cursor.isDead) {
700
+ if (this.isDead) {
788
701
  // if the cursor is dead, we clean it up
789
702
  // cleanupCursor should never throw, but if it does it indicates a bug in the driver
790
703
  // and we should surface the error
791
- await cleanupCursor(cursor, {});
792
- if (!shift) return false;
793
- return null;
704
+ await this.cleanup();
705
+ return;
706
+ }
707
+
708
+ if (this.cursorId == null) {
709
+ await this.cursorInit();
710
+ // If the cursor died or returned documents, return
711
+ if (this.documents.length !== 0 || this.isDead) return;
712
+ // Otherwise, run a getMore
794
713
  }
795
714
 
796
715
  // otherwise need to call getMore
797
- const batchSize = cursor[kOptions].batchSize || 1000;
716
+ const batchSize = this.cursorOptions.batchSize || 1000;
798
717
 
799
718
  try {
800
- const response = await cursor.getMore(batchSize);
719
+ const response = await this.getMore(batchSize);
720
+ // CursorResponse is disabled in this PR
721
+ // however the special `emptyGetMore` can be returned from find cursors
801
722
  if (CursorResponse.is(response)) {
802
- cursor[kId] = response.id;
803
- cursor[kDocuments] = response;
804
- } else if (response) {
805
- const cursorId =
806
- typeof response.cursor.id === 'number'
807
- ? Long.fromNumber(response.cursor.id)
808
- : typeof response.cursor.id === 'bigint'
809
- ? Long.fromBigInt(response.cursor.id)
810
- : response.cursor.id;
811
-
812
- cursor[kDocuments].pushMany(response.cursor.nextBatch);
813
- cursor[kId] = cursorId;
723
+ this.cursorId = response.id;
724
+ this.documents = response;
725
+ } else if (response?.cursor) {
726
+ const cursorId = getCursorId(response);
727
+ this.documents.pushMany(response.cursor.nextBatch);
728
+ this.cursorId = cursorId;
814
729
  }
815
730
  } catch (error) {
816
731
  try {
817
- await cleanupCursor(cursor, { error, needsToEmitClosed: true });
732
+ await this.cleanup(error);
818
733
  } catch (error) {
819
734
  // `cleanupCursor` should never throw, squash and throw the original error
820
735
  squashError(error);
@@ -822,7 +737,7 @@ async function next<T>(
822
737
  throw error;
823
738
  }
824
739
 
825
- if (cursor.isDead) {
740
+ if (this.isDead) {
826
741
  // If we successfully received a response from a cursor BUT the cursor indicates that it is exhausted,
827
742
  // we intentionally clean up the cursor to release its session back into the pool before the cursor
828
743
  // is iterated. This prevents a cursor that is exhausted on the server from holding
@@ -830,104 +745,95 @@ async function next<T>(
830
745
  //
831
746
  // cleanupCursorAsync should never throw, but if it does it indicates a bug in the driver
832
747
  // and we should surface the error
833
- await cleanupCursor(cursor, {});
834
- }
835
-
836
- if (cursor[kDocuments].length === 0 && blocking === false) {
837
- if (!shift) return false;
838
- return null;
839
- }
840
- } while (!cursor.isDead || cursor[kDocuments].length !== 0);
841
-
842
- if (!shift) return false;
843
- return null;
844
- }
845
-
846
- async function cleanupCursor(
847
- cursor: AbstractCursor,
848
- options: { error?: AnyError | undefined; needsToEmitClosed?: boolean } | undefined
849
- ): Promise<void> {
850
- const cursorId = cursor[kId];
851
- const cursorNs = cursor[kNamespace];
852
- const server = cursor[kServer];
853
- const session = cursor[kSession];
854
- const error = options?.error;
855
-
856
- // Cursors only emit closed events once the client-side cursor has been exhausted fully or there
857
- // was an error. Notably, when the server returns a cursor id of 0 and a non-empty batch, we
858
- // cleanup the cursor but don't emit a `close` event.
859
- const needsToEmitClosed = options?.needsToEmitClosed ?? cursor[kDocuments].length === 0;
860
-
861
- if (error) {
862
- if (cursor.loadBalanced && error instanceof MongoNetworkError) {
863
- return await completeCleanup();
748
+ await this.cleanup();
864
749
  }
865
750
  }
866
751
 
867
- if (cursorId == null || server == null || cursorId.isZero() || cursorNs == null) {
868
- if (needsToEmitClosed) {
869
- cursor[kClosed] = true;
870
- cursor[kId] = Long.ZERO;
871
- cursor.emit(AbstractCursor.CLOSE);
872
- }
873
-
874
- if (session) {
875
- if (session.owner === cursor) {
752
+ /** @internal */
753
+ private async cleanup(error?: Error) {
754
+ this.isClosed = true;
755
+ const session = this.cursorSession;
756
+ try {
757
+ if (
758
+ !this.isKilled &&
759
+ this.cursorId &&
760
+ !this.cursorId.isZero() &&
761
+ this.cursorNamespace &&
762
+ this.selectedServer &&
763
+ !session.hasEnded
764
+ ) {
765
+ this.isKilled = true;
766
+ await executeOperation(
767
+ this.cursorClient,
768
+ new KillCursorsOperation(this.cursorId, this.cursorNamespace, this.selectedServer, {
769
+ session
770
+ })
771
+ );
772
+ }
773
+ } catch (error) {
774
+ squashError(error);
775
+ } finally {
776
+ if (session?.owner === this) {
876
777
  await session.endSession({ error });
877
- return;
878
778
  }
879
-
880
- if (!session.inTransaction()) {
779
+ if (!session?.inTransaction()) {
881
780
  maybeClearPinnedConnection(session, { error });
882
781
  }
883
- }
884
782
 
885
- return;
783
+ this.emitClose();
784
+ }
886
785
  }
887
786
 
888
- async function completeCleanup() {
889
- if (session) {
890
- if (session.owner === cursor) {
891
- try {
892
- await session.endSession({ error });
893
- } finally {
894
- cursor.emit(AbstractCursor.CLOSE);
895
- }
896
- return;
897
- }
898
-
899
- if (!session.inTransaction()) {
900
- maybeClearPinnedConnection(session, { error });
787
+ /** @internal */
788
+ private hasEmittedClose = false;
789
+ /** @internal */
790
+ private emitClose() {
791
+ try {
792
+ if (!this.hasEmittedClose && (this.documents.length === 0 || this.isClosed)) {
793
+ // @ts-expect-error: CursorEvents is generic so Parameters<CursorEvents["close"]> may not be assignable to `[]`. Not sure how to require extenders do not add parameters.
794
+ this.emit('close');
901
795
  }
796
+ } finally {
797
+ this.hasEmittedClose = true;
902
798
  }
903
-
904
- cursor.emit(AbstractCursor.CLOSE);
905
- return;
906
799
  }
907
800
 
908
- cursor[kKilled] = true;
801
+ /** @internal */
802
+ private async transformDocument(document: NonNullable<TSchema>): Promise<TSchema> {
803
+ if (this.transform == null) return document;
909
804
 
910
- if (session.hasEnded) {
911
- return await completeCleanup();
805
+ try {
806
+ const transformedDocument = this.transform(document);
807
+ // eslint-disable-next-line no-restricted-syntax
808
+ if (transformedDocument === null) {
809
+ const TRANSFORM_TO_NULL_ERROR =
810
+ 'Cursor returned a `null` document, but the cursor is not exhausted. Mapping documents to `null` is not supported in the cursor transform.';
811
+ throw new MongoAPIError(TRANSFORM_TO_NULL_ERROR);
812
+ }
813
+ return transformedDocument;
814
+ } catch (transformError) {
815
+ try {
816
+ await this.close();
817
+ } catch (closeError) {
818
+ squashError(closeError);
819
+ }
820
+ throw transformError;
821
+ }
912
822
  }
913
823
 
914
- try {
915
- await executeOperation(
916
- cursor[kClient],
917
- new KillCursorsOperation(cursorId, cursorNs, server, { session })
918
- );
919
- } catch (error) {
920
- squashError(error);
921
- } finally {
922
- await completeCleanup();
824
+ /** @internal */
825
+ protected throwIfInitialized() {
826
+ if (this.initialized) throw new MongoCursorInUseError();
923
827
  }
924
828
  }
925
829
 
926
- /** @internal */
927
- export function assertUninitialized(cursor: AbstractCursor): void {
928
- if (cursor[kInitialized]) {
929
- throw new MongoCursorInUseError();
930
- }
830
+ /** A temporary helper to box up the many possible type issue of cursor ids */
831
+ function getCursorId(response: Document) {
832
+ return typeof response.cursor.id === 'number'
833
+ ? Long.fromNumber(response.cursor.id)
834
+ : typeof response.cursor.id === 'bigint'
835
+ ? Long.fromBigInt(response.cursor.id)
836
+ : response.cursor.id;
931
837
  }
932
838
 
933
839
  class ReadableCursorStream extends Readable {
@@ -960,8 +866,13 @@ class ReadableCursorStream extends Readable {
960
866
  }
961
867
 
962
868
  private _readNext() {
869
+ if (this._cursor.id === Long.ZERO) {
870
+ this.push(null);
871
+ return;
872
+ }
873
+
963
874
  // eslint-disable-next-line github/no-then
964
- next(this._cursor, { blocking: true, transform: true, shift: true }).then(
875
+ this._cursor.next().then(
965
876
  result => {
966
877
  if (result == null) {
967
878
  this.push(null);