gecko-dev/remote/shared/NavigationManager.sys.mjs

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);
};
}