All Downloads are FREE. Search and download functionalities are using the official Maven repository.

META-INF.dirigible.dev-tools.sdk.ResourceTreeModel.js Maven / Gradle / Ivy

There is a newer version: 10.6.27
Show newest version
/*
 * Copyright (C) 2011 Google Inc. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 *     * Redistributions of source code must retain the above copyright
 * notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above
 * copyright notice, this list of conditions and the following disclaimer
 * in the documentation and/or other materials provided with the
 * distribution.
 *     * Neither the name of Google Inc. nor the names of its
 * contributors may be used to endorse or promote products derived from
 * this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

import * as Common from '../common/common.js';
import * as ProtocolClient from '../protocol_client/protocol_client.js';

import {DOMModel} from './DOMModel.js';
import {Events as NetworkManagerEvents, NetworkManager} from './NetworkManager.js';
import {NetworkRequest} from './NetworkRequest.js';  // eslint-disable-line no-unused-vars
import {Resource} from './Resource.js';
import {ExecutionContext, RuntimeModel} from './RuntimeModel.js';
import {Capability, SDKModel, Target, TargetManager} from './SDKModel.js';  // eslint-disable-line no-unused-vars
import {SecurityOriginManager} from './SecurityOriginManager.js';

export class ResourceTreeModel extends SDKModel {
  /**
   * @param {!Target} target
   */
  constructor(target) {
    super(target);

    const networkManager = target.model(NetworkManager);
    if (networkManager) {
      networkManager.addEventListener(NetworkManagerEvents.RequestFinished, this._onRequestFinished, this);
      networkManager.addEventListener(NetworkManagerEvents.RequestUpdateDropped, this._onRequestUpdateDropped, this);
    }
    this._agent = target.pageAgent();
    this._agent.enable();
    this._securityOriginManager = target.model(SecurityOriginManager);

    target.registerPageDispatcher(new PageDispatcher(this));

    /** @type {!Map} */
    this._frames = new Map();
    this._cachedResourcesProcessed = false;
    this._pendingReloadOptions = null;
    this._reloadSuspensionCount = 0;
    this._isInterstitialShowing = false;
    /** @type {?ResourceTreeFrame} */
    this.mainFrame = null;

    this._agent.getResourceTree().then(this._processCachedResources.bind(this));
  }

  /**
   * @param {!NetworkRequest} request
   * @return {?ResourceTreeFrame}
   */
  static frameForRequest(request) {
    const networkManager = NetworkManager.forRequest(request);
    const resourceTreeModel = networkManager ? networkManager.target().model(ResourceTreeModel) : null;
    if (!resourceTreeModel) {
      return null;
    }
    return resourceTreeModel.frameForId(request.frameId);
  }

  /**
   * @return {!Array.}
   */
  static frames() {
    const result = [];
    for (const resourceTreeModel of TargetManager.instance().models(ResourceTreeModel)) {
      result.push(...resourceTreeModel._frames.values());
    }
    return result;
  }

  /**
   * @param {string} url
   * @return {?Resource}
   */
  static resourceForURL(url) {
    for (const resourceTreeModel of TargetManager.instance().models(ResourceTreeModel)) {
      const mainFrame = resourceTreeModel.mainFrame;
      const result = mainFrame ? mainFrame.resourceForURL(url) : null;
      if (result) {
        return result;
      }
    }
    return null;
  }

  /**
   * @param {boolean=} bypassCache
   * @param {string=} scriptToEvaluateOnLoad
   */
  static reloadAllPages(bypassCache, scriptToEvaluateOnLoad) {
    for (const resourceTreeModel of TargetManager.instance().models(ResourceTreeModel)) {
      if (!resourceTreeModel.target().parentTarget()) {
        resourceTreeModel.reloadPage(bypassCache, scriptToEvaluateOnLoad);
      }
    }
  }

  /**
   * @return {!DOMModel}
   */
  domModel() {
    return /** @type {!DOMModel} */ (this.target().model(DOMModel));
  }

  /**
   * @param {?Protocol.Page.FrameResourceTree} mainFramePayload
   */
  _processCachedResources(mainFramePayload) {
    if (mainFramePayload) {
      this.dispatchEventToListeners(Events.WillLoadCachedResources);
      this._addFramesRecursively(null, mainFramePayload);
      this.target().setInspectedURL(mainFramePayload.frame.url);
    }
    this._cachedResourcesProcessed = true;
    const runtimeModel = this.target().model(RuntimeModel);
    if (runtimeModel) {
      runtimeModel.setExecutionContextComparator(this._executionContextComparator.bind(this));
      runtimeModel.fireExecutionContextOrderChanged();
    }
    this.dispatchEventToListeners(Events.CachedResourcesLoaded, this);
  }

  /**
   * @return {boolean}
   */
  cachedResourcesLoaded() {
    return this._cachedResourcesProcessed;
  }

  /**
   * @return {boolean}
   */
  isInterstitialShowing() {
    return this._isInterstitialShowing;
  }

  /**
   * @param {!ResourceTreeFrame} frame
   * @param {boolean=} aboutToNavigate
   */
  _addFrame(frame, aboutToNavigate) {
    this._frames.set(frame.id, frame);
    if (frame.isMainFrame()) {
      this.mainFrame = frame;
    }
    this.dispatchEventToListeners(Events.FrameAdded, frame);
    this._updateSecurityOrigins();
  }

  /**
   * @param {!Protocol.Page.FrameId} frameId
   * @param {?Protocol.Page.FrameId} parentFrameId
   * @param {!Protocol.Runtime.StackTrace=} stackTrace
   * @return {?ResourceTreeFrame}
   */
  _frameAttached(frameId, parentFrameId, stackTrace) {
    const parentFrame = parentFrameId ? (this._frames.get(parentFrameId) || null) : null;
    // Do nothing unless cached resource tree is processed - it will overwrite everything.
    if (!this._cachedResourcesProcessed && parentFrame) {
      return null;
    }
    if (this._frames.has(frameId)) {
      return null;
    }

    const frame = new ResourceTreeFrame(this, parentFrame, frameId, null, stackTrace || null);
    if (parentFrameId && !parentFrame) {
      frame._crossTargetParentFrameId = parentFrameId;
    }
    if (frame.isMainFrame() && this.mainFrame) {
      // Navigation to the new backend process.
      this._frameDetached(this.mainFrame.id);
    }
    this._addFrame(frame, true);
    return frame;
  }

  /**
   * @param {!Protocol.Page.Frame} framePayload
   */
  _frameNavigated(framePayload) {
    const parentFrame = framePayload.parentId ? (this._frames.get(framePayload.parentId) || null) : null;
    // Do nothing unless cached resource tree is processed - it will overwrite everything.
    if (!this._cachedResourcesProcessed && parentFrame) {
      return;
    }
    let frame = this._frames.get(framePayload.id);
    if (!frame) {
      // Simulate missed "frameAttached" for a main frame navigation to the new backend process.
      frame = this._frameAttached(framePayload.id, framePayload.parentId || '');
      console.assert(frame);
    }

    this.dispatchEventToListeners(Events.FrameWillNavigate, frame);
    frame._navigate(framePayload);
    this.dispatchEventToListeners(Events.FrameNavigated, frame);

    if (frame.isMainFrame()) {
      this.dispatchEventToListeners(Events.MainFrameNavigated, frame);
    }

    // Fill frame with retained resources (the ones loaded using new loader).
    const resources = frame.resources();
    for (let i = 0; i < resources.length; ++i) {
      this.dispatchEventToListeners(Events.ResourceAdded, resources[i]);
    }

    if (frame.isMainFrame()) {
      this.target().setInspectedURL(frame.url);
    }
    this._updateSecurityOrigins();
  }

  /**
   * @param {!Protocol.Page.FrameId} frameId
   */
  _frameDetached(frameId) {
    // Do nothing unless cached resource tree is processed - it will overwrite everything.
    if (!this._cachedResourcesProcessed) {
      return;
    }

    const frame = this._frames.get(frameId);
    if (!frame) {
      return;
    }

    if (frame.parentFrame) {
      frame.parentFrame._removeChildFrame(frame);
    } else {
      frame._remove();
    }
    this._updateSecurityOrigins();
  }

  /**
   * @param {!Common.EventTarget.EventTargetEvent} event
   */
  _onRequestFinished(event) {
    if (!this._cachedResourcesProcessed) {
      return;
    }

    const request = /** @type {!NetworkRequest} */ (event.data);
    if (request.failed || request.resourceType() === Common.ResourceType.resourceTypes.XHR) {
      return;
    }

    const frame = this._frames.get(request.frameId);
    if (frame) {
      frame._addRequest(request);
    }
  }

  /**
   * @param {!Common.EventTarget.EventTargetEvent} event
   */
  _onRequestUpdateDropped(event) {
    if (!this._cachedResourcesProcessed) {
      return;
    }

    const frameId = event.data.frameId;
    const frame = this._frames.get(frameId);
    if (!frame) {
      return;
    }

    const url = event.data.url;
    if (frame._resourcesMap[url]) {
      return;
    }

    const resource = new Resource(
        this, null, url, frame.url, frameId, event.data.loaderId,
        Common.ResourceType.resourceTypes[event.data.resourceType], event.data.mimeType, event.data.lastModified, null);
    frame.addResource(resource);
  }

  /**
   * @param {!Protocol.Page.FrameId} frameId
   * @return {!ResourceTreeFrame}
   */
  frameForId(frameId) {
    return this._frames.get(frameId);
  }

  /**
   * @param {function(!Resource)} callback
   * @return {boolean}
   */
  forAllResources(callback) {
    if (this.mainFrame) {
      return this.mainFrame._callForFrameResources(callback);
    }
    return false;
  }

  /**
   * @return {!Array}
   */
  frames() {
    return [...this._frames.values()];
  }

  /**
   * @param {string} url
   * @return {?Resource}
   */
  resourceForURL(url) {
    // Workers call into this with no frames available.
    return this.mainFrame ? this.mainFrame.resourceForURL(url) : null;
  }

  /**
   * @param {?ResourceTreeFrame} parentFrame
   * @param {!Protocol.Page.FrameResourceTree} frameTreePayload
   */
  _addFramesRecursively(parentFrame, frameTreePayload) {
    const framePayload = frameTreePayload.frame;
    const frame = new ResourceTreeFrame(this, parentFrame, framePayload.id, framePayload, null);
    if (!parentFrame && framePayload.parentId) {
      frame._crossTargetParentFrameId = framePayload.parentId;
    }
    this._addFrame(frame);

    for (const childFrame of frameTreePayload.childFrames || []) {
      this._addFramesRecursively(frame, childFrame);
    }

    for (let i = 0; i < frameTreePayload.resources.length; ++i) {
      const subresource = frameTreePayload.resources[i];
      const resource = this._createResourceFromFramePayload(
          framePayload, subresource.url, Common.ResourceType.resourceTypes[subresource.type], subresource.mimeType,
          subresource.lastModified || null, subresource.contentSize || null);
      frame.addResource(resource);
    }

    if (!frame._resourcesMap[framePayload.url]) {
      const frameResource = this._createResourceFromFramePayload(
          framePayload, framePayload.url, Common.ResourceType.resourceTypes.Document, framePayload.mimeType, null,
          null);
      frame.addResource(frameResource);
    }
  }

  /**
   * @param {!Protocol.Page.Frame} frame
   * @param {string} url
   * @param {!Common.ResourceType.ResourceType} type
   * @param {string} mimeType
   * @param {?number} lastModifiedTime
   * @param {?number} contentSize
   * @return {!Resource}
   */
  _createResourceFromFramePayload(frame, url, type, mimeType, lastModifiedTime, contentSize) {
    const lastModified = typeof lastModifiedTime === 'number' ? new Date(lastModifiedTime * 1000) : null;
    return new Resource(
        this, null, url, frame.url, frame.id, frame.loaderId, type, mimeType, lastModified, contentSize);
  }

  suspendReload() {
    this._reloadSuspensionCount++;
  }

  resumeReload() {
    this._reloadSuspensionCount--;
    console.assert(this._reloadSuspensionCount >= 0, 'Unbalanced call to ResourceTreeModel.resumeReload()');
    if (!this._reloadSuspensionCount && this._pendingReloadOptions) {
      this.reloadPage.apply(this, this._pendingReloadOptions);
    }
  }

  /**
   * @param {boolean=} bypassCache
   * @param {string=} scriptToEvaluateOnLoad
   */
  reloadPage(bypassCache, scriptToEvaluateOnLoad) {
    // Only dispatch PageReloadRequested upon first reload request to simplify client logic.
    if (!this._pendingReloadOptions) {
      this.dispatchEventToListeners(Events.PageReloadRequested, this);
    }
    if (this._reloadSuspensionCount) {
      this._pendingReloadOptions = [bypassCache, scriptToEvaluateOnLoad];
      return;
    }
    this._pendingReloadOptions = null;
    this.dispatchEventToListeners(Events.WillReloadPage);
    this._agent.reload(bypassCache, scriptToEvaluateOnLoad);
  }

  /**
   * @param {string} url
   * @return {!Promise}
   */
  navigate(url) {
    return this._agent.navigate(url);
  }

  /**
   * @return {!Promise}>}
   */
  async navigationHistory() {
    const response = await this._agent.invoke_getNavigationHistory({});
    if (response[ProtocolClient.InspectorBackend.ProtocolError]) {
      return null;
    }
    return {currentIndex: response.currentIndex, entries: response.entries};
  }

  /**
   * @param {!Protocol.Page.NavigationEntry} entry
   */
  navigateToHistoryEntry(entry) {
    this._agent.navigateToHistoryEntry(entry.id);
  }

  /**
   * @return {!Promise<{url: string, data: ?string, errors: !Array}>}
   */
  async fetchAppManifest() {
    const response = await this._agent.invoke_getAppManifest({});
    if (response[ProtocolClient.InspectorBackend.ProtocolError]) {
      return {url: response.url, data: null, errors: []};
    }
    return {url: response.url, data: response.data || null, errors: response.errors};
  }

  /**
   * @return {!Promise>}
   */
  async getInstallabilityErrors() {
    const response = await this._agent.invoke_getInstallabilityErrors({});
    return response.installabilityErrors || [];
  }

  /**
   * @return {!Promise<{primaryIcon: ?string}>}
   */
  async getManifestIcons() {
    const response = await this._agent.invoke_getManifestIcons({});
    return {primaryIcon: response.primaryIcon || null};
  }

  /**
   * @param {!ExecutionContext} a
   * @param {!ExecutionContext} b
   * @return {number}
   */
  _executionContextComparator(a, b) {
    /**
     * @param {!ResourceTreeFrame} frame
     * @return {!Array}
     */
    function framePath(frame) {
      let currentFrame = frame;
      const parents = [];
      while (currentFrame) {
        parents.push(currentFrame);
        currentFrame = currentFrame.parentFrame;
      }
      return parents.reverse();
    }

    if (a.target() !== b.target()) {
      return ExecutionContext.comparator(a, b);
    }

    const framesA = a.frameId ? framePath(this.frameForId(a.frameId)) : [];
    const framesB = b.frameId ? framePath(this.frameForId(b.frameId)) : [];
    let frameA;
    let frameB;
    for (let i = 0;; i++) {
      if (!framesA[i] || !framesB[i] || (framesA[i] !== framesB[i])) {
        frameA = framesA[i];
        frameB = framesB[i];
        break;
      }
    }
    if (!frameA && frameB) {
      return -1;
    }

    if (!frameB && frameA) {
      return 1;
    }

    if (frameA && frameB) {
      return frameA.id.localeCompare(frameB.id);
    }

    return ExecutionContext.comparator(a, b);
  }

  /**
   * @return {!SecurityOriginData}
   */
  _getSecurityOriginData() {
    /** @type {!Set} */
    const securityOrigins = new Set();

    let mainSecurityOrigin = null;
    let unreachableMainSecurityOrigin = null;
    for (const frame of this._frames.values()) {
      const origin = frame.securityOrigin;
      if (!origin) {
        continue;
      }

      securityOrigins.add(origin);
      if (frame.isMainFrame()) {
        mainSecurityOrigin = origin;
        if (frame.unreachableUrl()) {
          const unreachableParsed = new Common.ParsedURL.ParsedURL(frame.unreachableUrl());
          unreachableMainSecurityOrigin = unreachableParsed.securityOrigin();
        }
      }
    }
    return {
      securityOrigins: securityOrigins,
      mainSecurityOrigin: mainSecurityOrigin,
      unreachableMainSecurityOrigin: unreachableMainSecurityOrigin
    };
  }

  _updateSecurityOrigins() {
    const data = this._getSecurityOriginData();
    this._securityOriginManager.setMainSecurityOrigin(
        data.mainSecurityOrigin || '', data.unreachableMainSecurityOrigin || '');
    this._securityOriginManager.updateSecurityOrigins(data.securityOrigins);
  }

  /**
   * @return {?string}
   */
  getMainSecurityOrigin() {
    const data = this._getSecurityOriginData();
    return data.mainSecurityOrigin || data.unreachableMainSecurityOrigin;
  }
}

