mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-24 05:11:16 +00:00
4cb7df0138
Differential Revision: https://phabricator.services.mozilla.com/D192110
415 lines
14 KiB
JavaScript
415 lines
14 KiB
JavaScript
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
import { EventEmitter } from "resource://gre/modules/EventEmitter.sys.mjs";
|
|
|
|
const lazy = {};
|
|
|
|
ChromeUtils.defineESModuleGetters(lazy, {
|
|
generateUUID: "chrome://remote/content/shared/UUID.sys.mjs",
|
|
Log: "chrome://remote/content/shared/Log.sys.mjs",
|
|
registerNavigationListenerActor:
|
|
"chrome://remote/content/shared/js-window-actors/NavigationListenerActor.sys.mjs",
|
|
TabManager: "chrome://remote/content/shared/TabManager.sys.mjs",
|
|
truncate: "chrome://remote/content/shared/Format.sys.mjs",
|
|
unregisterNavigationListenerActor:
|
|
"chrome://remote/content/shared/js-window-actors/NavigationListenerActor.sys.mjs",
|
|
});
|
|
|
|
ChromeUtils.defineLazyGetter(lazy, "logger", () => lazy.Log.get());
|
|
|
|
/**
|
|
* @typedef {object} BrowsingContextDetails
|
|
* @property {string} browsingContextId - The browsing context id.
|
|
* @property {string} browserId - The id of the Browser owning the browsing
|
|
* context.
|
|
* @property {BrowsingContext=} context - The BrowsingContext itself, if
|
|
* available.
|
|
* @property {boolean} isTopBrowsingContext - Whether the browsing context is
|
|
* top level.
|
|
*/
|
|
|
|
/**
|
|
* @typedef {object} NavigationInfo
|
|
* @property {boolean} finished - Whether the navigation is finished or not.
|
|
* @property {string} navigationId - The UUID for the navigation.
|
|
* @property {string} navigable - The UUID for the navigable.
|
|
* @property {string} url - The target url for the navigation.
|
|
*/
|
|
|
|
/**
|
|
* The NavigationRegistry is responsible for monitoring all navigations happening
|
|
* in the browser.
|
|
*
|
|
* It relies on a JSWindowActor pair called NavigationListener{Parent|Child},
|
|
* found under remote/shared/js-window-actors. As a simple overview, the
|
|
* NavigationListenerChild will monitor navigations in all window globals using
|
|
* content process WebProgressListener, and will forward each relevant update to
|
|
* the NavigationListenerParent
|
|
*
|
|
* The NavigationRegistry singleton holds the map of navigations, from navigable
|
|
* to NavigationInfo. It will also be called by NavigationListenerParent
|
|
* whenever a navigation event happens.
|
|
*
|
|
* This singleton is not exported outside of this class, and consumers instead
|
|
* need to use the NavigationManager class. The NavigationRegistry keeps track
|
|
* of how many NavigationListener instances are currently listening in order to
|
|
* know if the NavigationListenerActor should be registered or not.
|
|
*
|
|
* The NavigationRegistry exposes an API to retrieve the current or last
|
|
* navigation for a given navigable, and also forwards events to notify about
|
|
* navigation updates to individual NavigationManager instances.
|
|
*
|
|
* @class NavigationRegistry
|
|
*/
|
|
class NavigationRegistry extends EventEmitter {
|
|
#managers;
|
|
#navigations;
|
|
#navigationIds;
|
|
|
|
constructor() {
|
|
super();
|
|
|
|
// Set of NavigationManager instances currently used.
|
|
this.#managers = new Set();
|
|
|
|
// Maps navigable to NavigationInfo.
|
|
this.#navigations = new WeakMap();
|
|
|
|
// Maps navigable id to navigation id. Only used to pre-register navigation
|
|
// ids before the actual event is detected.
|
|
this.#navigationIds = new Map();
|
|
}
|
|
|
|
/**
|
|
* Retrieve the last known navigation data for a given browsing context.
|
|
*
|
|
* @param {BrowsingContext} context
|
|
* The browsing context for which the navigation event was recorded.
|
|
* @returns {NavigationInfo|null}
|
|
* The last known navigation data, or null.
|
|
*/
|
|
getNavigationForBrowsingContext(context) {
|
|
if (!lazy.TabManager.isValidCanonicalBrowsingContext(context)) {
|
|
// Bail out if the provided context is not a valid CanonicalBrowsingContext
|
|
// instance.
|
|
return null;
|
|
}
|
|
|
|
const navigable = lazy.TabManager.getNavigableForBrowsingContext(context);
|
|
if (!this.#navigations.has(navigable)) {
|
|
return null;
|
|
}
|
|
|
|
return this.#navigations.get(navigable);
|
|
}
|
|
|
|
/**
|
|
* Start monitoring navigations in all browsing contexts. This will register
|
|
* the NavigationListener JSWindowActor and will initialize them in all
|
|
* existing browsing contexts.
|
|
*/
|
|
startMonitoring(listener) {
|
|
if (this.#managers.size == 0) {
|
|
lazy.registerNavigationListenerActor();
|
|
}
|
|
|
|
this.#managers.add(listener);
|
|
}
|
|
|
|
/**
|
|
* Stop monitoring navigations. This will unregister the NavigationListener
|
|
* JSWindowActor and clear the information collected about navigations so far.
|
|
*/
|
|
stopMonitoring(listener) {
|
|
if (!this.#managers.has(listener)) {
|
|
return;
|
|
}
|
|
|
|
this.#managers.delete(listener);
|
|
if (this.#managers.size == 0) {
|
|
lazy.unregisterNavigationListenerActor();
|
|
// Clear the map.
|
|
this.#navigations = new WeakMap();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Called when a same-document navigation is recorded from the
|
|
* NavigationListener actors.
|
|
*
|
|
* This entry point is only intended to be called from
|
|
* NavigationListenerParent, to avoid setting up observers or listeners,
|
|
* which are unnecessary since NavigationManager has to be a singleton.
|
|
*
|
|
* @param {object} data
|
|
* @param {BrowsingContext} data.context
|
|
* The browsing context for which the navigation event was recorded.
|
|
* @param {string} data.url
|
|
* The URL as string for the navigation.
|
|
* @returns {NavigationInfo}
|
|
* The navigation created for this same-document navigation.
|
|
*/
|
|
notifyLocationChanged(data) {
|
|
const { contextDetails, url } = data;
|
|
|
|
const context = this.#getContextFromContextDetails(contextDetails);
|
|
const navigable = lazy.TabManager.getNavigableForBrowsingContext(context);
|
|
const navigableId = lazy.TabManager.getIdForBrowsingContext(context);
|
|
|
|
const navigationId = this.#getOrCreateNavigationId(navigableId);
|
|
const navigation = { finished: true, navigationId, url };
|
|
this.#navigations.set(navigable, navigation);
|
|
|
|
// Same document navigations are immediately done, fire a single event.
|
|
this.emit("location-changed", { navigationId, navigableId, url });
|
|
|
|
return navigation;
|
|
}
|
|
|
|
/**
|
|
* Called when a navigation-started event is recorded from the
|
|
* NavigationListener actors.
|
|
*
|
|
* This entry point is only intended to be called from
|
|
* NavigationListenerParent, to avoid setting up observers or listeners,
|
|
* which are unnecessary since NavigationManager has to be a singleton.
|
|
*
|
|
* @param {object} data
|
|
* @param {BrowsingContextDetails} data.contextDetails
|
|
* The details about the browsing context for this navigation.
|
|
* @param {string} data.url
|
|
* The URL as string for the navigation.
|
|
* @returns {NavigationInfo}
|
|
* The created navigation or the ongoing navigation, if applicable.
|
|
*/
|
|
notifyNavigationStarted(data) {
|
|
const { contextDetails, url } = data;
|
|
|
|
const context = this.#getContextFromContextDetails(contextDetails);
|
|
const navigable = lazy.TabManager.getNavigableForBrowsingContext(context);
|
|
const navigableId = lazy.TabManager.getIdForBrowsingContext(context);
|
|
|
|
let navigation = this.#navigations.get(navigable);
|
|
if (navigation && !navigation.finished) {
|
|
// If we are already monitoring a navigation for this navigable, for which
|
|
// we did not receive a navigation-stopped event, this navigation
|
|
// is already tracked and we don't want to create another id & event.
|
|
lazy.logger.trace(
|
|
`[${navigableId}] Skipping already tracked navigation, navigationId: ${navigation.navigationId}`
|
|
);
|
|
return navigation;
|
|
}
|
|
|
|
const navigationId = this.#getOrCreateNavigationId(navigableId);
|
|
navigation = { finished: false, navigationId, url };
|
|
this.#navigations.set(navigable, navigation);
|
|
|
|
lazy.logger.trace(
|
|
lazy.truncate`[${navigableId}] Navigation started for url: ${url} (${navigationId})`
|
|
);
|
|
|
|
this.emit("navigation-started", { navigationId, navigableId, url });
|
|
|
|
return navigation;
|
|
}
|
|
|
|
/**
|
|
* Called when a navigation-stopped event is recorded from the
|
|
* NavigationListener actors.
|
|
*
|
|
* @param {object} data
|
|
* @param {BrowsingContextDetails} data.contextDetails
|
|
* The details about the browsing context for this navigation.
|
|
* @param {string} data.url
|
|
* The URL as string for the navigation.
|
|
* @returns {NavigationInfo}
|
|
* The stopped navigation if any, or null.
|
|
*/
|
|
notifyNavigationStopped(data) {
|
|
const { contextDetails, url } = data;
|
|
|
|
const context = this.#getContextFromContextDetails(contextDetails);
|
|
const navigable = lazy.TabManager.getNavigableForBrowsingContext(context);
|
|
const navigableId = lazy.TabManager.getIdForBrowsingContext(context);
|
|
|
|
const navigation = this.#navigations.get(navigable);
|
|
if (!navigation) {
|
|
lazy.logger.trace(
|
|
lazy.truncate`[${navigableId}] No navigation found to stop for url: ${url}`
|
|
);
|
|
return null;
|
|
}
|
|
|
|
if (navigation.finished) {
|
|
lazy.logger.trace(
|
|
`[${navigableId}] Navigation already marked as finished, navigationId: ${navigation.navigationId}`
|
|
);
|
|
return navigation;
|
|
}
|
|
|
|
lazy.logger.trace(
|
|
lazy.truncate`[${navigableId}] Navigation finished for url: ${url} (${navigation.navigationId})`
|
|
);
|
|
|
|
navigation.finished = true;
|
|
|
|
this.emit("navigation-stopped", {
|
|
navigationId: navigation.navigationId,
|
|
navigableId,
|
|
url,
|
|
});
|
|
|
|
return navigation;
|
|
}
|
|
|
|
/**
|
|
* Register a navigation id to be used for the next navigation for the
|
|
* provided browsing context details.
|
|
*
|
|
* @param {object} data
|
|
* @param {BrowsingContextDetails} data.contextDetails
|
|
* The details about the browsing context for this navigation.
|
|
* @returns {string}
|
|
* The UUID created the upcoming navigation.
|
|
*/
|
|
registerNavigationId(data) {
|
|
const { contextDetails } = data;
|
|
const context = this.#getContextFromContextDetails(contextDetails);
|
|
const navigableId = lazy.TabManager.getIdForBrowsingContext(context);
|
|
|
|
const navigationId = lazy.generateUUID();
|
|
this.#navigationIds.set(navigableId, navigationId);
|
|
|
|
return navigationId;
|
|
}
|
|
|
|
#getContextFromContextDetails(contextDetails) {
|
|
if (contextDetails.context) {
|
|
return contextDetails.context;
|
|
}
|
|
|
|
return contextDetails.isTopBrowsingContext
|
|
? BrowsingContext.getCurrentTopByBrowserId(contextDetails.browserId)
|
|
: BrowsingContext.get(contextDetails.browsingContextId);
|
|
}
|
|
|
|
#getOrCreateNavigationId(navigableId) {
|
|
let navigationId;
|
|
if (this.#navigationIds.has(navigableId)) {
|
|
navigationId = this.#navigationIds.get(navigableId, navigationId);
|
|
this.#navigationIds.delete(navigableId);
|
|
} else {
|
|
navigationId = lazy.generateUUID();
|
|
}
|
|
return navigationId;
|
|
}
|
|
}
|
|
|
|
// Create a private NavigationRegistry singleton.
|
|
const navigationRegistry = new NavigationRegistry();
|
|
|
|
/**
|
|
* See NavigationRegistry.notifyLocationChanged.
|
|
*
|
|
* This entry point is only intended to be called from NavigationListenerParent,
|
|
* to avoid setting up observers or listeners, which are unnecessary since
|
|
* NavigationRegistry has to be a singleton.
|
|
*/
|
|
export function notifyLocationChanged(data) {
|
|
return navigationRegistry.notifyLocationChanged(data);
|
|
}
|
|
|
|
/**
|
|
* See NavigationRegistry.notifyNavigationStarted.
|
|
*
|
|
* This entry point is only intended to be called from NavigationListenerParent,
|
|
* to avoid setting up observers or listeners, which are unnecessary since
|
|
* NavigationRegistry has to be a singleton.
|
|
*/
|
|
export function notifyNavigationStarted(data) {
|
|
return navigationRegistry.notifyNavigationStarted(data);
|
|
}
|
|
|
|
/**
|
|
* See NavigationRegistry.notifyNavigationStopped.
|
|
*
|
|
* This entry point is only intended to be called from NavigationListenerParent,
|
|
* to avoid setting up observers or listeners, which are unnecessary since
|
|
* NavigationRegistry has to be a singleton.
|
|
*/
|
|
export function notifyNavigationStopped(data) {
|
|
return navigationRegistry.notifyNavigationStopped(data);
|
|
}
|
|
|
|
export function registerNavigationId(data) {
|
|
return navigationRegistry.registerNavigationId(data);
|
|
}
|
|
|
|
/**
|
|
* The NavigationManager exposes the NavigationRegistry data via a class which
|
|
* needs to be individually instantiated by each consumer. This allow to track
|
|
* how many consumers need navigation data at any point so that the
|
|
* NavigationRegistry can register or unregister the underlying JSWindowActors
|
|
* correctly.
|
|
*
|
|
* @fires navigation-started
|
|
* The NavigationManager emits "navigation-started" when a new navigation is
|
|
* detected, with the following object as payload:
|
|
* - {string} navigationId - The UUID for the navigation.
|
|
* - {string} navigableId - The UUID for the navigable.
|
|
* - {string} url - The target url for the navigation.
|
|
* @fires navigation-stopped
|
|
* The NavigationManager emits "navigation-stopped" when a known navigation
|
|
* is stopped, with the following object as payload:
|
|
* - {string} navigationId - The UUID for the navigation.
|
|
* - {string} navigableId - The UUID for the navigable.
|
|
* - {string} url - The target url for the navigation.
|
|
*/
|
|
export class NavigationManager extends EventEmitter {
|
|
#monitoring;
|
|
|
|
constructor() {
|
|
super();
|
|
|
|
this.#monitoring = false;
|
|
}
|
|
|
|
destroy() {
|
|
this.stopMonitoring();
|
|
}
|
|
|
|
getNavigationForBrowsingContext(context) {
|
|
return navigationRegistry.getNavigationForBrowsingContext(context);
|
|
}
|
|
|
|
startMonitoring() {
|
|
if (this.#monitoring) {
|
|
return;
|
|
}
|
|
|
|
this.#monitoring = true;
|
|
navigationRegistry.startMonitoring(this);
|
|
navigationRegistry.on("navigation-started", this.#onNavigationEvent);
|
|
navigationRegistry.on("location-changed", this.#onNavigationEvent);
|
|
navigationRegistry.on("navigation-stopped", this.#onNavigationEvent);
|
|
}
|
|
|
|
stopMonitoring() {
|
|
if (!this.#monitoring) {
|
|
return;
|
|
}
|
|
|
|
this.#monitoring = false;
|
|
navigationRegistry.stopMonitoring(this);
|
|
navigationRegistry.off("navigation-started", this.#onNavigationEvent);
|
|
navigationRegistry.off("location-changed", this.#onNavigationEvent);
|
|
navigationRegistry.off("navigation-stopped", this.#onNavigationEvent);
|
|
}
|
|
|
|
#onNavigationEvent = (eventName, data) => {
|
|
this.emit(eventName, data);
|
|
};
|
|
}
|