mongodb 6.12.0-dev.20250125.sha.c1bcf0de → 6.12.0-dev.20250129.sha.907aac19

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 (59) hide show
  1. package/lib/beta.d.ts +55 -13
  2. package/lib/client-side-encryption/auto_encrypter.js +4 -2
  3. package/lib/client-side-encryption/auto_encrypter.js.map +1 -1
  4. package/lib/client-side-encryption/client_encryption.js +4 -4
  5. package/lib/client-side-encryption/client_encryption.js.map +1 -1
  6. package/lib/client-side-encryption/state_machine.js +56 -30
  7. package/lib/client-side-encryption/state_machine.js.map +1 -1
  8. package/lib/cmap/connection.js +28 -22
  9. package/lib/cmap/connection.js.map +1 -1
  10. package/lib/cmap/connection_pool.js +5 -0
  11. package/lib/cmap/connection_pool.js.map +1 -1
  12. package/lib/cmap/wire_protocol/on_data.js +6 -1
  13. package/lib/cmap/wire_protocol/on_data.js.map +1 -1
  14. package/lib/collection.js.map +1 -1
  15. package/lib/cursor/abstract_cursor.js +47 -18
  16. package/lib/cursor/abstract_cursor.js.map +1 -1
  17. package/lib/cursor/aggregation_cursor.js +2 -1
  18. package/lib/cursor/aggregation_cursor.js.map +1 -1
  19. package/lib/cursor/find_cursor.js +2 -1
  20. package/lib/cursor/find_cursor.js.map +1 -1
  21. package/lib/cursor/list_collections_cursor.js +2 -1
  22. package/lib/cursor/list_collections_cursor.js.map +1 -1
  23. package/lib/db.js +2 -1
  24. package/lib/db.js.map +1 -1
  25. package/lib/mongo_client.js +4 -0
  26. package/lib/mongo_client.js.map +1 -1
  27. package/lib/operations/execute_operation.js +7 -3
  28. package/lib/operations/execute_operation.js.map +1 -1
  29. package/lib/operations/list_collections.js.map +1 -1
  30. package/lib/operations/operation.js.map +1 -1
  31. package/lib/sdam/server.js +26 -16
  32. package/lib/sdam/server.js.map +1 -1
  33. package/lib/sdam/topology.js +5 -0
  34. package/lib/sdam/topology.js.map +1 -1
  35. package/lib/utils.js +64 -7
  36. package/lib/utils.js.map +1 -1
  37. package/mongodb.d.ts +55 -13
  38. package/package.json +1 -1
  39. package/src/client-side-encryption/auto_encrypter.ts +12 -8
  40. package/src/client-side-encryption/client_encryption.ts +6 -4
  41. package/src/client-side-encryption/state_machine.ts +80 -36
  42. package/src/cmap/connection.ts +37 -29
  43. package/src/cmap/connection_pool.ts +17 -3
  44. package/src/cmap/wire_protocol/on_data.ts +9 -2
  45. package/src/collection.ts +15 -8
  46. package/src/cursor/abstract_cursor.ts +71 -23
  47. package/src/cursor/aggregation_cursor.ts +5 -3
  48. package/src/cursor/find_cursor.ts +5 -3
  49. package/src/cursor/list_collections_cursor.ts +5 -3
  50. package/src/db.ts +11 -7
  51. package/src/index.ts +1 -0
  52. package/src/mongo_client.ts +13 -0
  53. package/src/mongo_types.ts +38 -0
  54. package/src/operations/execute_operation.ts +9 -4
  55. package/src/operations/list_collections.ts +4 -1
  56. package/src/operations/operation.ts +3 -2
  57. package/src/sdam/server.ts +31 -18
  58. package/src/sdam/topology.ts +10 -2
  59. package/src/utils.ts +79 -6
@@ -12,7 +12,7 @@ import {
12
12
  MongoTailableCursorError
13
13
  } from '../error';