/** @enum {symbol} */
export const Events = {
  FrameAdded: Symbol('FrameAdded'),
  FrameNavigated: Symbol('FrameNavigated'),
  FrameDetached: Symbol('FrameDetached'),
  FrameResized: Symbol('FrameResized'),
  FrameWillNavigate: Symbol('FrameWillNavigate'),
  MainFrameNavigated: Symbol('MainFrameNavigated'),
  ResourceAdded: Symbol('ResourceAdded'),
  WillLoadCachedResources: Symbol('WillLoadCachedResources'),
  CachedResourcesLoaded: Symbol('CachedResourcesLoaded'),
  DOMContentLoaded: Symbol('DOMContentLoaded'),
  LifecycleEvent: Symbol('LifecycleEvent'),
  Load: Symbol('Load'),
  PageReloadRequested: Symbol('PageReloadRequested'),
  WillReloadPage: Symbol('WillReloadPage'),
  InterstitialShown: Symbol('InterstitialShown'),
  InterstitialHidden: Symbol('InterstitialHidden')
};

/**
 * @unrestricted
 */
export class ResourceTreeFrame {
  /**
   * @param {!ResourceTreeModel} model
   * @param {?ResourceTreeFrame} parentFrame
   * @param {!Protocol.Page.FrameId} frameId
   * @param {?Protocol.Page.Frame} payload
   * @param {?Protocol.Runtime.StackTrace} creationStackTrace
   */
  constructor(model, parentFrame, frameId, payload, creationStackTrace) {
    this._model = model;
    this._parentFrame = parentFrame;
    this._id = frameId;
    this._url = '';
    this._crossTargetParentFrameId = null;

    if (payload) {
      this._loaderId = payload.loaderId;
      this._name = payload.name;
      this._url = payload.url;
      this._securityOrigin = payload.securityOrigin;
      this._mimeType = payload.mimeType;
      this._unreachableUrl = payload.unreachableUrl || '';
    }

    this._creationStackTrace = creationStackTrace;

    /**
     * @type {!Set.}
     */
    this._childFrames = new Set();

    /**
     * @type {!Object.}
     */
    this._resourcesMap = {};

    if (this._parentFrame) {
      this._parentFrame._childFrames.add(this);
    }
  }


