opusdev/vector-similarity-api
1
1import * as crypto from 'crypto';2import type { SrvRecord } from 'dns';3import { type EventEmitter } from 'events';4import { promises as fs } from 'fs';5import * as http from 'http';6import { clearTimeout, setTimeout } from 'timers';7import * as url from 'url';8import { URL } from 'url';9import { promisify } from 'util';10 11import { deserialize, type Document, ObjectId, resolveBSONOptions } from './bson';12import type { Connection } from './cmap/connection';13import { MAX_SUPPORTED_WIRE_VERSION } from './cmap/wire_protocol/constants';14import type { Collection } from './collection';15import { kDecoratedKeys, LEGACY_HELLO_COMMAND } from './constants';16import type { AbstractCursor } from './cursor/abstract_cursor';17import type { FindCursor } from './cursor/find_cursor';18import type { Db } from './db';19import {20 type AnyError,21 MongoAPIError,22 MongoInvalidArgumentError,23 MongoNetworkTimeoutError,24 MongoNotConnectedError,25 MongoParseError,26 MongoRuntimeError27} from './error';28import type { MongoClient } from './mongo_client';29import { type Abortable } from './mongo_types';30import type { CommandOperationOptions, OperationParent } from './operations/command';31import type { Hint, OperationOptions } from './operations/operation';32import { ReadConcern } from './read_concern';33import { ReadPreference } from './read_preference';34import { ServerType } from './sdam/common';35import type { Server } from './sdam/server';36import type { Topology } from './sdam/topology';37import type { ClientSession } from './sessions';38import { type TimeoutContextOptions } from './timeout';39import { WriteConcern } from './write_concern';40 41/**42 * MongoDB Driver style callback43 * @public44 */45export type Callback<T = any> = (error?: AnyError, result?: T) => void;46 47export type AnyOptions = Document;48 49export const ByteUtils = {50 toLocalBufferType(this: void, buffer: Buffer | Uint8Array): Buffer {51 return Buffer.isBuffer(buffer)52 ? buffer53 : Buffer.from(buffer.buffer, buffer.byteOffset, buffer.byteLength);54 },55 56 equals(this: void, seqA: Uint8Array, seqB: Uint8Array) {57 return ByteUtils.toLocalBufferType(seqA).equals(seqB);58 },59 60 compare(this: void, seqA: Uint8Array, seqB: Uint8Array) {61 return ByteUtils.toLocalBufferType(seqA).compare(seqB);62 },63 64 toBase64(this: void, uint8array: Uint8Array) {65 return ByteUtils.toLocalBufferType(uint8array).toString('base64');66 }67};68 69/**70 * Returns true if value is a Uint8Array or a Buffer71 * @param value - any value that may be a Uint8Array72 */73export function isUint8Array(value: unknown): value is Uint8Array {74 return (75 value != null &&76 typeof value === 'object' &&77 Symbol.toStringTag in value &&78 value[Symbol.toStringTag] === 'Uint8Array'79 );80}81 82/**83 * Determines if a connection's address matches a user provided list84 * of domain wildcards.85 */86export function hostMatchesWildcards(host: string, wildcards: string[]): boolean {87 for (const wildcard of wildcards) {88 if (89 host === wildcard ||90 (wildcard.startsWith('*.') && host?.endsWith(wildcard.substring(2, wildcard.length))) ||91 (wildcard.startsWith('*/') && host?.endsWith(wildcard.substring(2, wildcard.length)))92 ) {93 return true;94 }95 }96 return false;97}98 99/**100 * Ensure Hint field is in a shape we expect:101 * - object of index names mapping to 1 or -1102 * - just an index name103 * @internal104 */105export function normalizeHintField(hint?: Hint): Hint | undefined {106 let finalHint = undefined;107 108 if (typeof hint === 'string') {109 finalHint = hint;110 } else if (Array.isArray(hint)) {111 finalHint = {};112 113 hint.forEach(param => {114 finalHint[param] = 1;115 });116 } else if (hint != null && typeof hint === 'object') {117 finalHint = {} as Document;118 for (const name in hint) {119 finalHint[name] = hint[name];120 }121 }122 123 return finalHint;124}125 126const TO_STRING = (object: unknown) => Object.prototype.toString.call(object);127/**128 * Checks if arg is an Object:129 * - **NOTE**: the check is based on the `[Symbol.toStringTag]() === 'Object'`130 * @internal131 */132 133export function isObject(arg: unknown): arg is object {134 return '[object Object]' === TO_STRING(arg);135}136 137/** @internal */138export function mergeOptions<T, S>(target: T, source: S): T & S {139 return { ...target, ...source };140}141 142/** @internal */143export function filterOptions(options: AnyOptions, names: ReadonlyArray<string>): AnyOptions {144 const filterOptions: AnyOptions = {};145 146 for (const name in options) {147 if (names.includes(name)) {148 filterOptions[name] = options[name];149 }150 }151 152 // Filtered options153 return filterOptions;154}155 156interface HasRetryableWrites {157 retryWrites?: boolean;158}159/**160 * Applies retryWrites: true to a command if retryWrites is set on the command's database.161 * @internal162 *163 * @param target - The target command to which we will apply retryWrites.164 * @param db - The database from which we can inherit a retryWrites value.165 */166export function applyRetryableWrites<T extends HasRetryableWrites>(target: T, db?: Db): T {167 if (db && db.s.options?.retryWrites) {168 target.retryWrites = true;169 }170 171 return target;172}173 174/**175 * Applies a write concern to a command based on well defined inheritance rules, optionally176 * detecting support for the write concern in the first place.177 * @internal178 *179 * @param target - the target command we will be applying the write concern to180 * @param sources - sources where we can inherit default write concerns from181 * @param options - optional settings passed into a command for write concern overrides182 */183 184/**185 * Checks if a given value is a Promise186 *187 * @typeParam T - The resolution type of the possible promise188 * @param value - An object that could be a promise189 * @returns true if the provided value is a Promise190 */191export function isPromiseLike<T = unknown>(value?: unknown): value is PromiseLike<T> {192 return (193 value != null &&194 typeof value === 'object' &&195 'then' in value &&196 typeof value.then === 'function'197 );198}199 200/**201 * Applies collation to a given command.202 * @internal203 *204 * @param command - the command on which to apply collation205 * @param target - target of command206 * @param options - options containing collation settings207 */208export function decorateWithCollation(command: Document, options: AnyOptions): void {209 if (options.collation && typeof options.collation === 'object') {210 command.collation = options.collation;211 }212}213 214/**215 * Applies a read concern to a given command.216 * @internal217 *218 * @param command - the command on which to apply the read concern219 * @param coll - the parent collection of the operation calling this method220 */221export function decorateWithReadConcern(222 command: Document,223 coll: { s: { readConcern?: ReadConcern } },224 options?: OperationOptions225): void {226 if (options && options.session && options.session.inTransaction()) {227 return;228 }229 const readConcern = Object.assign({}, command.readConcern || {});230 if (coll.s.readConcern) {231 Object.assign(readConcern, coll.s.readConcern);232 }233 234 if (Object.keys(readConcern).length > 0) {235 Object.assign(command, { readConcern: readConcern });236 }237}238 239/**240 * @internal241 */242export type TopologyProvider =243 | MongoClient244 | ClientSession245 | FindCursor246 | AbstractCursor247 | Collection<any>248 | Db;249 250/**251 * A helper function to get the topology from a given provider. Throws252 * if the topology cannot be found.253 * @throws MongoNotConnectedError254 * @internal255 */256export function getTopology(provider: TopologyProvider): Topology {257 // MongoClient or ClientSession or AbstractCursor258 if ('topology' in provider && provider.topology) {259 return provider.topology;260 } else if ('client' in provider && provider.client.topology) {261 return provider.client.topology;262 }263 264 throw new MongoNotConnectedError('MongoClient must be connected to perform this operation');265}266 267/** @internal */268export function ns(ns: string): MongoDBNamespace {269 return MongoDBNamespace.fromString(ns);270}271 272/** @public */273export class MongoDBNamespace {274 db: string;275 collection?: string;276 /**277 * Create a namespace object278 *279 * @param db - database name280 * @param collection - collection name281 */282 constructor(db: string, collection?: string) {283 this.db = db;284 this.collection = collection === '' ? undefined : collection;285 }286 287 toString(): string {288 return this.collection ? `${this.db}.${this.collection}` : this.db;289 }290 291 withCollection(collection: string): MongoDBCollectionNamespace {292 return new MongoDBCollectionNamespace(this.db, collection);293 }294 295 static fromString(namespace?: string): MongoDBNamespace {296 if (typeof namespace !== 'string' || namespace === '') {297 // TODO(NODE-3483): Replace with MongoNamespaceError298 throw new MongoRuntimeError(`Cannot parse namespace from "${namespace}"`);299 }300 301 const [db, ...collectionParts] = namespace.split('.');302 const collection = collectionParts.join('.');303 return new MongoDBNamespace(db, collection === '' ? undefined : collection);304 }305}306 307/**308 * @public309 *310 * A class representing a collection's namespace. This class enforces (through Typescript) that311 * the `collection` portion of the namespace is defined and should only be312 * used in scenarios where this can be guaranteed.313 */314export class MongoDBCollectionNamespace extends MongoDBNamespace {315 override collection: string;316 317 constructor(db: string, collection: string) {318 super(db, collection);319 this.collection = collection;320 }321 322 static override fromString(namespace?: string): MongoDBCollectionNamespace {323 return super.fromString(namespace) as MongoDBCollectionNamespace;324 }325}326 327/** @internal */328export function* makeCounter(seed = 0): Generator<number> {329 let count = seed;330 while (true) {331 const newCount = count;332 count += 1;333 yield newCount;334 }335}336 337/**338 * Synchronously Generate a UUIDv4339 * @internal340 */341export function uuidV4(): Buffer {342 const result = crypto.randomBytes(16);343 result[6] = (result[6] & 0x0f) | 0x40;344 result[8] = (result[8] & 0x3f) | 0x80;345 return result;346}347 348/**349 * A helper function for determining `maxWireVersion` between legacy and new topology instances350 * @internal351 */352export function maxWireVersion(handshakeAware?: Connection | Topology | Server): number {353 if (handshakeAware) {354 if (handshakeAware.hello) {355 return handshakeAware.hello.maxWireVersion;356 }357 358 if (handshakeAware.serverApi?.version) {359 // We return the max supported wire version for serverAPI.360 return MAX_SUPPORTED_WIRE_VERSION;361 }362 // This is the fallback case for load balanced mode. If we are building commands the363 // object being checked will be a connection, and we will have a hello response on364 // it. For other cases, such as retryable writes, the object will be a server or365 // topology, and there will be no hello response on those objects, so we return366 // the max wire version so we support retryability. Once we have a min supported367 // wire version of 9, then the needsRetryableWriteLabel() check can remove the368 // usage of passing the wire version into it.369 if (handshakeAware.loadBalanced) {370 return MAX_SUPPORTED_WIRE_VERSION;371 }372 373 if ('lastHello' in handshakeAware && typeof handshakeAware.lastHello === 'function') {374 const lastHello = handshakeAware.lastHello();375 if (lastHello) {376 return lastHello.maxWireVersion;377 }378 }379 380 if (381 handshakeAware.description &&382 'maxWireVersion' in handshakeAware.description &&383 handshakeAware.description.maxWireVersion != null384 ) {385 return handshakeAware.description.maxWireVersion;386 }387 }388 389 return 0;390}391 392/** @internal */393export function arrayStrictEqual(arr: unknown[], arr2: unknown[]): boolean {394 if (!Array.isArray(arr) || !Array.isArray(arr2)) {395 return false;396 }397 398 return arr.length === arr2.length && arr.every((elt, idx) => elt === arr2[idx]);399}400 401/** @internal */402export function errorStrictEqual(lhs?: AnyError | null, rhs?: AnyError | null): boolean {403 if (lhs === rhs) {404 return true;405 }406 407 if (!lhs || !rhs) {408 return lhs === rhs;409 }410 411 if ((lhs == null && rhs != null) || (lhs != null && rhs == null)) {412 return false;413 }414 415 if (lhs.constructor.name !== rhs.constructor.name) {416 return false;417 }418 419 if (lhs.message !== rhs.message) {420 return false;421 }422 423 return true;424}425 426interface StateTable {427 [key: string]: string[];428}429interface ObjectWithState {430 s: { state: string };431 emit(event: 'stateChanged', state: string, newState: string): void;432}433interface StateTransitionFunction {434 (target: ObjectWithState, newState: string): void;435}436 437/** @public */438export type EventEmitterWithState = {439 /** @internal */440 stateChanged(previous: string, current: string): void;441};442 443/** @internal */444export function makeStateMachine(stateTable: StateTable): StateTransitionFunction {445 return function stateTransition(target, newState) {446 const legalStates = stateTable[target.s.state];447 if (legalStates && legalStates.indexOf(newState) < 0) {448 throw new MongoRuntimeError(449 `illegal state transition from [${target.s.state}] => [${newState}], allowed: [${legalStates}]`450 );451 }452 453 target.emit('stateChanged', target.s.state, newState);454 target.s.state = newState;455 };456}457 458/** @internal */459export function now(): number {460 const hrtime = process.hrtime();461 return Math.floor(hrtime[0] * 1000 + hrtime[1] / 1000000);462}463 464/** @internal */465export function calculateDurationInMs(started: number | undefined): number {466 if (typeof started !== 'number') {467 return -1;468 }469 470 const elapsed = now() - started;471 return elapsed < 0 ? 0 : elapsed;472}473 474/** @internal */475export function hasAtomicOperators(476 doc: Document | Document[],477 options?: CommandOperationOptions478): boolean {479 if (Array.isArray(doc)) {480 for (const document of doc) {481 if (hasAtomicOperators(document)) {482 return true;483 }484 }485 return false;486 }487 488 const keys = Object.keys(doc);489 // In this case we need to throw if all the atomic operators are undefined.490 if (options?.ignoreUndefined) {491 let allUndefined = true;492 for (const key of keys) {493 // eslint-disable-next-line no-restricted-syntax494 if (doc[key] !== undefined) {495 allUndefined = false;496 break;497 }498 }499 if (allUndefined) {500 throw new MongoInvalidArgumentError(501 'Update operations require that all atomic operators have defined values, but none were provided.'502 );503 }504 }505 506 return keys.length > 0 && keys[0][0] === '$';507}508 509export function resolveTimeoutOptions<T extends Partial<TimeoutContextOptions>>(510 client: MongoClient,511 options: T512): T &513 Pick<514 MongoClient['s']['options'],515 'timeoutMS' | 'serverSelectionTimeoutMS' | 'waitQueueTimeoutMS' | 'socketTimeoutMS'516 > {517 const { socketTimeoutMS, serverSelectionTimeoutMS, waitQueueTimeoutMS, timeoutMS } =518 client.s.options;519 return { socketTimeoutMS, serverSelectionTimeoutMS, waitQueueTimeoutMS, timeoutMS, ...options };520}521/**522 * Merge inherited properties from parent into options, prioritizing values from options,523 * then values from parent.524 *525 * @param parent - An optional owning class of the operation being run. ex. Db/Collection/MongoClient.526 * @param options - The options passed to the operation method.527 *528 * @internal529 */530export function resolveOptions<T extends CommandOperationOptions>(531 parent: OperationParent | undefined,532 options?: T533): T {534 const result: T = Object.assign({}, options, resolveBSONOptions(options, parent));535 536 const timeoutMS = options?.timeoutMS ?? parent?.timeoutMS;537 // Users cannot pass a readConcern/writeConcern to operations in a transaction538 const session = options?.session;539 540 if (!session?.inTransaction()) {541 const readConcern = ReadConcern.fromOptions(options) ?? parent?.readConcern;542 if (readConcern) {543 result.readConcern = readConcern;544 }545 546 let writeConcern = WriteConcern.fromOptions(options) ?? parent?.writeConcern;547 if (writeConcern) {548 if (timeoutMS != null) {549 writeConcern = WriteConcern.fromOptions({550 writeConcern: {551 ...writeConcern,552 wtimeout: undefined,553 wtimeoutMS: undefined554 }555 });556 }557 result.writeConcern = writeConcern;558 }559 }560 561 result.timeoutMS = timeoutMS;562 563 const readPreference = ReadPreference.fromOptions(options) ?? parent?.readPreference;564 if (readPreference) {565 result.readPreference = readPreference;566 }567 568 const isConvenientTransaction = session?.explicit && session?.timeoutContext != null;569 if (isConvenientTransaction && options?.timeoutMS != null) {570 throw new MongoInvalidArgumentError(571 'An operation cannot be given a timeoutMS setting when inside a withTransaction call that has a timeoutMS setting'572 );573 }574 575 return result;576}577 578export function isSuperset(set: Set<any> | any[], subset: Set<any> | any[]): boolean {579 set = Array.isArray(set) ? new Set(set) : set;580 subset = Array.isArray(subset) ? new Set(subset) : subset;581 for (const elem of subset) {582 if (!set.has(elem)) {583 return false;584 }585 }586 return true;587}588 589/**590 * Checks if the document is a Hello request591 * @internal592 */593export function isHello(doc: Document): boolean {594 return doc[LEGACY_HELLO_COMMAND] || doc.hello ? true : false;595}596 597/** Returns the items that are uniquely in setA */598export function setDifference<T>(setA: Iterable<T>, setB: Iterable<T>): Set<T> {599 const difference = new Set<T>(setA);600 for (const elem of setB) {601 difference.delete(elem);602 }603 return difference;604}605 606const HAS_OWN = (object: unknown, prop: string) =>607 Object.prototype.hasOwnProperty.call(object, prop);608 609export function isRecord<T extends readonly string[]>(610 value: unknown,611 requiredKeys: T612): value is Record<T[number], any>;613export function isRecord(value: unknown): value is Record<string, any>;614export function isRecord(615 value: unknown,616 requiredKeys: string[] | undefined = undefined617): value is Record<string, any> {618 if (!isObject(value)) {619 return false;620 }621 622 const ctor = (value as any).constructor;623 if (ctor && ctor.prototype) {624 if (!isObject(ctor.prototype)) {625 return false;626 }627 628 // Check to see if some method exists from the Object exists629 if (!HAS_OWN(ctor.prototype, 'isPrototypeOf')) {630 return false;631 }632 }633 634 if (requiredKeys) {635 const keys = Object.keys(value as Record<string, any>);636 return isSuperset(keys, requiredKeys);637 }638 639 return true;640}641 642type ListNode<T> = {643 value: T;644 next: ListNode<T> | HeadNode<T>;645 prev: ListNode<T> | HeadNode<T>;646};647 648type HeadNode<T> = {649 value: null;650 next: ListNode<T>;651 prev: ListNode<T>;652};653 654/**655 * When a list is empty the head is a reference with pointers to itself656 * So this type represents that self referential state657 */658type EmptyNode = {659 value: null;660 next: EmptyNode;661 prev: EmptyNode;662};663 664/**665 * A sequential list of items in a circularly linked list666 * @remarks667 * The head node is special, it is always defined and has a value of null.668 * It is never "included" in the list, in that, it is not returned by pop/shift or yielded by the iterator.669 * The circular linkage and always defined head node are to reduce checks for null next/prev references to zero.670 * New nodes are declared as object literals with keys always in the same order: next, prev, value.671 * @internal672 */673export class List<T = unknown> {674 private readonly head: HeadNode<T> | EmptyNode;675 private count: number;676 677 get length() {678 return this.count;679 }680 681 get [Symbol.toStringTag]() {682 return 'List' as const;683 }684 685 constructor() {686 this.count = 0;687 688 // this is carefully crafted:689 // declaring a complete and consistently key ordered690 // object is beneficial to the runtime optimizations691 this.head = {692 next: null,693 prev: null,694 value: null695 } as unknown as EmptyNode;696 this.head.next = this.head;697 this.head.prev = this.head;698 }699 700 toArray() {701 return Array.from(this);702 }703 704 toString() {705 return `head <=> ${this.toArray().join(' <=> ')} <=> head`;706 }707 708 *[Symbol.iterator](): Generator<T, void, void> {709 for (const node of this.nodes()) {710 yield node.value;711 }712 }713 714 private *nodes(): Generator<ListNode<T>, void, void> {715 let ptr: HeadNode<T> | ListNode<T> | EmptyNode = this.head.next;716 while (ptr !== this.head) {717 // Save next before yielding so that we make removing within iteration safe718 const { next } = ptr as ListNode<T>;719 yield ptr as ListNode<T>;720 ptr = next;721 }722 }723 724 /** Insert at end of list */725 push(value: T) {726 this.count += 1;727 const newNode: ListNode<T> = {728 next: this.head as HeadNode<T>,729 prev: this.head.prev as ListNode<T>,730 value731 };732 this.head.prev.next = newNode;733 this.head.prev = newNode;734 }735 736 /** Inserts every item inside an iterable instead of the iterable itself */737 pushMany(iterable: Iterable<T>) {738 for (const value of iterable) {739 this.push(value);740 }741 }742 743 /** Insert at front of list */744 unshift(value: T) {745 this.count += 1;746 const newNode: ListNode<T> = {747 next: this.head.next as ListNode<T>,748 prev: this.head as HeadNode<T>,749 value750 };751 this.head.next.prev = newNode;752 this.head.next = newNode;753 }754 755 private remove(node: ListNode<T> | EmptyNode): T | null {756 if (node === this.head || this.length === 0) {757 return null;758 }759 760 this.count -= 1;761 762 const prevNode = node.prev;763 const nextNode = node.next;764 prevNode.next = nextNode;765 nextNode.prev = prevNode;766 767 return node.value;768 }769 770 /** Removes the first node at the front of the list */771 shift(): T | null {772 return this.remove(this.head.next);773 }774 775 /** Removes the last node at the end of the list */776 pop(): T | null {777 return this.remove(this.head.prev);778 }779 780 /** Iterates through the list and removes nodes where filter returns true */781 prune(filter: (value: T) => boolean) {782 for (const node of this.nodes()) {783 if (filter(node.value)) {784 this.remove(node);785 }786 }787 }788 789 clear() {790 this.count = 0;791 this.head.next = this.head as EmptyNode;792 this.head.prev = this.head as EmptyNode;793 }794 795 /** Returns the first item in the list, does not remove */796 first(): T | null {797 // If the list is empty, value will be the head's null798 return this.head.next.value;799 }800 801 /** Returns the last item in the list, does not remove */802 last(): T | null {803 // If the list is empty, value will be the head's null804 return this.head.prev.value;805 }806}807 808/**809 * A pool of Buffers which allow you to read them as if they were one810 * @internal811 */812export class BufferPool {813 private buffers: List<Buffer>;814 private totalByteLength: number;815 816 constructor() {817 this.buffers = new List();818 this.totalByteLength = 0;819 }820 821 get length(): number {822 return this.totalByteLength;823 }824 825 /** Adds a buffer to the internal buffer pool list */826 append(buffer: Buffer): void {827 this.buffers.push(buffer);828 this.totalByteLength += buffer.length;829 }830 831 /**832 * If BufferPool contains 4 bytes or more construct an int32 from the leading bytes,833 * otherwise return null. Size can be negative, caller should error check.834 */835 getInt32(): number | null {836 if (this.totalByteLength < 4) {837 return null;838 }839 const firstBuffer = this.buffers.first();840 if (firstBuffer != null && firstBuffer.byteLength >= 4) {841 return firstBuffer.readInt32LE(0);842 }843 844 // Unlikely case: an int32 is split across buffers.845 // Use read and put the returned buffer back on top846 const top4Bytes = this.read(4);847 const value = top4Bytes.readInt32LE(0);848 849 // Put it back.850 this.totalByteLength += 4;851 this.buffers.unshift(top4Bytes);852 853 return value;854 }855 856 /** Reads the requested number of bytes, optionally consuming them */857 read(size: number): Buffer {858 if (typeof size !== 'number' || size < 0) {859 throw new MongoInvalidArgumentError('Argument "size" must be a non-negative number');860 }861 862 // oversized request returns empty buffer863 if (size > this.totalByteLength) {864 return Buffer.alloc(0);865 }866 867 // We know we have enough, we just don't know how it is spread across chunks868 // TODO(NODE-4732): alloc API should change based on raw option869 const result = Buffer.allocUnsafe(size);870 871 for (let bytesRead = 0; bytesRead < size; ) {872 const buffer = this.buffers.shift();873 if (buffer == null) {874 break;875 }876 const bytesRemaining = size - bytesRead;877 const bytesReadable = Math.min(bytesRemaining, buffer.byteLength);878 const bytes = buffer.subarray(0, bytesReadable);879 880 result.set(bytes, bytesRead);881 882 bytesRead += bytesReadable;883 this.totalByteLength -= bytesReadable;884 if (bytesReadable < buffer.byteLength) {885 this.buffers.unshift(buffer.subarray(bytesReadable));886 }887 }888 889 return result;890 }891}892 893/** @public */894export class HostAddress {895 host: string | undefined = undefined;896 port: number | undefined = undefined;897 socketPath: string | undefined = undefined;898 isIPv6 = false;899 900 constructor(hostString: string) {901 const escapedHost = hostString.split(' ').join('%20'); // escape spaces, for socket path hosts902 903 if (escapedHost.endsWith('.sock')) {904 // heuristically determine if we're working with a domain socket905 this.socketPath = decodeURIComponent(escapedHost);906 return;907 }908 909 const urlString = `iLoveJS://${escapedHost}`;910 let url;911 try {912 url = new URL(urlString);913 } catch (urlError) {914 const runtimeError = new MongoRuntimeError(`Unable to parse ${escapedHost} with URL`);915 runtimeError.cause = urlError;916 throw runtimeError;917 }918 919 const hostname = url.hostname;920 const port = url.port;921 922 let normalized = decodeURIComponent(hostname).toLowerCase();923 if (normalized.startsWith('[') && normalized.endsWith(']')) {924 this.isIPv6 = true;925 normalized = normalized.substring(1, hostname.length - 1);926 }927 928 this.host = normalized.toLowerCase();929 930 if (typeof port === 'number') {931 this.port = port;932 } else if (typeof port === 'string' && port !== '') {933 this.port = Number.parseInt(port, 10);934 } else {935 this.port = 27017;936 }937 938 if (this.port === 0) {939 throw new MongoParseError('Invalid port (zero) with hostname');940 }941 Object.freeze(this);942 }943 944 [Symbol.for('nodejs.util.inspect.custom')](): string {945 return this.inspect();946 }947 948 inspect(): string {949 return `new HostAddress('${this.toString()}')`;950 }951 952 toString(): string {953 if (typeof this.host === 'string') {954 if (this.isIPv6) {955 return `[${this.host}]:${this.port}`;956 }957 return `${this.host}:${this.port}`;958 }959 return `${this.socketPath}`;960 }961 962 static fromString(this: void, s: string): HostAddress {963 return new HostAddress(s);964 }965 966 static fromHostPort(host: string, port: number): HostAddress {967 if (host.includes(':')) {968 host = `[${host}]`; // IPv6 address969 }970 return HostAddress.fromString(`${host}:${port}`);971 }972 973 static fromSrvRecord({ name, port }: SrvRecord): HostAddress {974 return HostAddress.fromHostPort(name, port);975 }976 977 toHostPort(): { host: string; port: number } {978 if (this.socketPath) {979 return { host: this.socketPath, port: 0 };980 }981 982 const host = this.host ?? '';983 const port = this.port ?? 0;984 return { host, port };985 }986}987 988export const DEFAULT_PK_FACTORY = {989 // We prefer not to rely on ObjectId having a createPk method990 createPk(): ObjectId {991 return new ObjectId();992 }993};994 995/**996 * When the driver used emitWarning the code will be equal to this.997 * @public998 *999 * @example1000 * ```ts1001 * process.on('warning', (warning) => {1002 * if (warning.code === MONGODB_WARNING_CODE) console.error('Ah an important warning! :)')1003 * })1004 * ```1005 */1006export const MONGODB_WARNING_CODE = 'MONGODB DRIVER';1007 1008/** @internal */1009export function emitWarning(message: string): void {1010 return process.emitWarning(message, { code: MONGODB_WARNING_CODE } as any);1011}1012 1013const emittedWarnings = new Set();1014/**1015 * Will emit a warning once for the duration of the application.1016 * Uses the message to identify if it has already been emitted1017 * so using string interpolation can cause multiple emits1018 * @internal1019 */1020export function emitWarningOnce(message: string): void {1021 if (!emittedWarnings.has(message)) {1022 emittedWarnings.add(message);1023 return emitWarning(message);1024 }1025}1026 1027/**1028 * Takes a JS object and joins the values into a string separated by ', '1029 */1030export function enumToString(en: Record<string, unknown>): string {1031 return Object.values(en).join(', ');1032}1033 1034/**1035 * Determine if a server supports retryable writes.1036 *1037 * @internal1038 */1039export function supportsRetryableWrites(server?: Server): boolean {1040 if (!server) {1041 return false;1042 }1043 1044 if (server.loadBalanced) {1045 // Loadbalanced topologies will always support retry writes1046 return true;1047 }1048 1049 if (server.description.logicalSessionTimeoutMinutes != null) {1050 // that supports sessions1051 if (server.description.type !== ServerType.Standalone) {1052 // and that is not a standalone1053 return true;1054 }1055 }1056 1057 return false;1058}1059 1060/**1061 * Fisher–Yates Shuffle1062 *1063 * Reference: https://bost.ocks.org/mike/shuffle/1064 * @param sequence - items to be shuffled1065 * @param limit - Defaults to `0`. If nonzero shuffle will slice the randomized array e.g, `.slice(0, limit)` otherwise will return the entire randomized array.1066 */1067export function shuffle<T>(sequence: Iterable<T>, limit = 0): Array<T> {1068 const items = Array.from(sequence); // shallow copy in order to never shuffle the input1069 1070 if (limit > items.length) {1071 throw new MongoRuntimeError('Limit must be less than the number of items');1072 }1073 1074 let remainingItemsToShuffle = items.length;1075 const lowerBound = limit % items.length === 0 ? 1 : items.length - limit;1076 while (remainingItemsToShuffle > lowerBound) {1077 // Pick a remaining element1078 const randomIndex = Math.floor(Math.random() * remainingItemsToShuffle);1079 remainingItemsToShuffle -= 1;1080 1081 // And swap it with the current element1082 const swapHold = items[remainingItemsToShuffle];1083 items[remainingItemsToShuffle] = items[randomIndex];1084 items[randomIndex] = swapHold;1085 }1086 1087 return limit % items.length === 0 ? items : items.slice(lowerBound);1088}1089 1090/**1091 * TODO(NODE-4936): read concern eligibility for commands should be codified in command construction1092 * @internal1093 * @see https://github.com/mongodb/specifications/blob/master/source/read-write-concern/read-write-concern.md#read-concern1094 */1095export function commandSupportsReadConcern(command: Document): boolean {1096 if (command.aggregate || command.count || command.distinct || command.find || command.geoNear) {1097 return true;1098 }1099 1100 return false;1101}1102 1103/**1104 * Compare objectIds. `null` is always less1105 * - `+1 = oid1 is greater than oid2`1106 * - `-1 = oid1 is less than oid2`1107 * - `+0 = oid1 is equal oid2`1108 */1109export function compareObjectId(oid1?: ObjectId | null, oid2?: ObjectId | null): 0 | 1 | -1 {1110 if (oid1 == null && oid2 == null) {1111 return 0;1112 }1113 1114 if (oid1 == null) {1115 return -1;1116 }1117 1118 if (oid2 == null) {1119 return 1;1120 }1121 1122 return ByteUtils.compare(oid1.id, oid2.id);1123}1124 1125export function parseInteger(value: unknown): number | null {1126 if (typeof value === 'number') return Math.trunc(value);1127 const parsedValue = Number.parseInt(String(value), 10);1128 1129 return Number.isNaN(parsedValue) ? null : parsedValue;1130}1131 1132export function parseUnsignedInteger(value: unknown): number | null {1133 const parsedInt = parseInteger(value);1134 1135 return parsedInt != null && parsedInt >= 0 ? parsedInt : null;1136}1137 1138/**1139 * This function throws a MongoAPIError in the event that either of the following is true:1140 * * If the provided address domain does not match the provided parent domain1141 * * If the parent domain contains less than three `.` separated parts and the provided address does not contain at least one more domain level than its parent1142 *1143 * If a DNS server were to become compromised SRV records would still need to1144 * advertise addresses that are under the same domain as the srvHost.1145 *1146 * @param address - The address to check against a domain1147 * @param srvHost - The domain to check the provided address against1148 * @returns void1149 */1150export function checkParentDomainMatch(address: string, srvHost: string): void {1151 // Remove trailing dot if exists on either the resolved address or the srv hostname1152 const normalizedAddress = address.endsWith('.') ? address.slice(0, address.length - 1) : address;1153 const normalizedSrvHost = srvHost.endsWith('.') ? srvHost.slice(0, srvHost.length - 1) : srvHost;1154 1155 const allCharacterBeforeFirstDot = /^.*?\./;1156 const srvIsLessThanThreeParts = normalizedSrvHost.split('.').length < 3;1157 // Remove all characters before first dot1158 // Add leading dot back to string so1159 // an srvHostDomain = '.trusted.site'1160 // will not satisfy an addressDomain that endsWith '.fake-trusted.site'1161 const addressDomain = `.${normalizedAddress.replace(allCharacterBeforeFirstDot, '')}`;1162 let srvHostDomain = srvIsLessThanThreeParts1163 ? normalizedSrvHost1164 : `.${normalizedSrvHost.replace(allCharacterBeforeFirstDot, '')}`;1165 1166 if (!srvHostDomain.startsWith('.')) {1167 srvHostDomain = '.' + srvHostDomain;1168 }1169 if (1170 srvIsLessThanThreeParts &&1171 normalizedAddress.split('.').length <= normalizedSrvHost.split('.').length1172 ) {1173 throw new MongoAPIError(1174 'Server record does not have at least one more domain level than parent URI'1175 );1176 }1177 if (!addressDomain.endsWith(srvHostDomain)) {1178 throw new MongoAPIError('Server record does not share hostname with parent URI');1179 }1180}1181 1182interface RequestOptions {1183 json?: boolean;1184 method?: string;1185 timeout?: number;1186 headers?: http.OutgoingHttpHeaders;1187}1188 1189/**1190 * Perform a get request that returns status and body.1191 * @internal1192 */1193export function get(1194 url: URL | string,1195 options: http.RequestOptions = {}1196): Promise<{ body: string; status: number | undefined }> {1197 return new Promise((resolve, reject) => {1198 /* eslint-disable prefer-const */1199 let timeoutId: NodeJS.Timeout;1200 const request = http