mongodb 6.7.0-dev.20240530.sha.f56938f → 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.
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.assertUninitialized = exports.AbstractCursor = exports.CURSOR_FLAGS = void 0;
3
+ exports.AbstractCursor = exports.CURSOR_FLAGS = void 0;
4
4
  const stream_1 = require("stream");
5
5
  const bson_1 = require("../bson");
6
6
  const responses_1 = require("../cmap/wire_protocol/responses");
@@ -13,30 +13,6 @@ const read_concern_1 = require("../read_concern");
13
13
  const read_preference_1 = require("../read_preference");
14
14
  const sessions_1 = require("../sessions");
15
15
  const utils_1 = require("../utils");
16
- /** @internal */
17
- const kId = Symbol('id');
18
- /** @internal */
19
- const kDocuments = Symbol('documents');
20
- /** @internal */
21
- const kServer = Symbol('server');
22
- /** @internal */
23
- const kNamespace = Symbol('namespace');
24
- /** @internal */
25
- const kClient = Symbol('client');
26
- /** @internal */
27
- const kSession = Symbol('session');
28
- /** @internal */
29
- const kOptions = Symbol('options');
30
- /** @internal */
31
- const kTransform = Symbol('transform');
32
- /** @internal */
33
- const kInitialized = Symbol('initialized');
34
- /** @internal */
35
- const kClosed = Symbol('closed');
36
- /** @internal */
37
- const kKilled = Symbol('killed');
38
- /** @internal */
39
- const kInit = Symbol('kInit');
40
16
  /** @public */