  /**
   * @param {!Protocol.Page.Frame} framePayload
   */
  _navigate(framePayload) {
    this._loaderId = framePayload.loaderId;
    this._name = framePayload.name;
    this._url = framePayload.url;
    this._securityOrigin = framePayload.securityOrigin;
    this._mimeType = framePayload.mimeType;
    this._unreachableUrl = framePayload.unreachableUrl || '';
    const mainResource = this._resourcesMap[this._url];
    this._resourcesMap = {};
    this._removeChildFrames();
    if (mainResource && mainResource.loaderId === this._loaderId) {
      this.addResource(mainResource);
    }
  }

  /**
   * @return {!ResourceTreeModel}
   */
  resourceTreeModel() {
    return this._model;
  }

  /**
   * @return {string}
   */
  get id() {
    return this._id;
  }

  /**
   * @return {string}
   */
  get name() {
    return this._name || '';
  }

  /**
   * @return {string}
   */
  get url() {
    return this._url;
  }

  /**
   * @return {string}
   */
  get securityOrigin() {
    return this._securityOrigin;
  }

  /**
   * @return {string}
   */
  unreachableUrl() {
    return this._unreachableUrl;
  }

  /**
   * @return {string}
   */
  get loaderId() {
    return this._loaderId;
  }

  /**
   * @return {?ResourceTreeFrame}
   */
  get parentFrame() {
    return this._parentFrame;
  }