14
14
  import type { MongoClient } from '../mongo_client';
15
- import { TypedEventEmitter } from '../mongo_types';
15
+ import { type Abortable, TypedEventEmitter } from '../mongo_types';
16
16
  import { executeOperation } from '../operations/execute_operation';
17
17
  import { GetMoreOperation } from '../operations/get_more';
18
18
  import { KillCursorsOperation } from '../operations/kill_cursors';
@@ -22,7 +22,13 @@ import { type AsyncDisposable, configureResourceManagement } from '../resource_m
22
22
  import type { Server } from '../sdam/server';
23
23
  import { ClientSession, maybeClearPinnedConnection } from '../sessions';
24
24
  import { type CSOTTimeoutContext, type Timeout, TimeoutContext } from '../timeout';
25
- import { type MongoDBNamespace, squashError } from '../utils';
25
+ import {
26
+ addAbortListener,
27
+ type Disposable,
28
+ kDispose,
29
+ type MongoDBNamespace,
30
+ squashError
31
+ } from '../utils';
26
32
 
27
33
  /**
28
34
  * @internal
@@ -61,6 +67,10 @@ export interface CursorStreamOptions {
61
67
  /** @public */
62
68
  export type CursorFlag = (typeof CURSOR_FLAGS)[number];
63
69
 
70
+ function removeActiveCursor(this: AbstractCursor) {
71
+ this.client.s.activeCursors.delete(this);
72
+ }
73
+
64
74
  /**
65
75
  * @public
66
76
  * @experimental
@@ -247,12 +257,14 @@ export abstract class AbstractCursor<
247
257
 
248
258
  /** @internal */
249
259
  protected deserializationOptions: OnDemandDocumentDeserializeOptions;
260
+ protected signal: AbortSignal | undefined;
261
+ private abortListener: Disposable | undefined;
250
262
 
251
263
  /** @internal */
252
264
  protected constructor(
253
265
  client: MongoClient,
254
266
  namespace: MongoDBNamespace,
255
- options: AbstractCursorOptions = {}
267
+ options: AbstractCursorOptions & Abortable = {}
256
268
  ) {
257
269
  super();
258
270
 
@@ -352,6 +364,12 @@ export abstract class AbstractCursor<
352
364
  };
353
365
 
354
366
  this.timeoutContext = options.timeoutContext;
367
+ this.signal = options.signal;
368
+ this.abortListener = addAbortListener(
369
+ this.signal,
370
+ () => void this.close().then(undefined, squashError)
371
+ );
372
+ this.trackCursor();
355
373
  }
356
374
 
357
375
  /**
@@ -431,6 +449,14 @@ export abstract class AbstractCursor<
431
449
  await this.close();
432
450
  }
433
451
 
452
+ /** Adds cursor to client's tracking so it will be closed by MongoClient.close() */
453
+ private trackCursor() {
454
+ this.cursorClient.s.activeCursors.add(this);
455
+ if (!this.listeners('close').includes(removeActiveCursor)) {
456
+ this.once('close', removeActiveCursor);
457
+ }
458
+ }
459
+
434
460
  /** Returns current buffered documents length */
435
461
  bufferedCount(): number {
436
462
  return this.documents?.length ?? 0;
@@ -455,6 +481,8 @@ export abstract class AbstractCursor<
455
481
  }
456
482
 
457
483
  async *[Symbol.asyncIterator](): AsyncGenerator<TSchema, void, void> {
484
+ this.signal?.throwIfAborted();
485
+
458
486
  if (this.closed) {
459
487
  return;
460
488
  }
@@ -481,6 +509,8 @@ export abstract class AbstractCursor<
481
509
  }
482
510
 
483
511
  yield document;
512
+
513
+ this.signal?.throwIfAborted();
484
514
  }