41
17
  exports.CURSOR_FLAGS = [
42
18
  'tailable',
@@ -51,102 +27,100 @@ class AbstractCursor extends mongo_types_1.TypedEventEmitter {
51
27
  /** @internal */
52
28
  constructor(client, namespace, options = {}) {
53
29
  super();
30
+ /** @internal */
31
+ this.hasEmittedClose = false;
54
32
  if (!client.s.isMongoClient) {
55
33
  throw new error_1.MongoRuntimeError('Cursor must be constructed with MongoClient');
56
34
  }
57
- this[kClient] = client;
58
- this[kNamespace] = namespace;
59
- this[kId] = null;
60
- this[kDocuments] = new utils_1.List();
61
- this[kInitialized] = false;
62
- this[kClosed] = false;
63
- this[kKilled] = false;
64
- this[kOptions] = {
35
+ this.cursorClient = client;
36
+ this.cursorNamespace = namespace;
37
+ this.cursorId = null;
38
+ this.documents = new utils_1.List();
39
+ this.initialized = false;
40
+ this.isClosed = false;
41
+ this.isKilled = false;
42
+ this.cursorOptions = {
65
43
  readPreference: options.readPreference && options.readPreference instanceof read_preference_1.ReadPreference
66
44
  ? options.readPreference
67
45
  : read_preference_1.ReadPreference.primary,
68
46
  ...(0, bson_1.pluckBSONSerializeOptions)(options)
69
47
  };
70
- this[kOptions].timeoutMS = options.timeoutMS;
48
+ this.cursorOptions.timeoutMS = options.timeoutMS;
71
49
  const readConcern = read_concern_1.ReadConcern.fromOptions(options);
72
50
  if (readConcern) {
73
- this[kOptions].readConcern = readConcern;
51
+ this.cursorOptions.readConcern = readConcern;
74
52
  }
75
53
  if (typeof options.batchSize === 'number') {
76
- this[kOptions].batchSize = options.batchSize;
54
+ this.cursorOptions.batchSize = options.batchSize;
77
55
  }
78
56
  // we check for undefined specifically here to allow falsy values
79
57
  // eslint-disable-next-line no-restricted-syntax
80
58
  if (options.comment !== undefined) {
81
- this[kOptions].comment = options.comment;
59
+ this.cursorOptions.comment = options.comment;
82
60
  }
83
61
  if (typeof options.maxTimeMS === 'number') {
84
- this[kOptions].maxTimeMS = options.maxTimeMS;
62
+ this.cursorOptions.maxTimeMS = options.maxTimeMS;
85
63
  }
86
64
  if (typeof options.maxAwaitTimeMS === 'number') {
87
- this[kOptions].maxAwaitTimeMS = options.maxAwaitTimeMS;
65
+ this.cursorOptions.maxAwaitTimeMS = options.maxAwaitTimeMS;
88
66
  }
89
67
  if (options.session instanceof sessions_1.ClientSession) {
90
- this[kSession] = options.session;
68
+ this.cursorSession = options.session;
91
69
  }
92
70
  else {
93
- this[kSession] = this[kClient].startSession({ owner: this, explicit: false });
71
+ this.cursorSession = this.cursorClient.startSession({ owner: this, explicit: false });
94
72
  }
95
73
  }
96
74
  get id() {
97
- return this[kId] ?? undefined;
75
+ return this.cursorId ?? undefined;
98
76
  }
99
77
  /** @internal */
100
78
  get isDead() {
101
- return (this[kId]?.isZero() ?? false) || this[kClosed] || this[kKilled];
79
+ return (this.cursorId?.isZero() ?? false) || this.isClosed || this.isKilled;
102
80
  }
103
81
  /** @internal */
104
82
  get client() {
105
- return this[kClient];
83
+ return this.cursorClient;
106
84
  }
107
85
  /** @internal */
108
86
  get server() {
109
- return this[kServer];
87
+ return this.selectedServer;
110
88
  }
111
89
  get namespace() {
112
- return this[kNamespace];
90
+ return this.cursorNamespace;
113
91
  }
114
92
  get readPreference() {
115
- return this[kOptions].readPreference;
93
+ return this.cursorOptions.readPreference;
116
94
  }
117
95
  get readConcern() {
118
- return this[kOptions].readConcern;
96
+ return this.cursorOptions.readConcern;
119
97
  }
120
98
  /** @internal */
121
99
  get session() {
122
- return this[kSession];
100
+ return this.cursorSession;
123
101
  }
124
102
  set session(clientSession) {
125
- this[kSession] = clientSession;
126
- }
127
- /** @internal */
128
- get cursorOptions() {
129
- return this[kOptions];
103
+ this.cursorSession = clientSession;
130
104
  }
131
105
  get closed() {
132
- return this[kClosed];
106
+ return this.isClosed;
133
107
  }
134
108
  get killed() {
135
- return this[kKilled];
109
+ return this.isKilled;
136
110
  }
137
111
  get loadBalanced() {
138
- return !!this[kClient].topology?.loadBalanced;
112
+ return !!this.cursorClient.topology?.loadBalanced;
139
113
  }
140
114
  /** Returns current buffered documents length */
141
115
  bufferedCount() {
142
- return this[kDocuments].length;
116
+ return this.documents.length;
143
117
  }
144
118
  /** Returns current buffered documents */
145
119
  readBufferedDocuments(number) {
146
120
  const bufferedDocs = [];
147
- const documentsToRead = Math.min(number ?? this[kDocuments].length, this[kDocuments].length);
121
+ const documentsToRead = Math.min(number ?? this.documents.length, this.documents.length);
148
122
  for (let count = 0; count < documentsToRead; count++) {
149
- const document = this[kDocuments].shift(this[kOptions]);
123
+ const document = this.documents.shift(this.cursorOptions);
150
124
  if (document != null) {
151
125
  bufferedDocs.push(document);
152
126
  }
@@ -154,39 +128,32 @@ class AbstractCursor extends mongo_types_1.TypedEventEmitter {
154
128
  return bufferedDocs;
155
129
  }
156
130
  async *[Symbol.asyncIterator]() {
157
- if (this.closed) {
131
+ if (this.isClosed) {
158
132
  return;
159
133
  }
160
134
  try {
161
135
  while (true) {
136
+ if (this.isKilled) {
137
+ return;
138
+ }
139
+ if (this.isClosed && this.documents.length === 0) {
140
+ return;
141
+ }
142
+ if (this.cursorId != null && this.isDead && this.documents.length === 0) {
143
+ return;
144
+ }
162
145
  const document = await this.next();
163
- // Intentional strict null check, because users can map cursors to falsey values.
164
- // We allow mapping to all values except for null.
165
146
  // eslint-disable-next-line no-restricted-syntax
166
147
  if (document === null) {
167
- if (!this.closed) {
168
- const message = 'Cursor returned a `null` document, but the cursor is not exhausted. Mapping documents to `null` is not supported in the cursor transform.';
169
- try {
170
- await cleanupCursor(this, { needsToEmitClosed: true });
171
- }
172
- catch (error) {
173
- (0, utils_1.squashError)(error);
174
- }
175
- throw new error_1.MongoAPIError(message);
176
- }
177
- break;
148
+ return;
178
149
  }
179
150
  yield document;
180
- if (this[kId] === bson_1.Long.ZERO) {
181
- // Cursor exhausted
182
- break;
183
- }
184
151
  }
185
152
  }
186
153
  finally {
187
154
  // Only close the cursor if it has not already been closed. This finally clause handles
188
155
  // the case when a user would break out of a for await of loop early.
189
- if (!this.closed) {
156
+ if (!this.isClosed) {
190
157
  try {
191
158
  await this.close();
192
159
  }
@@ -221,29 +188,54 @@ class AbstractCursor extends mongo_types_1.TypedEventEmitter {
221
188
  return new ReadableCursorStream(this);
222
189
  }
223
190
  async hasNext() {
224
- if (this[kId] === bson_1.Long.ZERO) {
191
+ if (this.cursorId === bson_1.Long.ZERO) {
225
192
  return false;
226
193
  }
227
- if (this[kDocuments].length !== 0) {
228
- return true;
229
- }
230
- return await next(this, { blocking: true, transform: false, shift: false });
194
+ do {
195
+ if (this.documents.length !== 0) {
196
+ return true;
197
+ }
198
+ await this.fetchBatch();
199
+ } while (!this.isDead || this.documents.length !== 0);
200
+ return false;
231
201
  }
232
202
  /** Get the next available document from the cursor, returns null if no more documents are available. */
233
203
  async next() {
234
- if (this[kId] === bson_1.Long.ZERO) {
204
+ if (this.cursorId === bson_1.Long.ZERO) {
235
205
  throw new error_1.MongoCursorExhaustedError();
236
206
  }
237
- return await next(this, { blocking: true, transform: true, shift: true });
207
+ do {
208
+ const doc = this.documents.shift();
209
+ if (doc != null) {
210
+ if (this.transform != null)
211
+ return await this.transformDocument(doc);
212
+ return doc;
213
+ }
214
+ await this.fetchBatch();
215
+ } while (!this.isDead || this.documents.length !== 0);
216
+ return null;
238
217
  }
239
218
  /**
240
219
  * Try to get the next available document from the cursor or `null` if an empty batch is returned
241
220
  */
242
221
  async tryNext() {
243
- if (this[kId] === bson_1.Long.ZERO) {
222
+ if (this.cursorId === bson_1.Long.ZERO) {
244
223
  throw new error_1.MongoCursorExhaustedError();
245
224
  }
246
- return await next(this, { blocking: false, transform: true, shift: true });
225
+ let doc = this.documents.shift();
226
+ if (doc != null) {
227
+ if (this.transform != null)
228
+ return await this.transformDocument(doc);
229
+ return doc;
230
+ }
231
+ await this.fetchBatch();
232
+ doc = this.documents.shift();
233
+ if (doc != null) {
234
+ if (this.transform != null)
235
+ return await this.transformDocument(doc);
236
+ return doc;
237
+ }
238
+ return null;
247
239
  }
248
240
  /**
249
241
  * Iterates over all the documents for this cursor using the iterator, callback pattern.
@@ -265,9 +257,7 @@ class AbstractCursor extends mongo_types_1.TypedEventEmitter {
265
257
  }
266
258
  }
267
259
  async close() {
268
- const needsToEmitClosed = !this[kClosed];
269
- this[kClosed] = true;
270
- await cleanupCursor(this, { needsToEmitClosed });
260
+ await this.cleanup();
271
261
  }
272
262
  /**
273
263
  * Returns an array of documents. The caller is responsible for making sure that there
@@ -289,14 +279,14 @@ class AbstractCursor extends mongo_types_1.TypedEventEmitter {
289
279
  * @param value - The flag boolean value.
290
280
  */
291
281
  addCursorFlag(flag, value) {
292
- assertUninitialized(this);
282
+ this.throwIfInitialized();
293
283
  if (!exports.CURSOR_FLAGS.includes(flag)) {
294
284
  throw new error_1.MongoInvalidArgumentError(`Flag ${flag} is not one of ${exports.CURSOR_FLAGS}`);
295
285
  }
296
286
  if (typeof value !== 'boolean') {
297
287
  throw new error_1.MongoInvalidArgumentError(`Flag ${flag} must be a boolean value`);
298
288
  }
299
- this[kOptions][flag] = value;
289
+ this.cursorOptions[flag] = value;
300
290
  return this;
301
291
  }
302
292
  /**
@@ -342,15 +332,15 @@ class AbstractCursor extends mongo_types_1.TypedEventEmitter {
342
332
  * @param transform - The mapping transformation method.
343
333
  */
344
334
  map(transform) {
345
- assertUninitialized(this);
346
- const oldTransform = this[kTransform]; // TODO(NODE-3283): Improve transform typing
335
+ this.throwIfInitialized();
336
+ const oldTransform = this.transform;
347
337
  if (oldTransform) {
348
- this[kTransform] = doc => {
338
+ this.transform = doc => {
349
339
  return transform(oldTransform(doc));
350
340
  };
351
341
  }
352
342
  else {
353
- this[kTransform] = transform;
343
+ this.transform = transform;
354
344
  }
355
345
  return this;
356
346
  }
@@ -360,12 +350,12 @@ class AbstractCursor extends mongo_types_1.TypedEventEmitter {
360
350
  * @param readPreference - The new read preference for the cursor.
361
351
  */
362
352
  withReadPreference(readPreference) {
363
- assertUninitialized(this);
353
+ this.throwIfInitialized();
364
354
  if (readPreference instanceof read_preference_1.ReadPreference) {
365
- this[kOptions].readPreference = readPreference;
355
+ this.cursorOptions.readPreference = readPreference;
366
356
  }
367
357
  else if (typeof readPreference === 'string') {
368
- this[kOptions].readPreference = read_preference_1.ReadPreference.fromString(readPreference);
358
+ this.cursorOptions.readPreference = read_preference_1.ReadPreference.fromString(readPreference);
369
359
  }
370
360
  else {
371
361
  throw new error_1.MongoInvalidArgumentError(`Invalid read preference: ${readPreference}`);
@@ -378,10 +368,10 @@ class AbstractCursor extends mongo_types_1.TypedEventEmitter {
378
368
  * @param readPreference - The new read preference for the cursor.
379
369
  */
380
370
  withReadConcern(readConcern) {
381
- assertUninitialized(this);
371
+ this.throwIfInitialized();
382
372
  const resolvedReadConcern = read_concern_1.ReadConcern.fromOptions({ readConcern });
383
373
  if (resolvedReadConcern) {
384
- this[kOptions].readConcern = resolvedReadConcern;
374
+ this.cursorOptions.readConcern = resolvedReadConcern;
385
375
  }
386
376
  return this;
387
377
  }
@@ -391,11 +381,11 @@ class AbstractCursor extends mongo_types_1.TypedEventEmitter {
391
381
  * @param value - Number of milliseconds to wait before aborting the query.
392
382
  */
393
383
  maxTimeMS(value) {
394
- assertUninitialized(this);
384
+ this.throwIfInitialized();
395
385
  if (typeof value !== 'number') {
396
386
  throw new error_1.MongoInvalidArgumentError('Argument for maxTimeMS must be a number');
397
387
  }
398
- this[kOptions].maxTimeMS = value;
388
+ this.cursorOptions.maxTimeMS = value;
399
389
  return this;
400
390
  }
401
391
  /**
@@ -404,14 +394,14 @@ class AbstractCursor extends mongo_types_1.TypedEventEmitter {
404
394
  * @param value - The number of documents to return per batch. See {@link https://www.mongodb.com/docs/manual/reference/command/find/|find command documentation}.
405
395
  */
406
396
  batchSize(value) {
407
- assertUninitialized(this);
408
- if (this[kOptions].tailable) {
397
+ this.throwIfInitialized();
398
+ if (this.cursorOptions.tailable) {
409
399
  throw new error_1.MongoTailableCursorError('Tailable cursor does not support batchSize');
410
400
  }
411
401
  if (typeof value !== 'number') {
412
402
  throw new error_1.MongoInvalidArgumentError('Operation "batchSize" requires an integer');
413
403
  }
414
- this[kOptions].batchSize = value;
404
+ this.cursorOptions.batchSize = value;
415
405
  return this;
416
406
  }
417
407
  /**
@@ -420,15 +410,15 @@ class AbstractCursor extends mongo_types_1.TypedEventEmitter {
420
410
  * if the resultant data has already been retrieved by this cursor.
421
411
  */
422
412
  rewind() {
423
- if (!this[kInitialized]) {
413
+ if (!this.initialized) {
424
414
  return;
425
415
  }
426
- this[kId] = null;
427
- this[kDocuments].clear();
428
- this[kClosed] = false;
429
- this[kKilled] = false;
430
- this[kInitialized] = false;
431
- const session = this[kSession];
416
+ this.cursorId = null;
417
+ this.documents.clear();
418
+ this.isClosed = false;
419
+ this.isKilled = false;
420
+ this.initialized = false;
421
+ const session = this.cursorSession;
432
422
  if (session) {
433
423
  // We only want to end this session if we created it, and it hasn't ended yet
434
424
  if (session.explicit === false) {
@@ -436,20 +426,25 @@ class AbstractCursor extends mongo_types_1.TypedEventEmitter {
436
426
  // eslint-disable-next-line github/no-then
437
427
  session.endSession().then(undefined, utils_1.squashError);
438
428
  }
439
- this[kSession] = this.client.startSession({ owner: this, explicit: false });
429
+ this.cursorSession = this.cursorClient.startSession({ owner: this, explicit: false });
440
430
  }
441
431
  }
442
432
  }
443
433
  /** @internal */
444
434
  async getMore(batchSize, useCursorResponse = false) {
445
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
446
- const getMoreOperation = new get_more_1.GetMoreOperation(this[kNamespace], this[kId], this[kServer], {
447
- ...this[kOptions],
448
- session: this[kSession],
435
+ if (this.cursorId == null) {
436
+ throw new error_1.MongoRuntimeError('Unexpected null cursor id. A cursor creating command should have set this');
437
+ }
438
+ if (this.selectedServer == null) {
439
+ throw new error_1.MongoRuntimeError('Unexpected null selectedServer. A cursor creating command should have set this');
440
+ }
441
+ const getMoreOperation = new get_more_1.GetMoreOperation(this.cursorNamespace, this.cursorId, this.selectedServer, {
442
+ ...this.cursorOptions,
443
+ session: this.cursorSession,
449
444
  batchSize,
450
445
  useCursorResponse
451
446
  });
452
- return await (0, execute_operation_1.executeOperation)(this[kClient], getMoreOperation);
447
+ return await (0, execute_operation_1.executeOperation)(this.cursorClient, getMoreOperation);
453
448
  }
454
449
  /**
455
450
  * @internal
@@ -458,118 +453,83 @@ class AbstractCursor extends mongo_types_1.TypedEventEmitter {
458
453
  * operation. We cannot refactor to use the abstract _initialize method without
459
454
  * a significant refactor.
460
455
  */
461
- async [kInit]() {
456
+ async cursorInit() {
462
457
  try {
463
- const state = await this._initialize(this[kSession]);
458
+ const state = await this._initialize(this.cursorSession);
464
459
  const response = state.response;
465
- this[kServer] = state.server;
460
+ this.selectedServer = state.server;
466
461
  if (responses_1.CursorResponse.is(response)) {
467
- this[kId] = response.id;
462
+ this.cursorId = response.id;
468
463
  if (response.ns)
469
- this[kNamespace] = response.ns;
470
- this[kDocuments] = response;
464
+ this.cursorNamespace = response.ns;
465
+ this.documents = response;
471
466
  }
472
467
  else if (response.cursor) {
473
468
  // TODO(NODE-2674): Preserve int64 sent from MongoDB
474
- this[kId] =
475
- typeof response.cursor.id === 'number'
476
- ? bson_1.Long.fromNumber(response.cursor.id)
477
- : typeof response.cursor.id === 'bigint'
478
- ? bson_1.Long.fromBigInt(response.cursor.id)
479
- : response.cursor.id;
480
- if (response.cursor.ns) {
481
- this[kNamespace] = (0, utils_1.ns)(response.cursor.ns);
482
- }
483
- this[kDocuments].pushMany(response.cursor.firstBatch);
469
+ this.cursorId = getCursorId(response);
470
+ if (response.cursor.ns)
471
+ this.cursorNamespace = (0, utils_1.ns)(response.cursor.ns);
472
+ this.documents.pushMany(response.cursor.firstBatch);
484
473
  }
485
- // When server responses return without a cursor document, we close this cursor
486
- // and return the raw server response. This is often the case for explain commands
487
- // for example
488
- if (this[kId] == null) {
489
- this[kId] = bson_1.Long.ZERO;
474
+ if (this.cursorId == null) {
475
+ // When server responses return without a cursor document, we close this cursor
476
+ // and return the raw server response. This is the case for explain commands
477
+ this.cursorId = bson_1.Long.ZERO;
490
478
  // TODO(NODE-3286): ExecutionResult needs to accept a generic parameter
491
- this[kDocuments].push(state.response);
479
+ this.documents.push(state.response);
492
480
  }
493
481
  // the cursor is now initialized, even if it is dead
494
- this[kInitialized] = true;
482
+ this.initialized = true;
495
483
  }
496
484
  catch (error) {
497
485
  // the cursor is now initialized, even if an error occurred
498
- this[kInitialized] = true;
499
- await cleanupCursor(this, { error });
486
+ this.initialized = true;
487
+ await this.cleanup(error);
500
488
  throw error;
501
489
  }
502
490
  if (this.isDead) {
503
- await cleanupCursor(this, undefined);
491
+ await this.cleanup();
504
492
  }
505
493
  return;
506
494
  }
507
- }
508
- /** @event */
509
- AbstractCursor.CLOSE = 'close';
510
- exports.AbstractCursor = AbstractCursor;
511
- async function next(cursor, { blocking, transform, shift }) {
512
- if (cursor.closed) {
513
- if (!shift)
514
- return false;
515
- return null;
516
- }
517
- do {
518
- if (cursor[kId] == null) {
519
- // All cursors must operate within a session, one must be made implicitly if not explicitly provided
520
- await cursor[kInit]();
521
- }
522
- if (cursor[kDocuments].length !== 0) {
523
- if (!shift)
524
- return true;
525
- const doc = cursor[kDocuments].shift(cursor[kOptions]);
526
- if (doc != null && transform && cursor[kTransform]) {
527
- try {
528
- return cursor[kTransform](doc);
529
- }
530
- catch (error) {
531
- try {
532
- await cleanupCursor(cursor, { error, needsToEmitClosed: true });
533
- }
534
- catch (error) {
535
- // `cleanupCursor` should never throw, squash and throw the original error
536
- (0, utils_1.squashError)(error);
537
- }
538
- throw error;
539
- }
540
- }
541
- return doc;
495
+ /** @internal Attempt to obtain more documents */
496
+ async fetchBatch() {
497
+ if (this.isClosed) {
498
+ return;
542
499
  }
543
- if (cursor.isDead) {
500
+ if (this.isDead) {
544
501
  // if the cursor is dead, we clean it up
545
502
  // cleanupCursor should never throw, but if it does it indicates a bug in the driver
546
503
  // and we should surface the error
547
- await cleanupCursor(cursor, {});
548
- if (!shift)
549
- return false;
550
- return null;
504
+ await this.cleanup();
505
+ return;
506
+ }
507
+ if (this.cursorId == null) {
508
+ await this.cursorInit();
509
+ // If the cursor died or returned documents, return
510
+ if (this.documents.length !== 0 || this.isDead)
511
+ return;
512
+ // Otherwise, run a getMore
551
513
  }
552
514
  // otherwise need to call getMore
553
- const batchSize = cursor[kOptions].batchSize || 1000;
515
+ const batchSize = this.cursorOptions.batchSize || 1000;
554
516
  try {
555
- const response = await cursor.getMore(batchSize);
517
+ const response = await this.getMore(batchSize);
518
+ // CursorResponse is disabled in this PR
519
+ // however the special `emptyGetMore` can be returned from find cursors
556
520
  if (responses_1.CursorResponse.is(response)) {
557
- cursor[kId] = response.id;
558
- cursor[kDocuments] = response;
521
+ this.cursorId = response.id;
522
+ this.documents = response;
559
523
  }
560
- else if (response) {
561
- const cursorId = typeof response.cursor.id === 'number'
562
- ? bson_1.Long.fromNumber(response.cursor.id)
563
- : typeof response.cursor.id === 'bigint'
564
- ? bson_1.Long.fromBigInt(response.cursor.id)
565
- : response.cursor.id;
566
- cursor[kDocuments].pushMany(response.cursor.nextBatch);
567
- cursor[kId] = cursorId;
524
+ else if (response?.cursor) {
525
+ const cursorId = getCursorId(response);
526
+ this.documents.pushMany(response.cursor.nextBatch);
527
+ this.cursorId = cursorId;
568
528
  }
569
529
  }
570
530
  catch (error) {
571
531
  try {
572
- await cleanupCursor(cursor, { error, needsToEmitClosed: true });
532
+ await this.cleanup(error);
573
533
  }
574
534
  catch (error) {
575
535
  // `cleanupCursor` should never throw, squash and throw the original error
@@ -577,7 +537,7 @@ async function next(cursor, { blocking, transform, shift }) {
577
537
  }
578
538
  throw error;
579
539
  }
580
- if (cursor.isDead) {
540
+ if (this.isDead) {
581
541
  // If we successfully received a response from a cursor BUT the cursor indicates that it is exhausted,
582
542
  // we intentionally clean up the cursor to release its session back into the pool before the cursor
583
543
  // is iterated. This prevents a cursor that is exhausted on the server from holding
@@ -585,89 +545,91 @@ async function next(cursor, { blocking, transform, shift }) {
585
545
  //
586
546
  // cleanupCursorAsync should never throw, but if it does it indicates a bug in the driver
587
547
  // and we should surface the error
588
- await cleanupCursor(cursor, {});
548
+ await this.cleanup();
589
549
  }
590
- if (cursor[kDocuments].length === 0 && blocking === false) {
591
- if (!shift)
592
- return false;
593
- return null;
550
+ }
551
+ /** @internal */
552
+ async cleanup(error) {
553
+ this.isClosed = true;
554
+ const session = this.cursorSession;
555
+ try {
556
+ if (!this.isKilled &&
557
+ this.cursorId &&
558
+ !this.cursorId.isZero() &&
559
+ this.cursorNamespace &&
560
+ this.selectedServer &&
561
+ !session.hasEnded) {
562
+ this.isKilled = true;
563
+ await (0, execute_operation_1.executeOperation)(this.cursorClient, new kill_cursors_1.KillCursorsOperation(this.cursorId, this.cursorNamespace, this.selectedServer, {
564
+ session
565
+ }));
566
+ }
594
567
  }
595
- } while (!cursor.isDead || cursor[kDocuments].length !== 0);
596
- if (!shift)
597
- return false;
598
- return null;
599
- }
600
- async function cleanupCursor(cursor, options) {
601
- const cursorId = cursor[kId];
602
- const cursorNs = cursor[kNamespace];
603
- const server = cursor[kServer];
604
- const session = cursor[kSession];
605
- const error = options?.error;
606
- // Cursors only emit closed events once the client-side cursor has been exhausted fully or there
607
- // was an error. Notably, when the server returns a cursor id of 0 and a non-empty batch, we
608
- // cleanup the cursor but don't emit a `close` event.
609
- const needsToEmitClosed = options?.needsToEmitClosed ?? cursor[kDocuments].length === 0;
610
- if (error) {
611
- if (cursor.loadBalanced && error instanceof error_1.MongoNetworkError) {
612
- return await completeCleanup();
613
- }
614
- }
615
- if (cursorId == null || server == null || cursorId.isZero() || cursorNs == null) {
616
- if (needsToEmitClosed) {
617
- cursor[kClosed] = true;
618
- cursor[kId] = bson_1.Long.ZERO;
619
- cursor.emit(AbstractCursor.CLOSE);
568
+ catch (error) {
569
+ (0, utils_1.squashError)(error);
620
570
  }
621
- if (session) {
622
- if (session.owner === cursor) {
571
+ finally {
572
+ if (session?.owner === this) {
623
573
  await session.endSession({ error });
624
- return;
625
574
  }
626
- if (!session.inTransaction()) {
575
+ if (!session?.inTransaction()) {
627
576
  (0, sessions_1.maybeClearPinnedConnection)(session, { error });
628
577
  }
578
+ this.emitClose();
629
579
  }
630
- return;
631
580
  }
632
- async function completeCleanup() {
633
- if (session) {
634
- if (session.owner === cursor) {
635
- try {
636
- await session.endSession({ error });
637
- }
638
- finally {
639
- cursor.emit(AbstractCursor.CLOSE);
640
- }
641
- return;
642
- }
643
- if (!session.inTransaction()) {
644
- (0, sessions_1.maybeClearPinnedConnection)(session, { error });
581
+ /** @internal */
582
+ emitClose() {
583
+ try {
584
+ if (!this.hasEmittedClose && (this.documents.length === 0 || this.isClosed)) {
585
+ // @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.
586
+ this.emit('close');
645
587
  }
646
588
  }
647
- cursor.emit(AbstractCursor.CLOSE);
648
- return;
649
- }
650
- cursor[kKilled] = true;
651
- if (session.hasEnded) {
652
- return await completeCleanup();
653
- }
654
- try {
655
- await (0, execute_operation_1.executeOperation)(cursor[kClient], new kill_cursors_1.KillCursorsOperation(cursorId, cursorNs, server, { session }));
589
+ finally {
590
+ this.hasEmittedClose = true;
591
+ }
656
592
  }
657
- catch (error) {
658
- (0, utils_1.squashError)(error);
593
+ /** @internal */
594
+ async transformDocument(document) {
595
+ if (this.transform == null)
596
+ return document;
597
+ try {
598
+ const transformedDocument = this.transform(document);
599
+ // eslint-disable-next-line no-restricted-syntax
600
+ if (transformedDocument === null) {
601
+ const TRANSFORM_TO_NULL_ERROR = 'Cursor returned a `null` document, but the cursor is not exhausted. Mapping documents to `null` is not supported in the cursor transform.';
602
+ throw new error_1.MongoAPIError(TRANSFORM_TO_NULL_ERROR);
603
+ }
604
+ return transformedDocument;
605
+ }
606
+ catch (transformError) {
607
+ try {
608
+ await this.close();
609
+ }
610
+ catch (closeError) {
611
+ (0, utils_1.squashError)(closeError);
612
+ }
613
+ throw transformError;
614
+ }
659
615
  }
660
- finally {
661
- await completeCleanup();
616
+ /** @internal */
617
+ throwIfInitialized() {
618
+ if (this.initialized)
619
+ throw new error_1.MongoCursorInUseError();
662
620
  }
663
621
  }
664
- /** @internal */
665
- function assertUninitialized(cursor) {
666
- if (cursor[kInitialized]) {
667
- throw new error_1.MongoCursorInUseError();
668
- }
622
+ /** @event */
623
+ AbstractCursor.CLOSE = 'close';
624
+ exports.AbstractCursor = AbstractCursor;
625
+ /** A temporary helper to box up the many possible type issue of cursor ids */
626
+ function getCursorId(response) {
627
+ return typeof response.cursor.id === 'number'
628
+ ? bson_1.Long.fromNumber(response.cursor.id)
629
+ : typeof response.cursor.id === 'bigint'
630
+ ? bson_1.Long.fromBigInt(response.cursor.id)
631
+ : response.cursor.id;
669
632
  }
670
- exports.assertUninitialized = assertUninitialized;
671
633
  class ReadableCursorStream extends stream_1.Readable {
672
634
  constructor(cursor) {
673
635
  super({
@@ -690,8 +652,12 @@ class ReadableCursorStream extends stream_1.Readable {
690
652
  this._cursor.close().then(() => callback(error), closeError => callback(closeError));
691
653
  }
692
654
  _readNext() {
655
+ if (this._cursor.id === bson_1.Long.ZERO) {
656
+ this.push(null);
657
+ return;
658
+ }
693
659
  // eslint-disable-next-line github/no-then
694
- next(this._cursor, { blocking: true, transform: true, shift: true }).then(result => {
660
+ this._cursor.next().then(result => {
695
661
  if (result == null) {
696
662
  this.push(null);
697
663
  }