tor-browser

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

WaitTask.ts (7039B)


      1 /**
      2 * @license
      3 * Copyright 2022 Google Inc.
      4 * SPDX-License-Identifier: Apache-2.0
      5 */
      6 
      7 import type {ElementHandle} from '../api/ElementHandle.js';
      8 import type {JSHandle} from '../api/JSHandle.js';
      9 import type {Realm} from '../api/Realm.js';
     10 import type {Poller} from '../injected/Poller.js';
     11 import {Deferred} from '../util/Deferred.js';
     12 import {isErrorLike} from '../util/ErrorLike.js';
     13 import {stringifyFunction} from '../util/Function.js';
     14 
     15 import {TimeoutError} from './Errors.js';
     16 import {LazyArg} from './LazyArg.js';
     17 import type {HandleFor} from './types.js';
     18 
     19 /**
     20 * @internal
     21 */
     22 export interface WaitTaskOptions {
     23  polling: 'raf' | 'mutation' | number;
     24  root?: ElementHandle<Node>;
     25  timeout: number;
     26  signal?: AbortSignal;
     27 }
     28 
     29 /**
     30 * @internal
     31 */
     32 export class WaitTask<T = unknown> {
     33  #world: Realm;
     34  #polling: 'raf' | 'mutation' | number;
     35  #root?: ElementHandle<Node>;
     36 
     37  #fn: string;
     38  #args: unknown[];
     39 
     40  #timeout?: NodeJS.Timeout;
     41  #timeoutError?: TimeoutError;
     42 
     43  #result = Deferred.create<HandleFor<T>>();
     44 
     45  #poller?: JSHandle<Poller<T>>;
     46  #signal?: AbortSignal;
     47  #reruns: AbortController[] = [];
     48 
     49  constructor(
     50    world: Realm,
     51    options: WaitTaskOptions,
     52    fn: ((...args: unknown[]) => Promise<T>) | string,
     53    ...args: unknown[]
     54  ) {
     55    this.#world = world;
     56    this.#polling = options.polling;
     57    this.#root = options.root;
     58    this.#signal = options.signal;
     59    this.#signal?.addEventListener('abort', this.#onAbortSignal, {
     60      once: true,
     61    });
     62 
     63    switch (typeof fn) {
     64      case 'string':
     65        this.#fn = `() => {return (${fn});}`;
     66        break;
     67      default:
     68        this.#fn = stringifyFunction(fn);
     69        break;
     70    }
     71    this.#args = args;
     72 
     73    this.#world.taskManager.add(this);
     74 
     75    if (options.timeout) {
     76      this.#timeoutError = new TimeoutError(
     77        `Waiting failed: ${options.timeout}ms exceeded`,
     78      );
     79      this.#timeout = setTimeout(() => {
     80        void this.terminate(this.#timeoutError);
     81      }, options.timeout);
     82    }
     83 
     84    void this.rerun();
     85  }
     86 
     87  get result(): Promise<HandleFor<T>> {
     88    return this.#result.valueOrThrow();
     89  }
     90 
     91  async rerun(): Promise<void> {
     92    for (const prev of this.#reruns) {
     93      prev.abort();
     94    }
     95    this.#reruns.length = 0;
     96    const controller = new AbortController();
     97    this.#reruns.push(controller);
     98    try {
     99      switch (this.#polling) {
    100        case 'raf':
    101          this.#poller = await this.#world.evaluateHandle(
    102            ({RAFPoller, createFunction}, fn, ...args) => {
    103              const fun = createFunction(fn);
    104              return new RAFPoller(() => {
    105                return fun(...args) as Promise<T>;
    106              });
    107            },
    108            LazyArg.create(context => {
    109              return context.puppeteerUtil;
    110            }),
    111            this.#fn,
    112            ...this.#args,
    113          );
    114          break;
    115        case 'mutation':
    116          this.#poller = await this.#world.evaluateHandle(
    117            ({MutationPoller, createFunction}, root, fn, ...args) => {
    118              const fun = createFunction(fn);
    119              return new MutationPoller(() => {
    120                return fun(...args) as Promise<T>;
    121              }, root || document);
    122            },
    123            LazyArg.create(context => {
    124              return context.puppeteerUtil;
    125            }),
    126            this.#root,
    127            this.#fn,
    128            ...this.#args,
    129          );
    130          break;
    131        default:
    132          this.#poller = await this.#world.evaluateHandle(
    133            ({IntervalPoller, createFunction}, ms, fn, ...args) => {
    134              const fun = createFunction(fn);
    135              return new IntervalPoller(() => {
    136                return fun(...args) as Promise<T>;
    137              }, ms);
    138            },
    139            LazyArg.create(context => {
    140              return context.puppeteerUtil;
    141            }),
    142            this.#polling,
    143            this.#fn,
    144            ...this.#args,
    145          );
    146          break;
    147      }
    148 
    149      await this.#poller.evaluate(poller => {
    150        void poller.start();
    151      });
    152 
    153      const result = await this.#poller.evaluateHandle(poller => {
    154        return poller.result();
    155      });
    156      this.#result.resolve(result);
    157 
    158      await this.terminate();
    159    } catch (error) {
    160      if (controller.signal.aborted) {
    161        return;
    162      }
    163      const badError = this.getBadError(error);
    164      if (badError) {
    165        await this.terminate(badError);
    166      }
    167    }
    168  }
    169 
    170  async terminate(error?: Error): Promise<void> {
    171    this.#world.taskManager.delete(this);
    172 
    173    this.#signal?.removeEventListener('abort', this.#onAbortSignal);
    174 
    175    clearTimeout(this.#timeout);
    176 
    177    if (error && !this.#result.finished()) {
    178      this.#result.reject(error);
    179    }
    180 
    181    if (this.#poller) {
    182      try {
    183        await this.#poller.evaluate(async poller => {
    184          await poller.stop();
    185        });
    186        if (this.#poller) {
    187          await this.#poller.dispose();
    188          this.#poller = undefined;
    189        }
    190      } catch {
    191        // Ignore errors since they most likely come from low-level cleanup.
    192      }
    193    }
    194  }
    195 
    196  /**
    197   * Not all errors lead to termination. They usually imply we need to rerun the task.
    198   */
    199  getBadError(error: unknown): Error | undefined {
    200    if (isErrorLike(error)) {
    201      // When frame is detached the task should have been terminated by the IsolatedWorld.
    202      // This can fail if we were adding this task while the frame was detached,
    203      // so we terminate here instead.
    204      if (
    205        error.message.includes(
    206          'Execution context is not available in detached frame',
    207        )
    208      ) {
    209        return new Error('Waiting failed: Frame detached');
    210      }
    211 
    212      // When the page is navigated, the promise is rejected.
    213      // We will try again in the new execution context.
    214      if (error.message.includes('Execution context was destroyed')) {
    215        return;
    216      }
    217 
    218      // We could have tried to evaluate in a context which was already
    219      // destroyed.
    220      if (error.message.includes('Cannot find context with specified id')) {
    221        return;
    222      }
    223 
    224      // Errors coming from WebDriver BiDi. TODO: Adjust messages after
    225      // https://github.com/w3c/webdriver-bidi/issues/540 is resolved.
    226      if (error.message.includes('DiscardedBrowsingContextError')) {
    227        return;
    228      }
    229 
    230      return error;
    231    }
    232 
    233    return new Error('WaitTask failed with an error', {
    234      cause: error,
    235    });
    236  }
    237 
    238  #onAbortSignal = () => {
    239    void this.terminate(this.#signal?.reason);
    240  };
    241 }
    242 
    243 /**
    244 * @internal
    245 */
    246 export class TaskManager {
    247  #tasks: Set<WaitTask> = new Set<WaitTask>();
    248 
    249  add(task: WaitTask<any>): void {
    250    this.#tasks.add(task);
    251  }
    252 
    253  delete(task: WaitTask<any>): void {
    254    this.#tasks.delete(task);
    255  }
    256 
    257  terminateAll(error?: Error): void {
    258    for (const task of this.#tasks) {
    259      void task.terminate(error);
    260    }
    261    this.#tasks.clear();
    262  }
    263 
    264  async rerunAll(): Promise<void> {
    265    await Promise.all(
    266      [...this.#tasks].map(task => {
    267        return task.rerun();
    268      }),
    269    );
    270  }
    271 }