485
515
  } finally {
486
516
  // Only close the cursor if it has not already been closed. This finally clause handles
@@ -496,9 +526,16 @@ export abstract class AbstractCursor<
496
526
  }
497
527
 
498
528
  stream(options?: CursorStreamOptions): Readable & AsyncIterable<TSchema> {
529
+ const readable = new ReadableCursorStream(this);
530
+ const abortListener = addAbortListener(this.signal, function () {
531
+ readable.destroy(this.reason);
532
+ });
533
+ readable.once('end', () => {
534
+ abortListener?.[kDispose]();
535
+ });
536
+
499
537
  if (options?.transform) {
500
538
  const transform = options.transform;
501
- const readable = new ReadableCursorStream(this);
502
539
 
503
540
  const transformedStream = readable.pipe(
504
541
  new Transform({
@@ -522,10 +559,12 @@ export abstract class AbstractCursor<
522
559
  return transformedStream;
523
560
  }
524
561
 
525
- return new ReadableCursorStream(this);
562
+ return readable;
526
563
  }
527
564
 
528
565
  async hasNext(): Promise<boolean> {
566
+ this.signal?.throwIfAborted();
567
+
529
568
  if (this.cursorId === Long.ZERO) {
530
569
  return false;
531
570
  }
@@ -551,6 +590,8 @@ export abstract class AbstractCursor<
551
590
 
552
591
  /** Get the next available document from the cursor, returns null if no more documents are available. */
553
592
  async next(): Promise<TSchema | null> {
593
+ this.signal?.throwIfAborted();
594
+
554
595
  if (this.cursorId === Long.ZERO) {
555
596
  throw new MongoCursorExhaustedError();
556
597
  }
@@ -581,6 +622,8 @@ export abstract class AbstractCursor<
581
622
  * Try to get the next available document from the cursor or `null` if an empty batch is returned
582
623
  */
583
624
  async tryNext(): Promise<TSchema | null> {
625
+ this.signal?.throwIfAborted();
626
+
584
627
  if (this.cursorId === Long.ZERO) {
585
628
  throw new MongoCursorExhaustedError();
586
629
  }
@@ -620,6 +663,8 @@ export abstract class AbstractCursor<
620
663
  * @deprecated - Will be removed in a future release. Use for await...of instead.
621
664
  */
622
665
  async forEach(iterator: (doc: TSchema) => boolean | void): Promise<void> {
666
+ this.signal?.throwIfAborted();
667
+
623
668
  if (typeof iterator !== 'function') {
624
669
  throw new MongoInvalidArgumentError('Argument "iterator" must be a function');
625
670
  }
@@ -645,6 +690,8 @@ export abstract class AbstractCursor<
645
690
  * cursor.rewind() can be used to reset the cursor.
646
691
  */
647
692
  async toArray(): Promise<TSchema[]> {
693
+ this.signal?.throwIfAborted();
694
+
648
695
  const array: TSchema[] = [];
649
696
  // at the end of the loop (since readBufferedDocuments is called) the buffer will be empty
650
697
  // then, the 'await of' syntax will run a getMore call
@@ -824,16 +871,15 @@ export abstract class AbstractCursor<
824
871
  this.isClosed = false;
825
872
  this.isKilled = false;
826
873
  this.initialized = false;
874
+ this.hasEmittedClose = false;
875
+ this.trackCursor();
827
876
 
828
- const session = this.cursorSession;
829
- if (session) {
830
- // We only want to end this session if we created it, and it hasn't ended yet
831
- if (session.explicit === false) {
832
- if (!session.hasEnded) {
833
- session.endSession().then(undefined, squashError);
834
- }
835
- this.cursorSession = this.cursorClient.startSession({ owner: this, explicit: false });
877
+ // We only want to end this session if we created it, and it hasn't ended yet
878
+ if (this.cursorSession.explicit === false) {
879
+ if (!this.cursorSession.hasEnded) {
880
+ this.cursorSession.endSession().then(undefined, squashError);
836
881
  }
882
+ this.cursorSession = this.cursorClient.startSession({ owner: this, explicit: false });
837
883
  }
838
884
  }
839
885
 
@@ -968,8 +1014,8 @@ export abstract class AbstractCursor<
968
1014
 
969
1015
  /** @internal */
970
1016
  private async cleanup(timeoutMS?: number, error?: Error) {
1017
+ this.abortListener?.[kDispose]();
971
1018
  this.isClosed = true;
972
- const session = this.cursorSession;
973
1019
  const timeoutContextForKillCursors = (): CursorTimeoutContext | undefined => {
974
1020
  if (timeoutMS != null) {
975
1021
  this.timeoutContext?.clear();
@@ -991,7 +1037,7 @@ export abstract class AbstractCursor<
991
1037
  !this.cursorId.isZero() &&
992
1038
  this.cursorNamespace &&
993
1039
  this.selectedServer &&
994
- !session.hasEnded
1040
+ !this.cursorSession.hasEnded
995
1041
  ) {
996
1042
  this.isKilled = true;
997
1043
  const cursorId = this.cursorId;
@@ -1000,7 +1046,7 @@ export abstract class AbstractCursor<
1000
1046
  await executeOperation(
1001
1047
  this.cursorClient,
1002
1048
  new KillCursorsOperation(cursorId, this.cursorNamespace, this.selectedServer, {
1003
- session
1049
+ session: this.cursorSession
1004
1050
  }),
1005
1051
  timeoutContextForKillCursors()
1006
1052
  );
@@ -1008,14 +1054,16 @@ export abstract class AbstractCursor<
1008
1054
  } catch (error) {
1009
1055
  squashError(error);
1010
1056
  } finally {
1011
- if (session?.owner === this) {
1012
- await session.endSession({ error });
1013
- }
1014
- if (!session?.inTransaction()) {
1015
- maybeClearPinnedConnection(session, { error });
1057
+ try {
1058
+ if (this.cursorSession?.owner === this) {
1059
+ await this.cursorSession.endSession({ error });
1060
+ }
1061
+ if (!this.cursorSession?.inTransaction()) {
1062
+ maybeClearPinnedConnection(this.cursorSession, { error });
1063
+ }
1064
+ } finally {
1065
+ this.emitClose();
1016
1066
  }
1017
-
1018
- this.emitClose();
1019
1067
  }
1020
1068
  }
1021
1069
 
@@ -8,6 +8,7 @@ import {
8
8
  validateExplainTimeoutOptions
9
9
  } from '../explain';
10
10
  import type { MongoClient } from '../mongo_client';
11
+ import { type Abortable } from '../mongo_types';
11
12
  import { AggregateOperation, type AggregateOptions } from '../operations/aggregate';
12
13
  import { executeOperation } from '../operations/execute_operation';
13
14
  import type { ClientSession } from '../sessions';
@@ -32,14 +33,14 @@ export interface AggregationCursorOptions extends AbstractCursorOptions, Aggrega
32
33
  export class AggregationCursor<TSchema = any> extends ExplainableCursor<TSchema> {
33
34
  public readonly pipeline: Document[];
34
35
  /** @internal */
35
- private aggregateOptions: AggregateOptions;
36
+ private aggregateOptions: AggregateOptions & Abortable;
36
37
 
37
38
  /** @internal */
38
39
  constructor(
39
40
  client: MongoClient,
40
41
  namespace: MongoDBNamespace,
41
42
  pipeline: Document[] = [],
42
- options: AggregateOptions = {}
43
+ options: AggregateOptions & Abortable = {}
43
44
  ) {
44
45
  super(client, namespace, options);
45
46
 
@@ -73,7 +74,8 @@ export class AggregationCursor<TSchema = any> extends ExplainableCursor<TSchema>
73
74
  const options = {
74
75
  ...this.aggregateOptions,
75
76
  ...this.cursorOptions,
76
- session
77
+ session,
78
+ signal: this.signal
77
79
  };
78
80
  if (options.explain) {
79
81
  try {
@@ -9,6 +9,7 @@ import {
9
9
  validateExplainTimeoutOptions
10
10
  } from '../explain';
11
11
  import type { MongoClient } from '../mongo_client';
12
+ import { type Abortable } from '../mongo_types';
12
13
  import type { CollationOptions } from '../operations/command';
13
14
  import { CountOperation, type CountOptions } from '../operations/count';
14
15
  import { executeOperation } from '../operations/execute_operation';
@@ -36,14 +37,14 @@ export class FindCursor<TSchema = any> extends ExplainableCursor<TSchema> {
36
37
  /** @internal */
37
38
  private numReturned = 0;
38
39
  /** @internal */
39
- private readonly findOptions: FindOptions;
40
+ private readonly findOptions: FindOptions & Abortable;
40
41
 
41
42
  /** @internal */
42
43
  constructor(
43
44
  client: MongoClient,
44
45
  namespace: MongoDBNamespace,
45
46
  filter: Document = {},
46
- options: FindOptions = {}
47
+ options: FindOptions & Abortable = {}
47
48
  ) {
48
49
  super(client, namespace, options);
49
50
 
@@ -72,7 +73,8 @@ export class FindCursor<TSchema = any> extends ExplainableCursor<TSchema> {
72
73
  const options = {
73
74
  ...this.findOptions, // NOTE: order matters here, we may need to refine this
74
75
  ...this.cursorOptions,
75
- session
76
+ session,
77
+ signal: this.signal
76
78
  };
77
79
 
78
80
  if (options.explain) {
@@ -1,5 +1,6 @@
1
1
  import type { Document } from '../bson';
2
2
  import type { Db } from '../db';
3
+ import { type Abortable } from '../mongo_types';
3
4
  import { executeOperation } from '../operations/execute_operation';
4
5
  import {
5
6
  type CollectionInfo,
@@ -17,9 +18,9 @@ export class ListCollectionsCursor<
17
18
  > extends AbstractCursor<T> {
18
19
  parent: Db;
19
20
  filter: Document;
20
- options?: ListCollectionsOptions;
21
+ options?: ListCollectionsOptions & Abortable;
21
22
 
22
- constructor(db: Db, filter: Document, options?: ListCollectionsOptions) {
23
+ constructor(db: Db, filter: Document, options?: ListCollectionsOptions & Abortable) {
23
24
  super(db.client, db.s.namespace, options);
24
25
  this.parent = db;
25
26
  this.filter = filter;
@@ -38,7 +39,8 @@ export class ListCollectionsCursor<
38
39
  const operation = new ListCollectionsOperation(this.parent, this.filter, {
39
40
  ...this.cursorOptions,
40
41
  ...this.options,
41
- session
42
+ session,
43
+ signal: this.signal
42
44
  });
43
45
 
44
46
  const response = await executeOperation(this.parent.client, operation, this.timeoutContext);
package/src/db.ts CHANGED
@@ -8,7 +8,7 @@ import { ListCollectionsCursor } from './cursor/list_collections_cursor';
8
8
  import { RunCommandCursor, type RunCursorCommandOptions } from './cursor/run_command_cursor';
9
9
  import { MongoInvalidArgumentError } from './error';
10
10
  import type { MongoClient, PkFactory } from './mongo_client';
11
- import type { TODO_NODE_3286 } from './mongo_types';
11
+ import type { Abortable, TODO_NODE_3286 } from './mongo_types';
12
12
  import type { AggregateOptions } from './operations/aggregate';
13
13
  import { CollectionsOperation } from './operations/collections';
14
14
  import {
@@ -273,7 +273,7 @@ export class Db {
273
273
  * @param command - The command to run
274
274
  * @param options - Optional settings for the command
275
275
  */
276
- async command(command: Document, options?: RunCommandOptions): Promise<Document> {
276
+ async command(command: Document, options?: RunCommandOptions & Abortable): Promise<Document> {
277
277
  // Intentionally, we do not inherit options from parent for this operation.
278
278
  return await executeOperation(
279
279
  this.client,
@@ -284,7 +284,8 @@ export class Db {
284
284
  ...resolveBSONOptions(options),
285
285
  timeoutMS: options?.timeoutMS ?? this.timeoutMS,
286
286
  session: options?.session,
287
- readPreference: options?.readPreference
287
+ readPreference: options?.readPreference,
288
+ signal: options?.signal
288
289
  })
289
290
  )
290
291
  );
@@ -351,22 +352,25 @@ export class Db {
351
352
  */
352
353
  listCollections(
353
354
  filter: Document,
354
- options: Exclude<ListCollectionsOptions, 'nameOnly'> & { nameOnly: true }
355
+ options: Exclude<ListCollectionsOptions, 'nameOnly'> & { nameOnly: true } & Abortable
355
356
  ): ListCollectionsCursor<Pick<CollectionInfo, 'name' | 'type'>>;
356
357
  listCollections(
357
358
  filter: Document,
358
- options: Exclude<ListCollectionsOptions, 'nameOnly'> & { nameOnly: false }
359
+ options: Exclude<ListCollectionsOptions, 'nameOnly'> & { nameOnly: false } & Abortable
359
360
  ): ListCollectionsCursor<CollectionInfo>;
360
361
  listCollections<
361
362
  T extends Pick<CollectionInfo, 'name' | 'type'> | CollectionInfo =
362
363
  | Pick<CollectionInfo, 'name' | 'type'>
363
364
  | CollectionInfo
364
- >(filter?: Document, options?: ListCollectionsOptions): ListCollectionsCursor<T>;
365
+ >(filter?: Document, options?: ListCollectionsOptions & Abortable): ListCollectionsCursor<T>;
365
366
  listCollections<
366
367
  T extends Pick<CollectionInfo, 'name' | 'type'> | CollectionInfo =
367
368
  | Pick<CollectionInfo, 'name' | 'type'>
368
369
  | CollectionInfo
369
- >(filter: Document = {}, options: ListCollectionsOptions = {}): ListCollectionsCursor<T> {
370
+ >(
371
+ filter: Document = {},
372
+ options: ListCollectionsOptions & Abortable = {}
373
+ ): ListCollectionsCursor<T> {
370
374
  return new ListCollectionsCursor<T>(this, filter, resolveOptions(this, options));
371
375
  }
372
376
 
package/src/index.ts CHANGED
@@ -430,6 +430,7 @@ export type {
430
430
  SeverityLevel
431
431
  } from './mongo_logger';
432
432
  export type {
433
+ Abortable,
433
434
  CommonEvents,
434
435
  EventsDescription,
435
436
  GenericListener,
@@ -18,6 +18,7 @@ import type { ClientMetadata } from './cmap/handshake/client_metadata';
18
18
  import type { CompressorName } from './cmap/wire_protocol/compression';
19
19
  import { parseOptions, resolveSRVRecord } from './connection_string';
20
20
  import { MONGO_CLIENT_EVENTS } from './constants';
21
+ import { type AbstractCursor } from './cursor/abstract_cursor';
21
22
  import { Db, type DbOptions } from './db';
22
23
  import type { Encrypter } from './encrypter';
23
24
  import { MongoInvalidArgumentError } from './error';
@@ -323,6 +324,12 @@ export interface MongoClientPrivate {
323
324
  * - used to notify the leak checker in our tests if test author forgot to clean up explicit sessions
324
325
  */
325
326
  readonly activeSessions: Set<ClientSession>;
327
+ /**
328
+ * We keep a reference to the cursors that are created from this client.
329
+ * - used to track and close all cursors in client.close().
330
+ * Cursors in this set are ones that still need to have their close method invoked (no other conditions are considered)
331
+ */
332
+ readonly activeCursors: Set<AbstractCursor>;
326
333
  readonly sessionPool: ServerSessionPool;
327
334
  readonly options: MongoOptions;
328
335
  readonly readConcern?: ReadConcern;
@@ -398,6 +405,7 @@ export class MongoClient extends TypedEventEmitter<MongoClientEvents> implements
398
405
  hasBeenClosed: false,
399
406
  sessionPool: new ServerSessionPool(this),
400
407
  activeSessions: new Set(),
408
+ activeCursors: new Set(),
401
409
  authProviders: new MongoClientAuthProviders(),
402
410
 
403
411
  get options() {
@@ -650,6 +658,11 @@ export class MongoClient extends TypedEventEmitter<MongoClientEvents> implements
650
658
  writable: false
651
659
  });
652
660
 
661
+ const activeCursorCloses = Array.from(this.s.activeCursors, cursor => cursor.close());
662
+ this.s.activeCursors.clear();
663
+
664
+ await Promise.all(activeCursorCloses);
665
+
653
666
  const activeSessionEnds = Array.from(this.s.activeSessions, session => session.endSession());
654
667
  this.s.activeSessions.clear();
655
668
 
@@ -474,6 +474,44 @@ export class TypedEventEmitter<Events extends EventsDescription> extends EventEm
474
474
  /** @public */
475
475
  export class CancellationToken extends TypedEventEmitter<{ cancel(): void }> {}
476
476
 
477
+ /** @public */
478
+ export type Abortable = {
479
+ /**
480
+ * @experimental
481
+ * When provided, the corresponding `AbortController` can be used to abort an asynchronous action.
482
+ *
483
+ * The `signal.reason` value is used as the error thrown.
484
+ *
485
+ * @remarks
486
+ * **NOTE:** If an abort signal aborts an operation while the driver is writing to the underlying
487
+ * socket or reading the response from the server, the socket will be closed.
488
+ * If signals are aborted at a high rate during socket read/writes this can lead to a high rate of connection reestablishment.
489
+ *
490
+ * We plan to mitigate this in a future release, please follow NODE-6062 (`timeoutMS` expiration suffers the same limitation).
491
+ *
492
+ * AbortSignals are likely a best fit for human interactive interruption (ex. ctrl-C) where the frequency
493
+ * of cancellation is reasonably low. If a signal is programmatically aborted for 100s of operations you can empty
494
+ * the driver's connection pool.
495
+ *
496
+ * @example
497
+ * ```js
498
+ * const controller = new AbortController();
499
+ * const { signal } = controller;
500
+ * process.on('SIGINT', () => controller.abort(new Error('^C pressed')));
501
+ *
502
+ * try {
503
+ * const res = await fetch('...', { signal });
504
+ * await collection.findOne(await res.json(), { signal });
505
+ * catch (error) {
506
+ * if (error === signal.reason) {
507
+ * // signal abort error handling
508
+ * }
509
+ * }
510
+ * ```
511
+ */
512
+ signal?: AbortSignal | undefined;
513
+ };
514
+
477
515
  /**
478
516
  * Helper types for dot-notation filter attributes
479
517
  */
@@ -25,7 +25,7 @@ import {
25
25
  import type { Topology } from '../sdam/topology';
26
26
  import type { ClientSession } from '../sessions';
27
27
  import { TimeoutContext } from '../timeout';
28
- import { supportsRetryableWrites } from '../utils';
28
+ import { abortable, supportsRetryableWrites } from '../utils';
29
29
  import { AbstractOperation, Aspect } from './operation';
30
30
 
31
31
  const MMAPv1_RETRY_WRITES_ERROR_CODE = MONGODB_ERROR_CODES.IllegalOperation;
@@ -64,7 +64,10 @@ export async function executeOperation<
64
64
  throw new MongoRuntimeError('This method requires a valid operation instance');
65
65
  }
66
66
 
67
- const topology = await autoConnect(client);
67
+ const topology =
68
+ client.topology == null
69
+ ? await abortable(autoConnect(client), operation.options)
70
+ : client.topology;
68
71
 
69
72
  // The driver sessions spec mandates that we implicitly create sessions for operations
70
73
  // that are not explicitly provided with a session.
@@ -198,7 +201,8 @@ async function tryOperation<
198
201
  let server = await topology.selectServer(selector, {
199
202
  session,
200
203
  operationName: operation.commandName,
201
- timeoutContext
204
+ timeoutContext,
205
+ signal: operation.options.signal
202
206
  });
203
207
 
204
208
  const hasReadAspect = operation.hasAspect(Aspect.READ_OPERATION);
@@ -260,7 +264,8 @@ async function tryOperation<
260
264
  server = await topology.selectServer(selector, {
261
265
  session,
262
266
  operationName: operation.commandName,
263
- previousServer
267
+ previousServer,
268
+ signal: operation.options.signal
264
269
  });
265
270
 
266
271
  if (hasWriteAspect && !supportsRetryableWrites(server)) {
@@ -2,6 +2,7 @@ import type { Binary, Document } from '../bson';
2
2
  import { CursorResponse } from '../cmap/wire_protocol/responses';
3
3
  import { type CursorTimeoutContext, type CursorTimeoutMode } from '../cursor/abstract_cursor';
4
4
  import type { Db } from '../db';
5
+ import { type Abortable } from '../mongo_types';
5
6
  import type { Server } from '../sdam/server';
6
7
  import type { ClientSession } from '../sessions';
7
8
  import { type TimeoutContext } from '../timeout';
@@ -10,7 +11,9 @@ import { CommandOperation, type CommandOperationOptions } from './command';
10
11
  import { Aspect, defineAspects } from './operation';
11
12
 
12
13
  /** @public */
13
- export interface ListCollectionsOptions extends Omit<CommandOperationOptions, 'writeConcern'> {
14
+ export interface ListCollectionsOptions
15
+ extends Omit<CommandOperationOptions, 'writeConcern'>,
16
+ Abortable {
14
17
  /** Since 4.0: If true, will only return the collection name in the response, and will omit additional info */
15
18
  nameOnly?: boolean;
16
19
  /** Since 4.0: If true and nameOnly is true, allows a user without the required privilege (i.e. listCollections action on the database) to run the command when access control is enforced. */
@@ -1,4 +1,5 @@
1
1
  import { type BSONSerializeOptions, type Document, resolveBSONOptions } from '../bson';
2
+ import { type Abortable } from '../mongo_types';
2
3
  import { ReadPreference, type ReadPreferenceLike } from '../read_preference';
3
4
  import type { Server } from '../sdam/server';
4
5
  import type { ClientSession } from '../sessions';
@@ -59,7 +60,7 @@ export abstract class AbstractOperation<TResult = any> {
59
60
  // BSON serialization options
60
61
  bsonOptions?: BSONSerializeOptions;
61
62
 
62
- options: OperationOptions;
63
+ options: OperationOptions & Abortable;
63
64
 
64
65
  /** Specifies the time an operation will run until it throws a timeout error. */
65
66
  timeoutMS?: number;
@@ -68,7 +69,7 @@ export abstract class AbstractOperation<TResult = any> {
68
69
 
69
70
  static aspects?: Set<symbol>;
70
71
 
71
- constructor(options: OperationOptions = {}) {
72
+ constructor(options: OperationOptions & Abortable = {}) {
72
73
  this.readPreference = this.hasAspect(Aspect.WRITE_OPERATION)
73
74
  ? ReadPreference.primary
74
75
  : (ReadPreference.fromOptions(options) ?? ReadPreference.primary);