mongodb 6.7.0 → 6.8.0

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.
Files changed (103) hide show
  1. package/README.md +20 -1
  2. package/lib/bson.js.map +1 -1
  3. package/lib/client-side-encryption/auto_encrypter.js +8 -61
  4. package/lib/client-side-encryption/auto_encrypter.js.map +1 -1
  5. package/lib/client-side-encryption/client_encryption.js +5 -5
  6. package/lib/client-side-encryption/client_encryption.js.map +1 -1
  7. package/lib/client-side-encryption/providers/index.js.map +1 -1
  8. package/lib/client-side-encryption/state_machine.js +15 -11
  9. package/lib/client-side-encryption/state_machine.js.map +1 -1
  10. package/lib/cmap/connection.js +22 -20
  11. package/lib/cmap/connection.js.map +1 -1
  12. package/lib/cmap/wire_protocol/on_demand/document.js +8 -5
  13. package/lib/cmap/wire_protocol/on_demand/document.js.map +1 -1
  14. package/lib/cmap/wire_protocol/responses.js +116 -40
  15. package/lib/cmap/wire_protocol/responses.js.map +1 -1
  16. package/lib/collection.js +13 -2
  17. package/lib/collection.js.map +1 -1
  18. package/lib/constants.js +9 -1
  19. package/lib/constants.js.map +1 -1
  20. package/lib/cursor/abstract_cursor.js +231 -285
  21. package/lib/cursor/abstract_cursor.js.map +1 -1
  22. package/lib/cursor/aggregation_cursor.js +11 -19
  23. package/lib/cursor/aggregation_cursor.js.map +1 -1
  24. package/lib/cursor/change_stream_cursor.js +12 -14
  25. package/lib/cursor/change_stream_cursor.js.map +1 -1
  26. package/lib/cursor/find_cursor.js +64 -84
  27. package/lib/cursor/find_cursor.js.map +1 -1
  28. package/lib/cursor/list_collections_cursor.js +0 -1
  29. package/lib/cursor/list_collections_cursor.js.map +1 -1
  30. package/lib/cursor/list_indexes_cursor.js +0 -1
  31. package/lib/cursor/list_indexes_cursor.js.map +1 -1
  32. package/lib/cursor/run_command_cursor.js +4 -6
  33. package/lib/cursor/run_command_cursor.js.map +1 -1
  34. package/lib/error.js +10 -23
  35. package/lib/error.js.map +1 -1
  36. package/lib/index.js.map +1 -1
  37. package/lib/operations/aggregate.js +2 -2
  38. package/lib/operations/aggregate.js.map +1 -1
  39. package/lib/operations/bulk_write.js +1 -2
  40. package/lib/operations/bulk_write.js.map +1 -1
  41. package/lib/operations/command.js +2 -3
  42. package/lib/operations/command.js.map +1 -1
  43. package/lib/operations/execute_operation.js.map +1 -1
  44. package/lib/operations/find.js +2 -1
  45. package/lib/operations/find.js.map +1 -1
  46. package/lib/operations/get_more.js +1 -1
  47. package/lib/operations/get_more.js.map +1 -1
  48. package/lib/operations/indexes.js +2 -1
  49. package/lib/operations/indexes.js.map +1 -1
  50. package/lib/operations/list_collections.js +2 -1
  51. package/lib/operations/list_collections.js.map +1 -1
  52. package/lib/operations/run_command.js +1 -1
  53. package/lib/operations/run_command.js.map +1 -1
  54. package/lib/operations/update.js +2 -1
  55. package/lib/operations/update.js.map +1 -1
  56. package/lib/sdam/server.js +7 -2
  57. package/lib/sdam/server.js.map +1 -1
  58. package/lib/sessions.js +1 -1
  59. package/lib/sessions.js.map +1 -1
  60. package/lib/utils.js +45 -1
  61. package/lib/utils.js.map +1 -1
  62. package/lib/write_concern.js +17 -1
  63. package/lib/write_concern.js.map +1 -1
  64. package/mongodb.d.ts +187 -150
  65. package/package.json +2 -2
  66. package/src/bson.ts +1 -0
  67. package/src/client-side-encryption/auto_encrypter.ts +9 -70
  68. package/src/client-side-encryption/client_encryption.ts +33 -19
  69. package/src/client-side-encryption/providers/index.ts +118 -92
  70. package/src/client-side-encryption/state_machine.ts +22 -18
  71. package/src/cmap/connection.ts +46 -50
  72. package/src/cmap/wire_protocol/on_demand/document.ts +13 -6
  73. package/src/cmap/wire_protocol/responses.ts +140 -45
  74. package/src/collection.ts +25 -5
  75. package/src/constants.ts +9 -0
  76. package/src/cursor/abstract_cursor.ts +280 -373
  77. package/src/cursor/aggregation_cursor.ts +24 -33
  78. package/src/cursor/change_stream_cursor.ts +31 -48
  79. package/src/cursor/find_cursor.ts +77 -92
  80. package/src/cursor/list_collections_cursor.ts +3 -4
  81. package/src/cursor/list_indexes_cursor.ts +3 -4
  82. package/src/cursor/run_command_cursor.ts +13 -19
  83. package/src/error.ts +20 -30
  84. package/src/index.ts +19 -10
  85. package/src/operations/aggregate.ts +12 -5
  86. package/src/operations/bulk_write.ts +1 -2
  87. package/src/operations/command.ts +17 -3
  88. package/src/operations/delete.ts +2 -2
  89. package/src/operations/execute_operation.ts +0 -13
  90. package/src/operations/find.ts +7 -3
  91. package/src/operations/find_and_modify.ts +1 -1
  92. package/src/operations/get_more.ts +6 -10
  93. package/src/operations/indexes.ts +7 -3
  94. package/src/operations/list_collections.ts +8 -3
  95. package/src/operations/run_command.ts +16 -6
  96. package/src/operations/update.ts +2 -1
  97. package/src/sdam/server.ts +7 -2
  98. package/src/sessions.ts +1 -1
  99. package/src/utils.ts +52 -2
  100. package/src/write_concern.ts +18 -0
  101. package/lib/operations/count_documents.js +0 -31
  102. package/lib/operations/count_documents.js.map +0 -1
  103. package/src/operations/count_documents.ts +0 -46
