tor-browser

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

disposable.ts (9781B)


      1 /**
      2 * @license
      3 * Copyright 2023 Google Inc.
      4 * SPDX-License-Identifier: Apache-2.0
      5 */
      6 
      7 declare global {
      8  interface SymbolConstructor {
      9    /**
     10     * A method that is used to release resources held by an object. Called by
     11     * the semantics of the `using` statement.
     12     */
     13    readonly dispose: unique symbol;
     14 
     15    /**
     16     * A method that is used to asynchronously release resources held by an
     17     * object. Called by the semantics of the `await using` statement.
     18     */
     19    readonly asyncDispose: unique symbol;
     20  }
     21 
     22  interface Disposable {
     23    [Symbol.dispose](): void;
     24  }
     25 
     26  interface AsyncDisposable {
     27    [Symbol.asyncDispose](): PromiseLike<void>;
     28  }
     29 }
     30 
     31 (Symbol as any).dispose ??= Symbol('dispose');
     32 (Symbol as any).asyncDispose ??= Symbol('asyncDispose');
     33 
     34 /**
     35 * @internal
     36 */
     37 export const disposeSymbol: typeof Symbol.dispose = Symbol.dispose;
     38 
     39 /**
     40 * @internal
     41 */
     42 export const asyncDisposeSymbol: typeof Symbol.asyncDispose =
     43  Symbol.asyncDispose;
     44 
     45 /**
     46 * @internal
     47 */
     48 export class DisposableStack {
     49  #disposed = false;
     50  #stack: Disposable[] = [];
     51 
     52  /**
     53   * Returns a value indicating whether the stack has been disposed.
     54   */
     55  get disposed(): boolean {
     56    return this.#disposed;
     57  }
     58 
     59  /**
     60   * Alias for `[Symbol.dispose]()`.
     61   */
     62  dispose(): void {
     63    this[disposeSymbol]();
     64  }
     65 
     66  /**
     67   * Adds a disposable resource to the top of stack, returning the resource.
     68   * Has no effect if provided `null` or `undefined`.
     69   *
     70   * @param value - A `Disposable` object, `null`, or `undefined`.
     71   * `null` and `undefined` will not be added, but will be returned.
     72   * @returns The provided `value`.
     73   */
     74  use<T extends Disposable | null | undefined>(value: T): T {
     75    if (value && typeof value[disposeSymbol] === 'function') {
     76      this.#stack.push(value);
     77    }
     78    return value;
     79  }
     80 
     81  /**
     82   * Adds a non-disposable resource and a disposal callback to the top of the stack.
     83   *
     84   * @param value - A resource to be disposed.
     85   * @param onDispose - A callback invoked to dispose the provided value.
     86   * Will be invoked with `value` as the first parameter.
     87   * @returns The provided `value`.
     88   */
     89  adopt<T>(value: T, onDispose: (value: T) => void): T {
     90    this.#stack.push({
     91      [disposeSymbol]() {
     92        onDispose(value);
     93      },
     94    });
     95    return value;
     96  }
     97 
     98  /**
     99   * Add a disposal callback to the top of the stack to be invoked when stack is disposed.
    100   * @param onDispose - A callback to invoke when this object is disposed.
    101   */
    102  defer(onDispose: () => void): void {
    103    this.#stack.push({
    104      [disposeSymbol]() {
    105        onDispose();
    106      },
    107    });
    108  }
    109 
    110  /**
    111   * Move all resources out of this stack and into a new `DisposableStack`, and
    112   * marks this stack as disposed.
    113   * @returns The new `DisposableStack`.
    114   *
    115   * @example
    116   *
    117   * ```ts
    118   * class C {
    119   *   #res1: Disposable;
    120   *   #res2: Disposable;
    121   *   #disposables: DisposableStack;
    122   *   constructor() {
    123   *     // stack will be disposed when exiting constructor for any reason
    124   *     using stack = new DisposableStack();
    125   *
    126   *     // get first resource
    127   *     this.#res1 = stack.use(getResource1());
    128   *
    129   *     // get second resource. If this fails, both `stack` and `#res1` will be disposed.
    130   *     this.#res2 = stack.use(getResource2());
    131   *
    132   *     // all operations succeeded, move resources out of `stack` so that
    133   *     // they aren't disposed when constructor exits
    134   *     this.#disposables = stack.move();
    135   *   }
    136   *
    137   *   [disposeSymbol]() {
    138   *     this.#disposables.dispose();
    139   *   }
    140   * }
    141   * ```
    142   */
    143  move(): DisposableStack {
    144    if (this.#disposed) {
    145      throw new ReferenceError('A disposed stack can not use anything new');
    146    }
    147    const stack = new DisposableStack();
    148    stack.#stack = this.#stack;
    149    this.#stack = [];
    150    this.#disposed = true;
    151    return stack;
    152  }
    153 
    154  /**
    155   * Disposes each resource in the stack in last-in-first-out (LIFO) manner.
    156   */
    157  [disposeSymbol](): void {
    158    if (this.#disposed) {
    159      return;
    160    }
    161    this.#disposed = true;
    162    const errors: unknown[] = [];
    163    for (const resource of this.#stack.reverse()) {
    164      try {
    165        resource[disposeSymbol]();
    166      } catch (e) {
    167        errors.push(e);
    168      }
    169    }
    170    if (errors.length === 1) {
    171      throw errors[0];
    172    } else if (errors.length > 1) {
    173      let suppressed = null;
    174      for (const error of errors.reverse()) {
    175        if (suppressed === null) {
    176          suppressed = error;
    177        } else {
    178          suppressed = new SuppressedError(error, suppressed);
    179        }
    180      }
    181      throw suppressed;
    182    }
    183  }
    184 
    185  readonly [Symbol.toStringTag] = 'DisposableStack';
    186 }
    187 
    188 /**
    189 * @internal
    190 */
    191 export class AsyncDisposableStack {
    192  #disposed = false;
    193  #stack: AsyncDisposable[] = [];
    194 
    195  /**
    196   * Returns a value indicating whether the stack has been disposed.
    197   */
    198  get disposed(): boolean {
    199    return this.#disposed;
    200  }
    201 
    202  /**
    203   * Alias for `[Symbol.asyncDispose]()`.
    204   */
    205  async dispose(): Promise<void> {
    206    await this[asyncDisposeSymbol]();
    207  }
    208 
    209  /**
    210   * Adds a AsyncDisposable resource to the top of stack, returning the resource.
    211   * Has no effect if provided `null` or `undefined`.
    212   *
    213   * @param value - A `AsyncDisposable` object, `null`, or `undefined`.
    214   * `null` and `undefined` will not be added, but will be returned.
    215   * @returns The provided `value`.
    216   */
    217  use<T extends AsyncDisposable | Disposable | null | undefined>(value: T): T {
    218    if (value) {
    219      const asyncDispose = (value as AsyncDisposable)[asyncDisposeSymbol];
    220      const dispose = (value as Disposable)[disposeSymbol];
    221 
    222      if (typeof asyncDispose === 'function') {
    223        this.#stack.push(value as AsyncDisposable);
    224      } else if (typeof dispose === 'function') {
    225        this.#stack.push({
    226          [asyncDisposeSymbol]: async () => {
    227            (value as Disposable)[disposeSymbol]();
    228          },
    229        });
    230      }
    231    }
    232 
    233    return value;
    234  }
    235 
    236  /**
    237   * Adds a non-disposable resource and a disposal callback to the top of the stack.
    238   *
    239   * @param value - A resource to be disposed.
    240   * @param onDispose - A callback invoked to dispose the provided value.
    241   * Will be invoked with `value` as the first parameter.
    242   * @returns The provided `value`.
    243   */
    244  adopt<T>(value: T, onDispose: (value: T) => Promise<void>): T {
    245    this.#stack.push({
    246      [asyncDisposeSymbol]() {
    247        return onDispose(value);
    248      },
    249    });
    250    return value;
    251  }
    252 
    253  /**
    254   * Add a disposal callback to the top of the stack to be invoked when stack is disposed.
    255   * @param onDispose - A callback to invoke when this object is disposed.
    256   */
    257  defer(onDispose: () => Promise<void>): void {
    258    this.#stack.push({
    259      [asyncDisposeSymbol]() {
    260        return onDispose();
    261      },
    262    });
    263  }
    264 
    265  /**
    266   * Move all resources out of this stack and into a new `DisposableStack`, and
    267   * marks this stack as disposed.
    268   * @returns The new `AsyncDisposableStack`.
    269   *
    270   * @example
    271   *
    272   * ```ts
    273   * class C {
    274   *   #res1: Disposable;
    275   *   #res2: Disposable;
    276   *   #disposables: DisposableStack;
    277   *   constructor() {
    278   *     // stack will be disposed when exiting constructor for any reason
    279   *     using stack = new DisposableStack();
    280   *
    281   *     // get first resource
    282   *     this.#res1 = stack.use(getResource1());
    283   *
    284   *     // get second resource. If this fails, both `stack` and `#res1` will be disposed.
    285   *     this.#res2 = stack.use(getResource2());
    286   *
    287   *     // all operations succeeded, move resources out of `stack` so that
    288   *     // they aren't disposed when constructor exits
    289   *     this.#disposables = stack.move();
    290   *   }
    291   *
    292   *   [disposeSymbol]() {
    293   *     this.#disposables.dispose();
    294   *   }
    295   * }
    296   * ```
    297   */
    298  move(): AsyncDisposableStack {
    299    if (this.#disposed) {
    300      throw new ReferenceError('A disposed stack can not use anything new');
    301    }
    302    const stack = new AsyncDisposableStack();
    303    stack.#stack = this.#stack;
    304    this.#stack = [];
    305    this.#disposed = true;
    306    return stack;
    307  }
    308 
    309  /**
    310   * Disposes each resource in the stack in last-in-first-out (LIFO) manner.
    311   */
    312  async [asyncDisposeSymbol](): Promise<void> {
    313    if (this.#disposed) {
    314      return;
    315    }
    316    this.#disposed = true;
    317    const errors: unknown[] = [];
    318    for (const resource of this.#stack.reverse()) {
    319      try {
    320        await resource[asyncDisposeSymbol]();
    321      } catch (e) {
    322        errors.push(e);
    323      }
    324    }
    325    if (errors.length === 1) {
    326      throw errors[0];
    327    } else if (errors.length > 1) {
    328      let suppressed = null;
    329      for (const error of errors.reverse()) {
    330        if (suppressed === null) {
    331          suppressed = error;
    332        } else {
    333          suppressed = new SuppressedError(error, suppressed);
    334        }
    335      }
    336      throw suppressed;
    337    }
    338  }
    339 
    340  readonly [Symbol.toStringTag] = 'AsyncDisposableStack';
    341 }
    342 
    343 /**
    344 * @internal
    345 * Represents an error that occurs when multiple errors are thrown during
    346 * the disposal of resources. This class encapsulates the primary error and
    347 * any suppressed errors that occurred subsequently.
    348 */
    349 export class SuppressedError extends Error {
    350  #error: unknown;
    351  #suppressed: unknown;
    352 
    353  constructor(
    354    error: unknown,
    355    suppressed: unknown,
    356    message = 'An error was suppressed during disposal',
    357  ) {
    358    super(message);
    359    this.name = 'SuppressedError';
    360    this.#error = error;
    361    this.#suppressed = suppressed;
    362  }
    363 
    364  /**
    365   * The primary error that occurred during disposal.
    366   */
    367  get error(): unknown {
    368    return this.#error;
    369  }
    370 
    371  /**
    372   * The suppressed error i.e. the error that was suppressed
    373   * because it occurred later in the flow after the original error.
    374   */
    375  get suppressed(): unknown {
    376    return this.#suppressed;
    377  }
    378 }