tor-browser

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

LifecycleWatcher.ts (7875B)


      1 /**
      2 * @license
      3 * Copyright 2019 Google Inc.
      4 * SPDX-License-Identifier: Apache-2.0
      5 */
      6 
      7 import type Protocol from 'devtools-protocol';
      8 
      9 import {type Frame, FrameEvent} from '../api/Frame.js';
     10 import type {HTTPRequest} from '../api/HTTPRequest.js';
     11 import type {HTTPResponse} from '../api/HTTPResponse.js';
     12 import type {TimeoutError} from '../common/Errors.js';
     13 import {EventEmitter} from '../common/EventEmitter.js';
     14 import {NetworkManagerEvent} from '../common/NetworkManagerEvents.js';
     15 import {assert} from '../util/assert.js';
     16 import {Deferred} from '../util/Deferred.js';
     17 import {DisposableStack} from '../util/disposable.js';
     18 
     19 import type {CdpFrame} from './Frame.js';
     20 import {FrameManagerEvent} from './FrameManagerEvents.js';
     21 import type {NetworkManager} from './NetworkManager.js';
     22 
     23 /**
     24 * @public
     25 */
     26 export type PuppeteerLifeCycleEvent =
     27  /**
     28   * Waits for the 'load' event.
     29   */
     30  | 'load'
     31  /**
     32   * Waits for the 'DOMContentLoaded' event.
     33   */
     34  | 'domcontentloaded'
     35  /**
     36   * Waits till there are no more than 0 network connections for at least `500`
     37   * ms.
     38   */
     39  | 'networkidle0'
     40  /**
     41   * Waits till there are no more than 2 network connections for at least `500`
     42   * ms.
     43   */
     44  | 'networkidle2';
     45 
     46 /**
     47 * @public
     48 */
     49 export type ProtocolLifeCycleEvent =
     50  | 'load'
     51  | 'DOMContentLoaded'
     52  | 'networkIdle'
     53  | 'networkAlmostIdle';
     54 
     55 const puppeteerToProtocolLifecycle = new Map<
     56  PuppeteerLifeCycleEvent,
     57  ProtocolLifeCycleEvent
     58 >([
     59  ['load', 'load'],
     60  ['domcontentloaded', 'DOMContentLoaded'],
     61  ['networkidle0', 'networkIdle'],
     62  ['networkidle2', 'networkAlmostIdle'],
     63 ]);
     64 
     65 /**
     66 * @internal
     67 */
     68 export class LifecycleWatcher {
     69  #expectedLifecycle: ProtocolLifeCycleEvent[];
     70  #frame: CdpFrame;
     71  #timeout: number;
     72  #navigationRequest: HTTPRequest | null = null;
     73  #subscriptions = new DisposableStack();
     74  #initialLoaderId: string;
     75 
     76  #terminationDeferred: Deferred<Error>;
     77  #sameDocumentNavigationDeferred = Deferred.create<undefined>();
     78  #lifecycleDeferred = Deferred.create<void>();
     79  #newDocumentNavigationDeferred = Deferred.create<undefined>();
     80 
     81  #hasSameDocumentNavigation?: boolean;
     82  #swapped?: boolean;
     83 
     84  #navigationResponseReceived?: Deferred<void>;
     85 
     86  constructor(
     87    networkManager: NetworkManager,
     88    frame: CdpFrame,
     89    waitUntil: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[],
     90    timeout: number,
     91    signal?: AbortSignal,
     92  ) {
     93    if (Array.isArray(waitUntil)) {
     94      waitUntil = waitUntil.slice();
     95    } else if (typeof waitUntil === 'string') {
     96      waitUntil = [waitUntil];
     97    }
     98    this.#initialLoaderId = frame._loaderId;
     99    this.#expectedLifecycle = waitUntil.map(value => {
    100      const protocolEvent = puppeteerToProtocolLifecycle.get(value);
    101      assert(protocolEvent, 'Unknown value for options.waitUntil: ' + value);
    102      return protocolEvent as ProtocolLifeCycleEvent;
    103    });
    104 
    105    signal?.addEventListener('abort', () => {
    106      this.#terminationDeferred.reject(signal.reason);
    107    });
    108 
    109    this.#frame = frame;
    110    this.#timeout = timeout;
    111    const frameManagerEmitter = this.#subscriptions.use(
    112      new EventEmitter(frame._frameManager),
    113    );
    114    frameManagerEmitter.on(
    115      FrameManagerEvent.LifecycleEvent,
    116      this.#checkLifecycleComplete.bind(this),
    117    );
    118 
    119    const frameEmitter = this.#subscriptions.use(new EventEmitter(frame));
    120    frameEmitter.on(
    121      FrameEvent.FrameNavigatedWithinDocument,
    122      this.#navigatedWithinDocument.bind(this),
    123    );
    124    frameEmitter.on(FrameEvent.FrameNavigated, this.#navigated.bind(this));
    125    frameEmitter.on(FrameEvent.FrameSwapped, this.#frameSwapped.bind(this));
    126    frameEmitter.on(
    127      FrameEvent.FrameSwappedByActivation,
    128      this.#frameSwapped.bind(this),
    129    );
    130    frameEmitter.on(FrameEvent.FrameDetached, this.#onFrameDetached.bind(this));
    131 
    132    const networkManagerEmitter = this.#subscriptions.use(
    133      new EventEmitter(networkManager),
    134    );
    135    networkManagerEmitter.on(
    136      NetworkManagerEvent.Request,
    137      this.#onRequest.bind(this),
    138    );
    139    networkManagerEmitter.on(
    140      NetworkManagerEvent.Response,
    141      this.#onResponse.bind(this),
    142    );
    143    networkManagerEmitter.on(
    144      NetworkManagerEvent.RequestFailed,
    145      this.#onRequestFailed.bind(this),
    146    );
    147 
    148    this.#terminationDeferred = Deferred.create<Error>({
    149      timeout: this.#timeout,
    150      message: `Navigation timeout of ${this.#timeout} ms exceeded`,
    151    });
    152 
    153    this.#checkLifecycleComplete();
    154  }
    155 
    156  #onRequest(request: HTTPRequest): void {
    157    if (request.frame() !== this.#frame || !request.isNavigationRequest()) {
    158      return;
    159    }
    160    this.#navigationRequest = request;
    161    // Resolve previous navigation response in case there are multiple
    162    // navigation requests reported by the backend. This generally should not
    163    // happen by it looks like it's possible.
    164    this.#navigationResponseReceived?.resolve();
    165    this.#navigationResponseReceived = Deferred.create();
    166    if (request.response() !== null) {
    167      this.#navigationResponseReceived?.resolve();
    168    }
    169  }
    170 
    171  #onRequestFailed(request: HTTPRequest): void {
    172    if (this.#navigationRequest?.id !== request.id) {
    173      return;
    174    }
    175    this.#navigationResponseReceived?.resolve();
    176  }
    177 
    178  #onResponse(response: HTTPResponse): void {
    179    if (this.#navigationRequest?.id !== response.request().id) {
    180      return;
    181    }
    182    this.#navigationResponseReceived?.resolve();
    183  }
    184 
    185  #onFrameDetached(frame: Frame): void {
    186    if (this.#frame === frame) {
    187      this.#terminationDeferred.resolve(
    188        new Error('Navigating frame was detached'),
    189      );
    190      return;
    191    }
    192    this.#checkLifecycleComplete();
    193  }
    194 
    195  async navigationResponse(): Promise<HTTPResponse | null> {
    196    // Continue with a possibly null response.
    197    await this.#navigationResponseReceived?.valueOrThrow();
    198    return this.#navigationRequest ? this.#navigationRequest.response() : null;
    199  }
    200 
    201  sameDocumentNavigationPromise(): Promise<Error | undefined> {
    202    return this.#sameDocumentNavigationDeferred.valueOrThrow();
    203  }
    204 
    205  newDocumentNavigationPromise(): Promise<Error | undefined> {
    206    return this.#newDocumentNavigationDeferred.valueOrThrow();
    207  }
    208 
    209  lifecyclePromise(): Promise<void> {
    210    return this.#lifecycleDeferred.valueOrThrow();
    211  }
    212 
    213  terminationPromise(): Promise<Error | TimeoutError | undefined> {
    214    return this.#terminationDeferred.valueOrThrow();
    215  }
    216 
    217  #navigatedWithinDocument(): void {
    218    this.#hasSameDocumentNavigation = true;
    219    this.#checkLifecycleComplete();
    220  }
    221 
    222  #navigated(navigationType: Protocol.Page.NavigationType): void {
    223    if (navigationType === 'BackForwardCacheRestore') {
    224      return this.#frameSwapped();
    225    }
    226    this.#checkLifecycleComplete();
    227  }
    228 
    229  #frameSwapped(): void {
    230    this.#swapped = true;
    231    this.#checkLifecycleComplete();
    232  }
    233 
    234  #checkLifecycleComplete(): void {
    235    // We expect navigation to commit.
    236    if (!checkLifecycle(this.#frame, this.#expectedLifecycle)) {
    237      return;
    238    }
    239    this.#lifecycleDeferred.resolve();
    240    if (this.#hasSameDocumentNavigation) {
    241      this.#sameDocumentNavigationDeferred.resolve(undefined);
    242    }
    243    if (this.#swapped || this.#frame._loaderId !== this.#initialLoaderId) {
    244      this.#newDocumentNavigationDeferred.resolve(undefined);
    245    }
    246 
    247    function checkLifecycle(
    248      frame: CdpFrame,
    249      expectedLifecycle: ProtocolLifeCycleEvent[],
    250    ): boolean {
    251      for (const event of expectedLifecycle) {
    252        if (!frame._lifecycleEvents.has(event)) {
    253          return false;
    254        }
    255      }
    256      for (const child of frame.childFrames()) {
    257        if (
    258          child._hasStartedLoading &&
    259          !checkLifecycle(child, expectedLifecycle)
    260        ) {
    261          return false;
    262        }
    263      }
    264      return true;
    265    }
    266  }
    267 
    268  dispose(): void {
    269    this.#subscriptions.dispose();
    270    this.#terminationDeferred.resolve(new Error('LifecycleWatcher disposed'));
    271  }
    272 }