@@ -1,52 +1,43 @@
1
1
  import { Readable, Transform } from 'stream';
2
2
 
3
3
  import { type BSONSerializeOptions, type Document, Long, pluckBSONSerializeOptions } from '../bson';
4
- import { CursorResponse } from '../cmap/wire_protocol/responses';
4
+ import { type 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';
15
13
  import type { MongoClient } from '../mongo_client';
16
- import { type TODO_NODE_3286, TypedEventEmitter } from '../mongo_types';
17
- import { executeOperation, type ExecutionResult } from '../operations/execute_operation';
14
+ import { TypedEventEmitter } from '../mongo_types';
15
+ import { executeOperation } from '../operations/execute_operation';
18
16
  import { GetMoreOperation } from '../operations/get_more';
19
17
  import { KillCursorsOperation } from '../operations/kill_cursors';
20
18
  import { ReadConcern, type ReadConcernLike } from '../read_concern';
21
19
  import { ReadPreference, type ReadPreferenceLike } from '../read_preference';
22
20
  import type { Server } from '../sdam/server';
23
21
  import { ClientSession, maybeClearPinnedConnection } from '../sessions';
24
- import { List, type MongoDBNamespace, ns, squashError } from '../utils';
22
+ import { type MongoDBNamespace, 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');
24
+ /**
25
+ * @internal
26
+ * TODO(NODE-2882): A cursor's getMore commands must be run on the same server it was started on
27
+ * and the same session must be used for the lifetime of the cursor. This object serves to get the
28
+ * server and session (along with the response) out of executeOperation back to the AbstractCursor.
29
+ *
30
+ * There may be a better design for communicating these values back to the cursor, currently an operation
31
+ * MUST store the selected server on itself so it can be read after executeOperation has returned.
32
+ */
33
+ export interface InitialCursorResponse {
34
+ /** The server selected for the operation */
35
+ server: Server;
36
+ /** The session used for this operation, may be implicitly created */
37
+ session?: ClientSession;
38
+ /** The raw server response for the operation */
39
+ response: CursorResponse;
40
+ }
50
41
 
51
42
  /** @public */
52
43
  export const CURSOR_FLAGS = [
@@ -137,39 +128,33 @@ export abstract class AbstractCursor<
137
128
  CursorEvents extends AbstractCursorEvents = AbstractCursorEvents
138
129
  > extends TypedEventEmitter<CursorEvents> {
139
130
  /** @internal */
140
- [kId]: Long | null;
131
+ private cursorId: Long | null;
141
132
  /** @internal */
142
- [kSession]: ClientSession;
133
+ private cursorSession: ClientSession;
143
134
  /** @internal */
144
- [kServer]?: Server;
135
+ private selectedServer?: Server;
145
136
  /** @internal */
146
- [kNamespace]: MongoDBNamespace;
137
+ private cursorNamespace: MongoDBNamespace;
147
138
  /** @internal */
148
- [kDocuments]: {
149
- length: number;
150
- shift(bsonOptions?: any): TSchema | null;
151
- clear(): void;
152
- pushMany(many: Iterable<TSchema>): void;
153
- push(item: TSchema): void;
154
- };
139
+ private documents: CursorResponse | null = null;
155
140
  /** @internal */
156
- [kClient]: MongoClient;
141
+ private cursorClient: MongoClient;
157
142
  /** @internal */
158
- [kTransform]?: (doc: TSchema) => any;
143
+ private transform?: (doc: TSchema) => any;
159
144
  /** @internal */
160
- [kInitialized]: boolean;
145
+ private initialized: boolean;
161
146
  /** @internal */
162
- [kClosed]: boolean;
147
+ private isClosed: boolean;
163
148
  /** @internal */
164
- [kKilled]: boolean;
149
+ private isKilled: boolean;
165
150
  /** @internal */
166
- [kOptions]: InternalAbstractCursorOptions;
151
+ protected readonly cursorOptions: InternalAbstractCursorOptions;
167
152
 
168
153
  /** @event */
169
154
  static readonly CLOSE = 'close' as const;
170
155
 
171
156
  /** @internal */
172
- constructor(
157
+ protected constructor(
173
158
  client: MongoClient,
174
159
  namespace: MongoDBNamespace,
175
160
  options: AbstractCursorOptions = {}
@@ -179,121 +164,132 @@ export abstract class AbstractCursor<
179
164
  if (!client.s.isMongoClient) {
180
165
  throw new MongoRuntimeError('Cursor must be constructed with MongoClient');
181
166
  }
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] = {
167
+ this.cursorClient = client;
168
+ this.cursorNamespace = namespace;
169
+ this.cursorId = null;
170
+ this.initialized = false;
171
+ this.isClosed = false;
172
+ this.isKilled = false;
173
+ this.cursorOptions = {
190
174
  readPreference:
191
175
  options.readPreference && options.readPreference instanceof ReadPreference
192
176
  ? options.readPreference
193
177
  : ReadPreference.primary,
194
178
  ...pluckBSONSerializeOptions(options)
195
179
  };
196
- this[kOptions].timeoutMS = options.timeoutMS;
180
+ this.cursorOptions.timeoutMS = options.timeoutMS;
197
181
 
198
182
  const readConcern = ReadConcern.fromOptions(options);
199
183
  if (readConcern) {
200
- this[kOptions].readConcern = readConcern;
184
+ this.cursorOptions.readConcern = readConcern;
201
185
  }
202
186
 
203
187
  if (typeof options.batchSize === 'number') {
204
- this[kOptions].batchSize = options.batchSize;
188
+ this.cursorOptions.batchSize = options.batchSize;
205
189
  }
206
190
 
207
191
  // we check for undefined specifically here to allow falsy values
208
192
  // eslint-disable-next-line no-restricted-syntax
209
193
  if (options.comment !== undefined) {
210
- this[kOptions].comment = options.comment;
194
+ this.cursorOptions.comment = options.comment;
211
195
  }
212
196
 
213
197
  if (typeof options.maxTimeMS === 'number') {
214
- this[kOptions].maxTimeMS = options.maxTimeMS;
198
+ this.cursorOptions.maxTimeMS = options.maxTimeMS;
215
199
  }
216
200
 
217
201
  if (typeof options.maxAwaitTimeMS === 'number') {
218
- this[kOptions].maxAwaitTimeMS = options.maxAwaitTimeMS;
202
+ this.cursorOptions.maxAwaitTimeMS = options.maxAwaitTimeMS;
219
203
  }
220
204
 
221
205
  if (options.session instanceof ClientSession) {
222
- this[kSession] = options.session;
206
+ this.cursorSession = options.session;
223
207
  } else {
224
- this[kSession] = this[kClient].startSession({ owner: this, explicit: false });
208
+ this.cursorSession = this.cursorClient.startSession({ owner: this, explicit: false });
225
209
  }
226
210
  }
227
211
 
212
+ /**
213
+ * The cursor has no id until it receives a response from the initial cursor creating command.
214
+ *
215
+ * It is non-zero for as long as the database has an open cursor.
216
+ *
217
+ * The initiating command may receive a zero id if the entire result is in the `firstBatch`.
218
+ */
228
219
  get id(): Long | undefined {
229
- return this[kId] ?? undefined;
220
+ return this.cursorId ?? undefined;
230
221
  }
231
222
 
232
223
  /** @internal */
233
224
  get isDead() {
234
- return (this[kId]?.isZero() ?? false) || this[kClosed] || this[kKilled];
225
+ return (this.cursorId?.isZero() ?? false) || this.isClosed || this.isKilled;
235
226
  }
236
227
 
237
228
  /** @internal */
238
229
  get client(): MongoClient {
239
- return this[kClient];
230
+ return this.cursorClient;
240
231
  }
241
232
 
242
233
  /** @internal */
243
234
  get server(): Server | undefined {
244
- return this[kServer];
235
+ return this.selectedServer;
245
236
  }
246
237
 
247
238
  get namespace(): MongoDBNamespace {
248
- return this[kNamespace];
239
+ return this.cursorNamespace;
249
240
  }
250
241
 
251
242
  get readPreference(): ReadPreference {
252
- return this[kOptions].readPreference;
243
+ return this.cursorOptions.readPreference;
253
244
  }
254
245
 
255
246
  get readConcern(): ReadConcern | undefined {
256
- return this[kOptions].readConcern;
247
+ return this.cursorOptions.readConcern;
257
248
  }
258
249
 
259
250
  /** @internal */
260
251
  get session(): ClientSession {
261
- return this[kSession];
252
+ return this.cursorSession;
262
253
  }
263
254
 
264
255
  set session(clientSession: ClientSession) {
265
- this[kSession] = clientSession;
266
- }
267
-
268
- /** @internal */
269
- get cursorOptions(): InternalAbstractCursorOptions {
270
- return this[kOptions];
256
+ this.cursorSession = clientSession;
271
257
  }
272
258
 
259
+ /**
260
+ * The cursor is closed and all remaining locally buffered documents have been iterated.
261
+ */
273
262
  get closed(): boolean {
274
- return this[kClosed];
263
+ return this.isClosed && (this.documents?.length ?? 0) === 0;
275
264
  }
276
265
 
266
+ /**
267
+ * A `killCursors` command was attempted on this cursor.
268
+ * This is performed if the cursor id is non zero.
269
+ */
277
270
  get killed(): boolean {
278
- return this[kKilled];
271
+ return this.isKilled;
279
272
  }
280
273
 
281
274
  get loadBalanced(): boolean {
282
- return !!this[kClient].topology?.loadBalanced;
275
+ return !!this.cursorClient.topology?.loadBalanced;
283
276
  }
284
277
 
285
278
  /** Returns current buffered documents length */
286
279
  bufferedCount(): number {
287
- return this[kDocuments].length;
280
+ return this.documents?.length ?? 0;
288
281
  }
289
282
 
290
283
  /** Returns current buffered documents */
291
284
  readBufferedDocuments(number?: number): TSchema[] {
292
285
  const bufferedDocs: TSchema[] = [];
293
- const documentsToRead = Math.min(number ?? this[kDocuments].length, this[kDocuments].length);
286
+ const documentsToRead = Math.min(
287
+ number ?? this.documents?.length ?? 0,
288
+ this.documents?.length ?? 0
289
+ );
294
290
 
295
291
  for (let count = 0; count < documentsToRead; count++) {
296
- const document = this[kDocuments].shift(this[kOptions]);
292
+ const document = this.documents?.shift(this.cursorOptions);
297
293
  if (document != null) {
298
294
  bufferedDocs.push(document);
299
295
  }
@@ -301,46 +297,38 @@ export abstract class AbstractCursor<
301
297
 
302
298
  return bufferedDocs;
303
299
  }
304
-
305
300
  async *[Symbol.asyncIterator](): AsyncGenerator<TSchema, void, void> {
306
- if (this.closed) {
301
+ if (this.isClosed) {
307
302
  return;
308
303
  }
309
304
 
310
305
  try {
311
306
  while (true) {
307
+ if (this.isKilled) {
308
+ return;
309
+ }
310
+
311
+ if (this.closed) {
312
+ return;
313
+ }
314
+
315
+ if (this.cursorId != null && this.isDead && (this.documents?.length ?? 0) === 0) {
316
+ return;
317
+ }
318
+
312
319
  const document = await this.next();
313
320
 
314
- // Intentional strict null check, because users can map cursors to falsey values.
315
- // We allow mapping to all values except for null.
316
321
  // eslint-disable-next-line no-restricted-syntax
317
322
  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;
323
+ return;
331
324
  }
332
325
 
333
326
  yield document;
334
-
335
- if (this[kId] === Long.ZERO) {
336
- // Cursor exhausted
337
- break;
338
- }
339
327
  }
340
328
  } finally {
341
329
  // Only close the cursor if it has not already been closed. This finally clause handles
342
330
  // the case when a user would break out of a for await of loop early.
343
- if (!this.closed) {
331
+ if (!this.isClosed) {
344
332
  try {
345
333
  await this.close();
346
334
  } catch (error) {
@@ -381,35 +369,61 @@ export abstract class AbstractCursor<
381
369
  }
382
370
 
383
371
  async hasNext(): Promise<boolean> {
384
- if (this[kId] === Long.ZERO) {
372
+ if (this.cursorId === Long.ZERO) {
385
373
  return false;
386
374
  }
387
375
 
388
- if (this[kDocuments].length !== 0) {
389
- return true;
390
- }
376
+ do {
377
+ if ((this.documents?.length ?? 0) !== 0) {
378
+ return true;
379
+ }
380
+ await this.fetchBatch();
381
+ } while (!this.isDead || (this.documents?.length ?? 0) !== 0);
391
382
 
392
- return await next(this, { blocking: true, transform: false, shift: false });
383
+ return false;
393
384
  }
394
385
 
395
386
  /** Get the next available document from the cursor, returns null if no more documents are available. */
396
387
  async next(): Promise<TSchema | null> {
397
- if (this[kId] === Long.ZERO) {
388
+ if (this.cursorId === Long.ZERO) {
398
389
  throw new MongoCursorExhaustedError();
399
390
  }
400
391
 
401
- return await next(this, { blocking: true, transform: true, shift: true });
392
+ do {
393
+ const doc = this.documents?.shift(this.cursorOptions);
394
+ if (doc != null) {
395
+ if (this.transform != null) return await this.transformDocument(doc);
396
+ return doc;
397
+ }
398
+ await this.fetchBatch();
399
+ } while (!this.isDead || (this.documents?.length ?? 0) !== 0);
400
+
401
+ return null;
402
402
  }
403
403
 
404
404
  /**
405
405
  * Try to get the next available document from the cursor or `null` if an empty batch is returned
406
406
  */
407
407
  async tryNext(): Promise<TSchema | null> {
408
- if (this[kId] === Long.ZERO) {
408
+ if (this.cursorId === Long.ZERO) {
409
409
  throw new MongoCursorExhaustedError();
410
410
  }
411
411
 
412
- return await next(this, { blocking: false, transform: true, shift: true });
412
+ let doc = this.documents?.shift(this.cursorOptions);
413
+ if (doc != null) {
414
+ if (this.transform != null) return await this.transformDocument(doc);
415
+ return doc;
416
+ }
417
+
418
+ await this.fetchBatch();
419
+
420
+ doc = this.documents?.shift(this.cursorOptions);
421
+ if (doc != null) {
422
+ if (this.transform != null) return await this.transformDocument(doc);
423
+ return doc;
424
+ }
425
+
426
+ return null;
413
427
  }
414
428
 
415
429
  /**
@@ -433,9 +447,7 @@ export abstract class AbstractCursor<
433
447
  }
434
448
 
435
449
  async close(): Promise<void> {
436
- const needsToEmitClosed = !this[kClosed];
437
- this[kClosed] = true;
438
- await cleanupCursor(this, { needsToEmitClosed });
450
+ await this.cleanup();
439
451
  }
440
452
 
441
453
  /**
@@ -459,7 +471,7 @@ export abstract class AbstractCursor<
459
471
  * @param value - The flag boolean value.
460
472
  */
461
473
  addCursorFlag(flag: CursorFlag, value: boolean): this {
462
- assertUninitialized(this);
474
+ this.throwIfInitialized();
463
475
  if (!CURSOR_FLAGS.includes(flag)) {
464
476
  throw new MongoInvalidArgumentError(`Flag ${flag} is not one of ${CURSOR_FLAGS}`);
465
477
  }
@@ -468,7 +480,7 @@ export abstract class AbstractCursor<
468
480
  throw new MongoInvalidArgumentError(`Flag ${flag} must be a boolean value`);
469
481
  }
470
482
 
471
- this[kOptions][flag] = value;
483
+ this.cursorOptions[flag] = value;
472
484
  return this;
473
485
  }
474
486
 
@@ -515,14 +527,14 @@ export abstract class AbstractCursor<
515
527
  * @param transform - The mapping transformation method.
516
528
  */
517
529
  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
530
+ this.throwIfInitialized();
531
+ const oldTransform = this.transform;
520
532
  if (oldTransform) {
521
- this[kTransform] = doc => {
533
+ this.transform = doc => {
522
534
  return transform(oldTransform(doc));
523
535
  };
524
536
  } else {
525
- this[kTransform] = transform;
537
+ this.transform = transform;
526
538
  }
527
539
 
528
540
  return this as unknown as AbstractCursor<T>;
@@ -534,11 +546,11 @@ export abstract class AbstractCursor<
534
546
  * @param readPreference - The new read preference for the cursor.
535
547
  */
536
548
  withReadPreference(readPreference: ReadPreferenceLike): this {
537
- assertUninitialized(this);
549
+ this.throwIfInitialized();
538
550
  if (readPreference instanceof ReadPreference) {
539
- this[kOptions].readPreference = readPreference;
551
+ this.cursorOptions.readPreference = readPreference;
540
552
  } else if (typeof readPreference === 'string') {
541
- this[kOptions].readPreference = ReadPreference.fromString(readPreference);
553
+ this.cursorOptions.readPreference = ReadPreference.fromString(readPreference);
542
554
  } else {
543
555
  throw new MongoInvalidArgumentError(`Invalid read preference: ${readPreference}`);
544
556
  }
@@ -552,10 +564,10 @@ export abstract class AbstractCursor<
552
564
  * @param readPreference - The new read preference for the cursor.
553
565
  */
554
566
  withReadConcern(readConcern: ReadConcernLike): this {
555
- assertUninitialized(this);
567
+ this.throwIfInitialized();
556
568
  const resolvedReadConcern = ReadConcern.fromOptions({ readConcern });
557
569
  if (resolvedReadConcern) {
558
- this[kOptions].readConcern = resolvedReadConcern;
570
+ this.cursorOptions.readConcern = resolvedReadConcern;
559
571
  }
560
572
 
561
573
  return this;
@@ -567,12 +579,12 @@ export abstract class AbstractCursor<
567
579
  * @param value - Number of milliseconds to wait before aborting the query.
568
580
  */
569
581
  maxTimeMS(value: number): this {
570
- assertUninitialized(this);
582
+ this.throwIfInitialized();
571
583
  if (typeof value !== 'number') {
572
584
  throw new MongoInvalidArgumentError('Argument for maxTimeMS must be a number');
573
585
  }
574
586
 
575
- this[kOptions].maxTimeMS = value;
587
+ this.cursorOptions.maxTimeMS = value;
576
588
  return this;
577
589
  }
578
590
 
@@ -582,8 +594,8 @@ export abstract class AbstractCursor<
582
594
  * @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
595
  */
584
596
  batchSize(value: number): this {
585
- assertUninitialized(this);
586
- if (this[kOptions].tailable) {
597
+ this.throwIfInitialized();
598
+ if (this.cursorOptions.tailable) {
587
599
  throw new MongoTailableCursorError('Tailable cursor does not support batchSize');
588
600
  }
589
601
 
@@ -591,7 +603,7 @@ export abstract class AbstractCursor<
591
603
  throw new MongoInvalidArgumentError('Operation "batchSize" requires an integer');
592
604
  }
593
605
 
594
- this[kOptions].batchSize = value;
606
+ this.cursorOptions.batchSize = value;
595
607
  return this;
596
608
  }
597
609
 
@@ -601,17 +613,17 @@ export abstract class AbstractCursor<
601
613
  * if the resultant data has already been retrieved by this cursor.
602
614
  */
603
615
  rewind(): void {
604
- if (!this[kInitialized]) {
616
+ if (!this.initialized) {
605
617
  return;
606
618
  }
607
619
 
608
- this[kId] = null;
609
- this[kDocuments].clear();
610
- this[kClosed] = false;
611
- this[kKilled] = false;
612
- this[kInitialized] = false;
620
+ this.cursorId = null;
621
+ this.documents?.clear();
622
+ this.isClosed = false;
623
+ this.isKilled = false;
624
+ this.initialized = false;
613
625
 
614
- const session = this[kSession];
626
+ const session = this.cursorSession;
615
627
  if (session) {
616
628
  // We only want to end this session if we created it, and it hasn't ended yet
617
629
  if (session.explicit === false) {
@@ -619,7 +631,7 @@ export abstract class AbstractCursor<
619
631
  // eslint-disable-next-line github/no-then
620
632
  session.endSession().then(undefined, squashError);
621
633
  }
622
- this[kSession] = this.client.startSession({ owner: this, explicit: false });
634
+ this.cursorSession = this.cursorClient.startSession({ owner: this, explicit: false });
623
635
  }
624
636
  }
625
637
  }
@@ -630,19 +642,34 @@ export abstract class AbstractCursor<
630
642
  abstract clone(): AbstractCursor<TSchema>;
631
643
 
632
644
  /** @internal */
633
- protected abstract _initialize(session: ClientSession | undefined): Promise<ExecutionResult>;
645
+ protected abstract _initialize(
646
+ session: ClientSession | undefined
647
+ ): Promise<InitialCursorResponse>;
634
648
 
635
649
  /** @internal */
636
- 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
- });
650
+ async getMore(batchSize: number): Promise<CursorResponse> {
651
+ if (this.cursorId == null) {
652
+ throw new MongoRuntimeError(
653
+ 'Unexpected null cursor id. A cursor creating command should have set this'
654
+ );
655
+ }
656
+ if (this.selectedServer == null) {
657
+ throw new MongoRuntimeError(
658
+ 'Unexpected null selectedServer. A cursor creating command should have set this'
659
+ );
660
+ }
661
+ const getMoreOperation = new GetMoreOperation(
662
+ this.cursorNamespace,
663
+ this.cursorId,
664
+ this.selectedServer,
665
+ {
666
+ ...this.cursorOptions,
667
+ session: this.cursorSession,
668
+ batchSize
669
+ }
670
+ );
644
671
 
645
- return await executeOperation(this[kClient], getMoreOperation);
672
+ return await executeOperation(this.cursorClient, getMoreOperation);
646
673
  }
647
674
 
648
675
  /**
@@ -652,169 +679,60 @@ export abstract class AbstractCursor<
652
679
  * operation. We cannot refactor to use the abstract _initialize method without
653
680
  * a significant refactor.
654
681
  */
655
- async [kInit](): Promise<void> {
682
+ private async cursorInit(): Promise<void> {
656
683
  try {
657
- const state = await this._initialize(this[kSession]);
684
+ const state = await this._initialize(this.cursorSession);
658
685
  const response = state.response;
659
- this[kServer] = state.server;
660
- if (CursorResponse.is(response)) {
661
- this[kId] = response.id;
662
- if (response.ns) this[kNamespace] = response.ns;
663
- this[kDocuments] = response;
664
- } else if (response.cursor) {
665
- // 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);
678
- }
679
-
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;
685
- // TODO(NODE-3286): ExecutionResult needs to accept a generic parameter
686
- this[kDocuments].push(state.response as TODO_NODE_3286);
687
- }
688
-
689
- // the cursor is now initialized, even if it is dead
690
- this[kInitialized] = true;
686
+ this.selectedServer = state.server;
687
+ this.cursorId = response.id;
688
+ this.cursorNamespace = response.ns ?? this.namespace;
689
+ this.documents = response;
690
+ this.initialized = true; // the cursor is now initialized, even if it is dead
691
691
  } catch (error) {
692
692
  // the cursor is now initialized, even if an error occurred
693
- this[kInitialized] = true;
694
- await cleanupCursor(this, { error });
693
+ this.initialized = true;
694
+ await this.cleanup(error);
695
695
  throw error;
696
696
  }
697
697
 
698
698
  if (this.isDead) {
699
- await cleanupCursor(this, undefined);
699
+ await this.cleanup();
700
700
  }
701
701
 
702
702
  return;
703
703
  }
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
-
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
704
 
784
- return doc;
705
+ /** @internal Attempt to obtain more documents */
706
+ private async fetchBatch(): Promise<void> {
707
+ if (this.isClosed) {
708
+ return;
785
709
  }
786
710
 
787
- if (cursor.isDead) {
711
+ if (this.isDead) {
788
712
  // if the cursor is dead, we clean it up
789
713
  // cleanupCursor should never throw, but if it does it indicates a bug in the driver
790
714
  // and we should surface the error
791
- await cleanupCursor(cursor, {});
792
- if (!shift) return false;
793
- return null;
715
+ await this.cleanup();
716
+ return;
717
+ }
718
+
719
+ if (this.cursorId == null) {
720
+ await this.cursorInit();
721
+ // If the cursor died or returned documents, return
722
+ if ((this.documents?.length ?? 0) !== 0 || this.isDead) return;
723
+ // Otherwise, run a getMore
794
724
  }
795
725
 
796
726
  // otherwise need to call getMore
797
- const batchSize = cursor[kOptions].batchSize || 1000;
727
+ const batchSize = this.cursorOptions.batchSize || 1000;
798
728
 
799
729
  try {
800
- const response = await cursor.getMore(batchSize);
801
- 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;
814
- }
730
+ const response = await this.getMore(batchSize);
731
+ this.cursorId = response.id;
732
+ this.documents = response;
815
733
  } catch (error) {
816
734
  try {
817
- await cleanupCursor(cursor, { error, needsToEmitClosed: true });
735
+ await this.cleanup(error);
818
736
  } catch (error) {
819
737
  // `cleanupCursor` should never throw, squash and throw the original error
820
738
  squashError(error);
@@ -822,7 +740,7 @@ async function next<T>(
822
740
  throw error;
823
741
  }
824
742
 
825
- if (cursor.isDead) {
743
+ if (this.isDead) {
826
744
  // If we successfully received a response from a cursor BUT the cursor indicates that it is exhausted,
827
745
  // we intentionally clean up the cursor to release its session back into the pool before the cursor
828
746
  // is iterated. This prevents a cursor that is exhausted on the server from holding
@@ -830,103 +748,87 @@ async function next<T>(
830
748
  //
831
749
  // cleanupCursorAsync should never throw, but if it does it indicates a bug in the driver
832
750
  // 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();
751
+ await this.cleanup();
864
752
  }
865
753
  }
866
754
 
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) {
755
+ /** @internal */
756
+ private async cleanup(error?: Error) {
757
+ this.isClosed = true;
758
+ const session = this.cursorSession;
759
+ try {
760
+ if (
761
+ !this.isKilled &&
762
+ this.cursorId &&
763
+ !this.cursorId.isZero() &&
764
+ this.cursorNamespace &&
765
+ this.selectedServer &&
766
+ !session.hasEnded
767
+ ) {
768
+ this.isKilled = true;
769
+ const cursorId = this.cursorId;
770
+ this.cursorId = Long.ZERO;
771
+ await executeOperation(
772
+ this.cursorClient,
773
+ new KillCursorsOperation(cursorId, this.cursorNamespace, this.selectedServer, {
774
+ session
775
+ })
776
+ );
777
+ }
778
+ } catch (error) {
779
+ squashError(error);
780
+ } finally {
781
+ if (session?.owner === this) {
876
782
  await session.endSession({ error });
877
- return;
878
783
  }
879
-
880
- if (!session.inTransaction()) {
784
+ if (!session?.inTransaction()) {
881
785
  maybeClearPinnedConnection(session, { error });
882
786
  }
883
- }
884
787
 
885
- return;
788
+ this.emitClose();
789
+ }
886
790
  }
887
791
 
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 });
792
+ /** @internal */
793
+ private hasEmittedClose = false;
794
+ /** @internal */
795
+ private emitClose() {
796
+ try {
797
+ if (!this.hasEmittedClose && ((this.documents?.length ?? 0) === 0 || this.isClosed)) {
798
+ // @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.
799
+ this.emit('close');
901
800
  }
801
+ } finally {
802
+ this.hasEmittedClose = true;
902
803
  }
903
-
904
- cursor.emit(AbstractCursor.CLOSE);
905
- return;
906
804
  }
907
805
 
908
- cursor[kKilled] = true;
909
-
910
- if (session.hasEnded) {
911
- return await completeCleanup();
912
- }
806
+ /** @internal */
807
+ private async transformDocument(document: NonNullable<TSchema>): Promise<TSchema> {
808
+ if (this.transform == null) return document;
913
809
 
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();
810
+ try {
811
+ const transformedDocument = this.transform(document);
812
+ // eslint-disable-next-line no-restricted-syntax
813
+ if (transformedDocument === null) {
814
+ const TRANSFORM_TO_NULL_ERROR =
815
+ 'Cursor returned a `null` document, but the cursor is not exhausted. Mapping documents to `null` is not supported in the cursor transform.';
816
+ throw new MongoAPIError(TRANSFORM_TO_NULL_ERROR);
817
+ }
818
+ return transformedDocument;
819
+ } catch (transformError) {
820
+ try {
821
+ await this.close();
822
+ } catch (closeError) {
823
+ squashError(closeError);
824
+ }
825
+ throw transformError;
826
+ }
923
827
  }
924
- }
925
828
 
926
- /** @internal */
927
- export function assertUninitialized(cursor: AbstractCursor): void {
928
- if (cursor[kInitialized]) {
929
- throw new MongoCursorInUseError();
829
+ /** @internal */
830
+ protected throwIfInitialized() {
831
+ if (this.initialized) throw new MongoCursorInUseError();
930
832
  }
931
833
  }
932
834
 
@@ -960,8 +862,13 @@ class ReadableCursorStream extends Readable {
960
862
  }
961
863
 
962
864
  private _readNext() {
865
+ if (this._cursor.id === Long.ZERO) {
866
+ this.push(null);
867
+ return;
868
+ }
869
+
963
870
  // eslint-disable-next-line github/no-then
964
- next(this._cursor, { blocking: true, transform: true, shift: true }).then(
871
+ this._cursor.next().then(
965
872
  result => {
966
873
  if (result == null) {
967
874
  this.push(null);