tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

CallbackRegistry.ts (4117B)


      1 /**
      2 * @license
      3 * Copyright 2023 Google Inc.
      4 * SPDX-License-Identifier: Apache-2.0
      5 */
      6 
      7 import {Deferred} from '../util/Deferred.js';
      8 import {rewriteError} from '../util/ErrorLike.js';
      9 import {createIncrementalIdGenerator} from '../util/incremental-id-generator.js';
     10 
     11 import {ProtocolError, TargetCloseError} from './Errors.js';
     12 import {debugError} from './util.js';
     13 
     14 const idGenerator = createIncrementalIdGenerator();
     15 
     16 /**
     17 * Manages callbacks and their IDs for the protocol request/response communication.
     18 *
     19 * @internal
     20 */
     21 export class CallbackRegistry {
     22  #callbacks = new Map<number, Callback>();
     23  #idGenerator = idGenerator;
     24 
     25  create(
     26    label: string,
     27    timeout: number | undefined,
     28    request: (id: number) => void,
     29  ): Promise<unknown> {
     30    const callback = new Callback(this.#idGenerator(), label, timeout);
     31    this.#callbacks.set(callback.id, callback);
     32    try {
     33      request(callback.id);
     34    } catch (error) {
     35      // We still throw sync errors synchronously and clean up the scheduled
     36      // callback.
     37      callback.promise.catch(debugError).finally(() => {
     38        this.#callbacks.delete(callback.id);
     39      });
     40      callback.reject(error as Error);
     41      throw error;
     42    }
     43    // Must only have sync code up until here.
     44    return callback.promise.finally(() => {
     45      this.#callbacks.delete(callback.id);
     46    });
     47  }
     48 
     49  reject(id: number, message: string, originalMessage?: string): void {
     50    const callback = this.#callbacks.get(id);
     51    if (!callback) {
     52      return;
     53    }
     54    this._reject(callback, message, originalMessage);
     55  }
     56 
     57  rejectRaw(id: number, error: object): void {
     58    const callback = this.#callbacks.get(id);
     59    if (!callback) {
     60      return;
     61    }
     62    callback.reject(error as any);
     63  }
     64 
     65  _reject(
     66    callback: Callback,
     67    errorMessage: string | ProtocolError,
     68    originalMessage?: string,
     69  ): void {
     70    let error: ProtocolError;
     71    let message: string;
     72    if (errorMessage instanceof ProtocolError) {
     73      error = errorMessage;
     74      error.cause = callback.error;
     75      message = errorMessage.message;
     76    } else {
     77      error = callback.error;
     78      message = errorMessage;
     79    }
     80 
     81    callback.reject(
     82      rewriteError(
     83        error,
     84        `Protocol error (${callback.label}): ${message}`,
     85        originalMessage,
     86      ),
     87    );
     88  }
     89 
     90  resolve(id: number, value: unknown): void {
     91    const callback = this.#callbacks.get(id);
     92    if (!callback) {
     93      return;
     94    }
     95    callback.resolve(value);
     96  }
     97 
     98  clear(): void {
     99    for (const callback of this.#callbacks.values()) {
    100      // TODO: probably we can accept error messages as params.
    101      this._reject(callback, new TargetCloseError('Target closed'));
    102    }
    103    this.#callbacks.clear();
    104  }
    105 
    106  /**
    107   * @internal
    108   */
    109  getPendingProtocolErrors(): Error[] {
    110    const result: Error[] = [];
    111    for (const callback of this.#callbacks.values()) {
    112      result.push(
    113        new Error(
    114          `${callback.label} timed out. Trace: ${callback.error.stack}`,
    115        ),
    116      );
    117    }
    118    return result;
    119  }
    120 }
    121 /**
    122 * @internal
    123 */
    124 
    125 export class Callback {
    126  #id: number;
    127  #error = new ProtocolError();
    128  #deferred = Deferred.create<unknown>();
    129  #timer?: ReturnType<typeof setTimeout>;
    130  #label: string;
    131 
    132  constructor(id: number, label: string, timeout?: number) {
    133    this.#id = id;
    134    this.#label = label;
    135    if (timeout) {
    136      this.#timer = setTimeout(() => {
    137        this.#deferred.reject(
    138          rewriteError(
    139            this.#error,
    140            `${label} timed out. Increase the 'protocolTimeout' setting in launch/connect calls for a higher timeout if needed.`,
    141          ),
    142        );
    143      }, timeout);
    144    }
    145  }
    146 
    147  resolve(value: unknown): void {
    148    clearTimeout(this.#timer);
    149    this.#deferred.resolve(value);
    150  }
    151 
    152  reject(error: Error): void {
    153    clearTimeout(this.#timer);
    154    this.#deferred.reject(error);
    155  }
    156 
    157  get id(): number {
    158    return this.#id;
    159  }
    160 
    161  get promise(): Promise<unknown> {
    162    return this.#deferred.valueOrThrow();
    163  }
    164 
    165  get error(): ProtocolError {
    166    return this.#error;
    167  }
    168 
    169  get label(): string {
    170    return this.#label;
    171  }
    172 }