CoolFace
Apppublic

opusdev/vector-similarity-api

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes
timeout.ts406 linesDownload Raw Back to src
1import { clearTimeout, setTimeout } from 'timers';2 3import { type Document } from './bson';4import { MongoInvalidArgumentError, MongoOperationTimeoutError, MongoRuntimeError } from './error';5import { type ClientSession } from './sessions';6import { csotMin, noop, squashError } from './utils';7 8/** @internal */9export class TimeoutError extends Error {10  duration: number;11  override get name(): 'TimeoutError' {12    return 'TimeoutError';13  }14 15  constructor(message: string, options: { cause?: Error; duration: number }) {16    super(message, options);17    this.duration = options.duration;18  }19 20  static is(error: unknown): error is TimeoutError {21    return (22      error != null && typeof error === 'object' && 'name' in error && error.name === 'TimeoutError'23    );24  }25}26 27type Executor = ConstructorParameters<typeof Promise<never>>[0];28type Reject = Parameters<ConstructorParameters<typeof Promise<never>>[0]>[1];29/**30 * @internal31 * This class is an abstraction over timeouts32 * The Timeout class can only be in the pending or rejected states. It is guaranteed not to resolve33 * if interacted with exclusively through its public API34 * */35export class Timeout extends Promise<never> {36  private id?: NodeJS.Timeout;37 38  public readonly start: number;39  public ended: number | null = null;40  public duration: number;41  private timedOut = false;42  public cleared = false;43 44  get remainingTime(): number {45    if (this.timedOut) return 0;46    if (this.duration === 0) return Infinity;47    return this.start + this.duration - Math.trunc(performance.now());48  }49 50  get timeElapsed(): number {51    return Math.trunc(performance.now()) - this.start;52  }53 54  /** Create a new timeout that expires in `duration` ms */55  private constructor(56    executor: Executor = () => null,57    options?: { duration: number; unref?: true; rejection?: Error }58  ) {59    const duration = options?.duration ?? 0;60    const unref = !!options?.unref;61    const rejection = options?.rejection;62 63    if (duration < 0) {64      throw new MongoInvalidArgumentError('Cannot create a Timeout with a negative duration');65    }66 67    let reject!: Reject;68    super((_, promiseReject) => {69      reject = promiseReject;70 71      executor(noop, promiseReject);72    });73 74    this.duration = duration;75    this.start = Math.trunc(performance.now());76 77    if (rejection == null && this.duration > 0) {78      this.id = setTimeout(() => {79        this.ended = Math.trunc(performance.now());80        this.timedOut = true;81        reject(new TimeoutError(`Expired after ${duration}ms`, { duration }));82      }, this.duration);83      if (typeof this.id.unref === 'function' && unref) {84        // Ensure we do not keep the Node.js event loop running85        this.id.unref();86      }87    } else if (rejection != null) {88      this.ended = Math.trunc(performance.now());89      this.timedOut = true;90      reject(rejection);91    }92  }93 94  /**95   * Clears the underlying timeout. This method is idempotent96   */97  clear(): void {98    clearTimeout(this.id);99    this.id = undefined;100    this.timedOut = false;101    this.cleared = true;102  }103 104  throwIfExpired(): void {105    if (this.timedOut) {106      // This method is invoked when someone wants to throw immediately instead of await the result of this promise107      // Since they won't be handling the rejection from the promise (because we're about to throw here)108      // attach handling to prevent this from bubbling up to Node.js109      this.then(undefined, squashError);110      throw new TimeoutError('Timed out', { duration: this.duration });111    }112  }113 114  public static expires(duration: number, unref?: true): Timeout {115    return new Timeout(undefined, { duration, unref });116  }117 118  static override reject(rejection?: Error): Timeout {119    return new Timeout(undefined, { duration: 0, unref: true, rejection });120  }121}122 123/** @internal */124export type TimeoutContextOptions = (LegacyTimeoutContextOptions | CSOTTimeoutContextOptions) & {125  session?: ClientSession;126};127 128/** @internal */129export type LegacyTimeoutContextOptions = {130  serverSelectionTimeoutMS: number;131  waitQueueTimeoutMS: number;132  socketTimeoutMS?: number;133};134 135/** @internal */136export type CSOTTimeoutContextOptions = {137  timeoutMS: number;138  serverSelectionTimeoutMS: number;139  socketTimeoutMS?: number;140};141 142function isLegacyTimeoutContextOptions(v: unknown): v is LegacyTimeoutContextOptions {143  return (144    v != null &&145    typeof v === 'object' &&146    'serverSelectionTimeoutMS' in v &&147    typeof v.serverSelectionTimeoutMS === 'number' &&148    'waitQueueTimeoutMS' in v &&149    typeof v.waitQueueTimeoutMS === 'number'150  );151}152 153function isCSOTTimeoutContextOptions(v: unknown): v is CSOTTimeoutContextOptions {154  return (155    v != null &&156    typeof v === 'object' &&157    'serverSelectionTimeoutMS' in v &&158    typeof v.serverSelectionTimeoutMS === 'number' &&159    'timeoutMS' in v &&160    typeof v.timeoutMS === 'number'161  );162}163 164/** @internal */165export abstract class TimeoutContext {166  static create(options: TimeoutContextOptions): TimeoutContext {167    if (options.session?.timeoutContext != null) return options.session?.timeoutContext;168    if (isCSOTTimeoutContextOptions(options)) return new CSOTTimeoutContext(options);169    else if (isLegacyTimeoutContextOptions(options)) return new LegacyTimeoutContext(options);170    else throw new MongoRuntimeError('Unrecognized options');171  }172 173  abstract get maxTimeMS(): number | null;174 175  abstract get serverSelectionTimeout(): Timeout | null;176 177  abstract get connectionCheckoutTimeout(): Timeout | null;178 179  abstract get clearServerSelectionTimeout(): boolean;180 181  abstract get timeoutForSocketWrite(): Timeout | null;182 183  abstract get timeoutForSocketRead(): Timeout | null;184 185  abstract csotEnabled(): this is CSOTTimeoutContext;186 187  abstract refresh(): void;188 189  abstract clear(): void;190 191  /** Returns a new instance of the TimeoutContext, with all timeouts refreshed and restarted. */192  abstract refreshed(): TimeoutContext;193 194  abstract addMaxTimeMSToCommand(command: Document, options: { omitMaxTimeMS?: boolean }): void;195 196  abstract getSocketTimeoutMS(): number | undefined;197}198 199/** @internal */200export class CSOTTimeoutContext extends TimeoutContext {201  timeoutMS: number;202  serverSelectionTimeoutMS: number;203  socketTimeoutMS?: number;204 205  clearServerSelectionTimeout: boolean;206 207  private _serverSelectionTimeout?: Timeout | null;208  private _connectionCheckoutTimeout?: Timeout | null;209  public minRoundTripTime = 0;210  public start: number;211 212  constructor(options: CSOTTimeoutContextOptions) {213    super();214    this.start = Math.trunc(performance.now());215 216    this.timeoutMS = options.timeoutMS;217 218    this.serverSelectionTimeoutMS = options.serverSelectionTimeoutMS;219 220    this.socketTimeoutMS = options.socketTimeoutMS;221 222    this.clearServerSelectionTimeout = false;223  }224 225  get maxTimeMS(): number {226    return this.remainingTimeMS - this.minRoundTripTime;227  }228 229  get remainingTimeMS() {230    const timePassed = Math.trunc(performance.now()) - this.start;231    return this.timeoutMS <= 0 ? Infinity : this.timeoutMS - timePassed;232  }233 234  csotEnabled(): this is CSOTTimeoutContext {235    return true;236  }237 238  get serverSelectionTimeout(): Timeout | null {239    // check for undefined240    if (typeof this._serverSelectionTimeout !== 'object' || this._serverSelectionTimeout?.cleared) {241      const { remainingTimeMS, serverSelectionTimeoutMS } = this;242      if (remainingTimeMS <= 0)243        return Timeout.reject(244          new MongoOperationTimeoutError(`Timed out in server selection after ${this.timeoutMS}ms`)245        );246      const usingServerSelectionTimeoutMS =247        serverSelectionTimeoutMS !== 0 &&248        csotMin(remainingTimeMS, serverSelectionTimeoutMS) === serverSelectionTimeoutMS;249      if (usingServerSelectionTimeoutMS) {250        this._serverSelectionTimeout = Timeout.expires(serverSelectionTimeoutMS);251      } else {252        if (remainingTimeMS > 0 && Number.isFinite(remainingTimeMS)) {253          this._serverSelectionTimeout = Timeout.expires(remainingTimeMS);254        } else {255          this._serverSelectionTimeout = null;256        }257      }258    }259 260    return this._serverSelectionTimeout;261  }262 263  get connectionCheckoutTimeout(): Timeout | null {264    if (265      typeof this._connectionCheckoutTimeout !== 'object' ||266      this._connectionCheckoutTimeout?.cleared267    ) {268      if (typeof this._serverSelectionTimeout === 'object') {269        // null or Timeout270        this._connectionCheckoutTimeout = this._serverSelectionTimeout;271      } else {272        throw new MongoRuntimeError(273          'Unreachable. If you are seeing this error, please file a ticket on the NODE driver project on Jira'274        );275      }276    }277    return this._connectionCheckoutTimeout;278  }279 280  get timeoutForSocketWrite(): Timeout | null {281    const { remainingTimeMS } = this;282    if (!Number.isFinite(remainingTimeMS)) return null;283    if (remainingTimeMS > 0) return Timeout.expires(remainingTimeMS);284    return Timeout.reject(new MongoOperationTimeoutError('Timed out before socket write'));285  }286 287  get timeoutForSocketRead(): Timeout | null {288    const { remainingTimeMS } = this;289    if (!Number.isFinite(remainingTimeMS)) return null;290    if (remainingTimeMS > 0) return Timeout.expires(remainingTimeMS);291    return Timeout.reject(new MongoOperationTimeoutError('Timed out before socket read'));292  }293 294  refresh(): void {295    this.start = Math.trunc(performance.now());296    this.minRoundTripTime = 0;297    this._serverSelectionTimeout?.clear();298    this._connectionCheckoutTimeout?.clear();299  }300 301  clear(): void {302    this._serverSelectionTimeout?.clear();303    this._connectionCheckoutTimeout?.clear();304  }305 306  /**307   * @internal308   * Throws a MongoOperationTimeoutError if the context has expired.309   * If the context has not expired, returns the `remainingTimeMS`310   **/311  getRemainingTimeMSOrThrow(message?: string): number {312    const { remainingTimeMS } = this;313    if (remainingTimeMS <= 0)314      throw new MongoOperationTimeoutError(message ?? `Expired after ${this.timeoutMS}ms`);315    return remainingTimeMS;316  }317 318  /**319   * @internal320   * This method is intended to be used in situations where concurrent operation are on the same deadline, but cannot share a single `TimeoutContext` instance.321   * Returns a new instance of `CSOTTimeoutContext` constructed with identical options, but setting the `start` property to `this.start`.322   */323  clone(): CSOTTimeoutContext {324    const timeoutContext = new CSOTTimeoutContext({325      timeoutMS: this.timeoutMS,326      serverSelectionTimeoutMS: this.serverSelectionTimeoutMS327    });328    timeoutContext.start = this.start;329    return timeoutContext;330  }331 332  override refreshed(): CSOTTimeoutContext {333    return new CSOTTimeoutContext(this);334  }335 336  override addMaxTimeMSToCommand(command: Document, options: { omitMaxTimeMS?: boolean }): void {337    if (options.omitMaxTimeMS) return;338    const maxTimeMS = this.remainingTimeMS - this.minRoundTripTime;339    if (maxTimeMS > 0 && Number.isFinite(maxTimeMS)) command.maxTimeMS = maxTimeMS;340  }341 342  override getSocketTimeoutMS(): number | undefined {343    return 0;344  }345}346 347/** @internal */348export class LegacyTimeoutContext extends TimeoutContext {349  options: LegacyTimeoutContextOptions;350  clearServerSelectionTimeout: boolean;351 352  constructor(options: LegacyTimeoutContextOptions) {353    super();354    this.options = options;355    this.clearServerSelectionTimeout = true;356  }357 358  csotEnabled(): this is CSOTTimeoutContext {359    return false;360  }361 362  get serverSelectionTimeout(): Timeout | null {363    if (this.options.serverSelectionTimeoutMS != null && this.options.serverSelectionTimeoutMS > 0)364      return Timeout.expires(this.options.serverSelectionTimeoutMS);365    return null;366  }367 368  get connectionCheckoutTimeout(): Timeout | null {369    if (this.options.waitQueueTimeoutMS != null && this.options.waitQueueTimeoutMS > 0)370      return Timeout.expires(this.options.waitQueueTimeoutMS);371    return null;372  }373 374  get timeoutForSocketWrite(): Timeout | null {375    return null;376  }377 378  get timeoutForSocketRead(): Timeout | null {379    return null;380  }381 382  refresh(): void {383    return;384  }385 386  clear(): void {387    return;388  }389 390  get maxTimeMS() {391    return null;392  }393 394  override refreshed(): LegacyTimeoutContext {395    return new LegacyTimeoutContext(this.options);396  }397 398  override addMaxTimeMSToCommand(_command: Document, _options: { omitMaxTimeMS?: boolean }): void {399    // No max timeMS is added to commands in legacy timeout mode.400  }401 402  override getSocketTimeoutMS(): number | undefined {403    return this.options.socketTimeoutMS;404  }405}406