  /**
   * @return {!Array.}
   */
  get childFrames() {
    return [...this._childFrames];
  }

  /**
   * @return {?ResourceTreeFrame}
   */
  crossTargetParentFrame() {
    if (!this._crossTargetParentFrameId) {
      return null;
    }
    if (!this._model.target().parentTarget()) {
      return null;
    }
    const parentModel = this._model.target().parentTarget().model(ResourceTreeModel);
    if (!parentModel) {
      return null;
    }
    // Note that parent model has already processed cached resources:
    // - when parent target was created, we issued getResourceTree call;
    // - strictly after we issued setAutoAttach call;
    // - both of them were handled in renderer in the same order;
    // - cached resource tree got processed on parent model;
    // - child target was created as a result of setAutoAttach call.
    return parentModel._frames.get(this._crossTargetParentFrameId) || null;
  }

  /**
   * @param {function(!Protocol.Runtime.CallFrame):boolean} searchFn
   * @return {?Protocol.Runtime.CallFrame}
   */
  findCreationCallFrame(searchFn) {
    let stackTrace = this._creationStackTrace;
    while (stackTrace) {
      const foundEntry = stackTrace.callFrames.find(searchFn);
      if (foundEntry) {
        return foundEntry;
      }
      stackTrace = this.parent;
    }
    return null;
  }

