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.
- package/README.md +19 -0
- package/lib/cursor/abstract_cursor.js +234 -268
- package/lib/cursor/abstract_cursor.js.map +1 -1
- package/lib/cursor/aggregation_cursor.js +10 -17
- package/lib/cursor/aggregation_cursor.js.map +1 -1
- package/lib/cursor/change_stream_cursor.js +6 -6
- package/lib/cursor/change_stream_cursor.js.map +1 -1
- package/lib/cursor/find_cursor.js +64 -72
- package/lib/cursor/find_cursor.js.map +1 -1
- package/lib/index.js.map +1 -1
- package/mongodb.d.ts +48 -64
- package/package.json +2 -2
- package/src/client-side-encryption/client_encryption.ts +27 -13
- package/src/cursor/abstract_cursor.ts +259 -348
- package/src/cursor/aggregation_cursor.ts +13 -23
- package/src/cursor/change_stream_cursor.ts +12 -15
- package/src/cursor/find_cursor.ts +67 -74
- package/src/index.ts +1 -0
|
@@ -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
|
-
|
|
113
|
+
private cursorId: Long | null;
|
|
141
114
|
/** @internal */
|
|
142
|
-
|
|
115
|
+
private cursorSession: ClientSession;
|
|
143
116
|
/** @internal */
|
|
144
|
-
|
|
117
|
+
private selectedServer?: Server;
|
|
145
118
|
/** @internal */
|
|
146
|
-
|
|
119
|
+
private cursorNamespace: MongoDBNamespace;
|
|
147
120
|
/** @internal */
|
|
148
|
-
|
|
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
|
-
|
|
129
|
+
private cursorClient: MongoClient;
|
|
157
130
|
/** @internal */
|
|
158
|
-
|
|
131
|
+
private transform?: (doc: TSchema) => any;
|
|
159
132
|
/** @internal */
|
|
160
|
-
|
|
133
|
+
private initialized: boolean;
|
|
161
134
|
/** @internal */
|
|
162
|
-
|
|
135
|
+
private isClosed: boolean;
|
|
163
136
|
/** @internal */
|
|
164
|
-
|
|
137
|
+
private isKilled: boolean;
|
|
165
138
|
/** @internal */
|
|
166
|
-
|
|
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
|
|
183
|
-
this
|
|
184
|
-
this
|
|
185
|
-
this
|
|
186
|
-
this
|
|
187
|
-
this
|
|
188
|
-
this
|
|
189
|
-
this
|
|
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
|
|
169
|
+
this.cursorOptions.timeoutMS = options.timeoutMS;
|
|
197
170
|
|
|
198
171
|
const readConcern = ReadConcern.fromOptions(options);
|
|
199
172
|
if (readConcern) {
|
|
200
|
-
this
|
|
173
|
+
this.cursorOptions.readConcern = readConcern;
|
|
201
174
|
}
|
|
202
175
|
|
|
203
176
|
if (typeof options.batchSize === 'number') {
|
|
204
|
-
this
|
|
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
|
|
183
|
+
this.cursorOptions.comment = options.comment;
|
|
211
184
|
}
|
|
212
185
|
|
|
213
186
|
if (typeof options.maxTimeMS === 'number') {
|
|
214
|
-
this
|
|
187
|
+
this.cursorOptions.maxTimeMS = options.maxTimeMS;
|
|
215
188
|
}
|
|
216
189
|
|
|
217
190
|
if (typeof options.maxAwaitTimeMS === 'number') {
|
|
218
|
-
this
|
|
191
|
+
this.cursorOptions.maxAwaitTimeMS = options.maxAwaitTimeMS;
|
|
219
192
|
}
|
|
220
193
|
|
|
221
194
|
if (options.session instanceof ClientSession) {
|
|
222
|
-
this
|
|
195
|
+
this.cursorSession = options.session;
|
|
223
196
|
} else {
|
|
224
|
-
this
|
|
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
|
|
202
|
+
return this.cursorId ?? undefined;
|
|
230
203
|
}
|
|
231
204
|
|
|
232
205
|
/** @internal */
|
|
233
206
|
get isDead() {
|
|
234
|
-
return (this
|
|
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
|
|
212
|
+
return this.cursorClient;
|
|
240
213
|
}
|
|
241
214
|
|
|
242
215
|
/** @internal */
|
|
243
216
|
get server(): Server | undefined {
|
|
244
|
-
return this
|
|
217
|
+
return this.selectedServer;
|
|
245
218
|
}
|
|
246
219
|
|
|
247
220
|
get namespace(): MongoDBNamespace {
|
|
248
|
-
return this
|
|
221
|
+
return this.cursorNamespace;
|
|
249
222
|
}
|
|
250
223
|
|
|
251
224
|
get readPreference(): ReadPreference {
|
|
252
|
-
return this
|
|
225
|
+
return this.cursorOptions.readPreference;
|
|
253
226
|
}
|
|
254
227
|
|
|
255
228
|
get readConcern(): ReadConcern | undefined {
|
|
256
|
-
return this
|
|
229
|
+
return this.cursorOptions.readConcern;
|
|
257
230
|
}
|
|
258
231
|
|
|
259
232
|
/** @internal */
|
|
260
233
|
get session(): ClientSession {
|
|
261
|
-
return this
|
|
234
|
+
return this.cursorSession;
|
|
262
235
|
}
|
|
263
236
|
|
|
264
237
|
set session(clientSession: ClientSession) {
|
|
265
|
-
this
|
|
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
|
|
242
|
+
return this.isClosed;
|
|
275
243
|
}
|
|
276
244
|
|
|
277
245
|
get killed(): boolean {
|
|
278
|
-
return this
|
|
246
|
+
return this.isKilled;
|
|
279
247
|
}
|
|
280
248
|
|
|
281
249
|
get loadBalanced(): boolean {
|
|
282
|
-
return !!this
|
|
250
|
+
return !!this.cursorClient.topology?.loadBalanced;
|
|
283
251
|
}
|
|
284
252
|
|
|
285
253
|
/** Returns current buffered documents length */
|
|
286
254
|
bufferedCount(): number {
|
|
287
|
-
return this
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
|
345
|
+
if (this.cursorId === Long.ZERO) {
|
|
385
346
|
return false;
|
|
386
347
|
}
|
|
387
348
|
|
|
388
|
-
|
|
389
|
-
|
|
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
|
|
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
|
|
361
|
+
if (this.cursorId === Long.ZERO) {
|
|
398
362
|
throw new MongoCursorExhaustedError();
|
|
399
363
|
}
|
|
400
364
|
|
|
401
|
-
|
|
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
|
|
381
|
+
if (this.cursorId === Long.ZERO) {
|
|
409
382
|
throw new MongoCursorExhaustedError();
|
|
410
383
|
}
|
|
411
384
|
|
|
412
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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[
|
|
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
|
-
|
|
519
|
-
const oldTransform = this
|
|
503
|
+
this.throwIfInitialized();
|
|
504
|
+
const oldTransform = this.transform;
|
|
520
505
|
if (oldTransform) {
|
|
521
|
-
this
|
|
506
|
+
this.transform = doc => {
|
|
522
507
|
return transform(oldTransform(doc));
|
|
523
508
|
};
|
|
524
509
|
} else {
|
|
525
|
-
this
|
|
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
|
-
|
|
522
|
+
this.throwIfInitialized();
|
|
538
523
|
if (readPreference instanceof ReadPreference) {
|
|
539
|
-
this
|
|
524
|
+
this.cursorOptions.readPreference = readPreference;
|
|
540
525
|
} else if (typeof readPreference === 'string') {
|
|
541
|
-
this
|
|
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
|
-
|
|
540
|
+
this.throwIfInitialized();
|
|
556
541
|
const resolvedReadConcern = ReadConcern.fromOptions({ readConcern });
|
|
557
542
|
if (resolvedReadConcern) {
|
|
558
|
-
this
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
586
|
-
if (this
|
|
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
|
|
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
|
|
589
|
+
if (!this.initialized) {
|
|
605
590
|
return;
|
|
606
591
|
}
|
|
607
592
|
|
|
608
|
-
this
|
|
609
|
-
this
|
|
610
|
-
this
|
|
611
|
-
this
|
|
612
|
-
this
|
|
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
|
|
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
|
|
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
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
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
|
|
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
|
|
654
|
+
private async cursorInit(): Promise<void> {
|
|
656
655
|
try {
|
|
657
|
-
const state = await this._initialize(this
|
|
656
|
+
const state = await this._initialize(this.cursorSession);
|
|
658
657
|
const response = state.response;
|
|
659
|
-
this
|
|
658
|
+
this.selectedServer = state.server;
|
|
660
659
|
if (CursorResponse.is(response)) {
|
|
661
|
-
this
|
|
662
|
-
if (response.ns) this
|
|
663
|
-
this
|
|
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
|
|
667
|
-
|
|
668
|
-
|
|
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
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
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
|
|
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
|
|
679
|
+
this.initialized = true;
|
|
691
680
|
} catch (error) {
|
|
692
681
|
// the cursor is now initialized, even if an error occurred
|
|
693
|
-
this
|
|
694
|
-
await
|
|
682
|
+
this.initialized = true;
|
|
683
|
+
await this.cleanup(error);
|
|
695
684
|
throw error;
|
|
696
685
|
}
|
|
697
686
|
|
|
698
687
|
if (this.isDead) {
|
|
699
|
-
await
|
|
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
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
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 (
|
|
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
|
|
792
|
-
|
|
793
|
-
|
|
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 =
|
|
716
|
+
const batchSize = this.cursorOptions.batchSize || 1000;
|
|
798
717
|
|
|
799
718
|
try {
|
|
800
|
-
const response = await
|
|
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
|
-
|
|
803
|
-
|
|
804
|
-
} else if (response) {
|
|
805
|
-
const cursorId =
|
|
806
|
-
|
|
807
|
-
|
|
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
|
|
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 (
|
|
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
|
|
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
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
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
|
-
|
|
783
|
+
this.emitClose();
|
|
784
|
+
}
|
|
886
785
|
}
|
|
887
786
|
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
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
|
-
|
|
801
|
+
/** @internal */
|
|
802
|
+
private async transformDocument(document: NonNullable<TSchema>): Promise<TSchema> {
|
|
803
|
+
if (this.transform == null) return document;
|
|
909
804
|
|
|
910
|
-
|
|
911
|
-
|
|
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
|
-
|
|
915
|
-
|
|
916
|
-
|
|
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
|
-
/**
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
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
|
-
|
|
875
|
+
this._cursor.next().then(
|
|
965
876
|
result => {
|
|
966
877
|
if (result == null) {
|
|
967
878
|
this.push(null);
|