  /**
   * @return {boolean}
   */
  isMainFrame() {
    return !this._parentFrame;
  }

  isTopFrame() {
    return !this._parentFrame && !this._crossTargetParentFrameId;
  }

  /**
   * @return {!Resource}
   */
  get mainResource() {
    return this._resourcesMap[this._url];
  }

  /**
   * @param {!ResourceTreeFrame} frame
   */
  _removeChildFrame(frame) {
    this._childFrames.delete(frame);
    frame._remove();
  }

  _removeChildFrames() {
    const frames = this._childFrames;
    this._childFrames = new Set();
    for (const frame of frames) {
      frame._remove();
    }
  }

  _remove() {
    this._removeChildFrames();
    this._model._frames.delete(this.id);
    this._model.dispatchEventToListeners(Events.FrameDetached, this);
  }

  /**
   * @param {!Resource} resource
   */
  addResource(resource) {
    if (this._resourcesMap[resource.url] === resource) {
      // Already in the tree, we just got an extra update.
      return;
    }
    this._resourcesMap[resource.url] = resource;
    this._model.dispatchEventToListeners(Events.ResourceAdded, resource);
  }

  /**
   * @param {!NetworkRequest} request
   */
  _addRequest(request) {
    let resource = this._resourcesMap[request.url()];
    if (resource && resource.request === request) {
      // Already in the tree, we just got an extra update.
      return;
    }
    resource = new Resource(
        this._model, request, request.url(), request.documentURL, request.frameId, request.loaderId,
        request.resourceType(), request.mimeType, null, null);
    this._resourcesMap[resource.url] = resource;
    this._model.dispatchEventToListeners(Events.ResourceAdded, resource);
  }

  /**
   * @return {!Array.}
   */
  resources() {
    const result = [];
    for (const url in this._resourcesMap) {
      result.push(this._resourcesMap[url]);
    }
    return result;
  }

  /**
   * @param {string} url
   * @return {?Resource}
   */
  resourceForURL(url) {
    const resource = this._resourcesMap[url];
    if (resource) {
      return resource;
    }
    for (const frame of this._childFrames) {
      const resource = frame.resourceForURL(url);
      if (resource) {
        return resource;
      }
    }
    return null;
  }

  /**
   * @param {function(!Resource)} callback
   * @return {boolean}
   */
  _callForFrameResources(callback) {
    for (const url in this._resourcesMap) {
      if (callback(this._resourcesMap[url])) {
        return true;
      }
    }

    for (const frame of this._childFrames) {
      if (frame._callForFrameResources(callback)) {
        return true;
      }
    }
    return false;
  }

  /**
   * @return {string}
   */
  displayName() {
    if (this.isTopFrame()) {
      return Common.UIString.UIString('top');
    }
    const subtitle = new Common.ParsedURL.ParsedURL(this._url).displayName;
    if (subtitle) {
      if (!this._name) {
        return subtitle;
      }
      return this._name + ' (' + subtitle + ')';
    }
    return Common.UIString.UIString('