1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 import { EventEmitter } from "resource://gre/modules/EventEmitter.sys.mjs";
9 ChromeUtils.defineESModuleGetters(lazy, {
10 generateUUID: "chrome://remote/content/shared/UUID.sys.mjs",
11 Log: "chrome://remote/content/shared/Log.sys.mjs",
12 registerNavigationListenerActor:
13 "chrome://remote/content/shared/js-window-actors/NavigationListenerActor.sys.mjs",
14 TabManager: "chrome://remote/content/shared/TabManager.sys.mjs",
15 truncate: "chrome://remote/content/shared/Format.sys.mjs",
16 unregisterNavigationListenerActor:
17 "chrome://remote/content/shared/js-window-actors/NavigationListenerActor.sys.mjs",
20 ChromeUtils.defineLazyGetter(lazy, "logger", () => lazy.Log.get());
23 * @typedef {object} BrowsingContextDetails
24 * @property {string} browsingContextId - The browsing context id.
25 * @property {string} browserId - The id of the Browser owning the browsing
27 * @property {BrowsingContext=} context - The BrowsingContext itself, if
29 * @property {boolean} isTopBrowsingContext - Whether the browsing context is
34 * @typedef {object} NavigationInfo
35 * @property {boolean} finished - Whether the navigation is finished or not.
36 * @property {string} navigationId - The UUID for the navigation.
37 * @property {string} navigable - The UUID for the navigable.
38 * @property {string} url - The target url for the navigation.
42 * The NavigationRegistry is responsible for monitoring all navigations happening
45 * It relies on a JSWindowActor pair called NavigationListener{Parent|Child},
46 * found under remote/shared/js-window-actors. As a simple overview, the
47 * NavigationListenerChild will monitor navigations in all window globals using
48 * content process WebProgressListener, and will forward each relevant update to
49 * the NavigationListenerParent
51 * The NavigationRegistry singleton holds the map of navigations, from navigable
52 * to NavigationInfo. It will also be called by NavigationListenerParent
53 * whenever a navigation event happens.
55 * This singleton is not exported outside of this class, and consumers instead
56 * need to use the NavigationManager class. The NavigationRegistry keeps track
57 * of how many NavigationListener instances are currently listening in order to
58 * know if the NavigationListenerActor should be registered or not.
60 * The NavigationRegistry exposes an API to retrieve the current or last
61 * navigation for a given navigable, and also forwards events to notify about
62 * navigation updates to individual NavigationManager instances.
64 * @class NavigationRegistry
66 class NavigationRegistry extends EventEmitter {
73 // Set of NavigationManager instances currently used.
74 this.#managers = new Set();
76 // Maps navigable to NavigationInfo.
77 this.#navigations = new WeakMap();
81 * Retrieve the last known navigation data for a given browsing context.
83 * @param {BrowsingContext} context
84 * The browsing context for which the navigation event was recorded.
85 * @returns {NavigationInfo|null}
86 * The last known navigation data, or null.
88 getNavigationForBrowsingContext(context) {
89 if (!lazy.TabManager.isValidCanonicalBrowsingContext(context)) {
90 // Bail out if the provided context is not a valid CanonicalBrowsingContext
95 const navigable = lazy.TabManager.getNavigableForBrowsingContext(context);
96 if (!this.#navigations.has(navigable)) {
100 return this.#navigations.get(navigable);
104 * Start monitoring navigations in all browsing contexts. This will register
105 * the NavigationListener JSWindowActor and will initialize them in all
106 * existing browsing contexts.
108 startMonitoring(listener) {
109 if (this.#managers.size == 0) {
110 lazy.registerNavigationListenerActor();
113 this.#managers.add(listener);
117 * Stop monitoring navigations. This will unregister the NavigationListener
118 * JSWindowActor and clear the information collected about navigations so far.
120 stopMonitoring(listener) {
121 if (!this.#managers.has(listener)) {
125 this.#managers.delete(listener);
126 if (this.#managers.size == 0) {
127 lazy.unregisterNavigationListenerActor();
129 this.#navigations = new WeakMap();
134 * Called when a same-document navigation is recorded from the
135 * NavigationListener actors.
137 * This entry point is only intended to be called from
138 * NavigationListenerParent, to avoid setting up observers or listeners,
139 * which are unnecessary since NavigationManager has to be a singleton.
141 * @param {object} data
142 * @param {BrowsingContext} data.context
143 * The browsing context for which the navigation event was recorded.
144 * @param {string} data.url
145 * The URL as string for the navigation.
146 * @returns {NavigationInfo}
147 * The navigation created for this same-document navigation.
149 notifyLocationChanged(data) {
150 const { contextDetails, url } = data;
152 const context = this.#getContextFromContextDetails(contextDetails);
153 const navigable = lazy.TabManager.getNavigableForBrowsingContext(context);
154 const navigableId = lazy.TabManager.getIdForBrowsingContext(context);
156 const navigationId = lazy.generateUUID();
157 const navigation = { finished: true, navigationId, url };
158 this.#navigations.set(navigable, navigation);
160 // Same document navigations are immediately done, fire a single event.
161 this.emit("location-changed", { navigationId, navigableId, url });
167 * Called when a navigation-started event is recorded from the
168 * NavigationListener actors.
170 * This entry point is only intended to be called from
171 * NavigationListenerParent, to avoid setting up observers or listeners,
172 * which are unnecessary since NavigationManager has to be a singleton.
174 * @param {object} data
175 * @param {BrowsingContextDetails} data.contextDetails
176 * The details about the browsing context for this navigation.
177 * @param {string} data.url
178 * The URL as string for the navigation.
179 * @returns {NavigationInfo}
180 * The created navigation or the ongoing navigation, if applicable.
182 notifyNavigationStarted(data) {
183 const { contextDetails, url } = data;
185 const context = this.#getContextFromContextDetails(contextDetails);
186 const navigable = lazy.TabManager.getNavigableForBrowsingContext(context);
187 const navigableId = lazy.TabManager.getIdForBrowsingContext(context);
189 let navigation = this.#navigations.get(navigable);
190 if (navigation && !navigation.finished) {
191 // If we are already monitoring a navigation for this navigable, for which
192 // we did not receive a navigation-stopped event, this navigation
193 // is already tracked and we don't want to create another id & event.
195 `[${navigableId}] Skipping already tracked navigation, navigationId: ${navigation.navigationId}`
200 const navigationId = lazy.generateUUID();
202 lazy.truncate`[${navigableId}] Navigation started for url: ${url} (${navigationId})`
205 navigation = { finished: false, navigationId, url };
206 this.#navigations.set(navigable, navigation);
208 this.emit("navigation-started", { navigationId, navigableId, url });
214 * Called when a navigation-stopped event is recorded from the
215 * NavigationListener actors.
217 * @param {object} data
218 * @param {BrowsingContextDetails} data.contextDetails
219 * The details about the browsing context for this navigation.
220 * @param {string} data.url
221 * The URL as string for the navigation.
222 * @returns {NavigationInfo}
223 * The stopped navigation if any, or null.
225 notifyNavigationStopped(data) {
226 const { contextDetails, url } = data;
228 const context = this.#getContextFromContextDetails(contextDetails);
229 const navigable = lazy.TabManager.getNavigableForBrowsingContext(context);
230 const navigableId = lazy.TabManager.getIdForBrowsingContext(context);
232 const navigation = this.#navigations.get(navigable);
235 lazy.truncate`[${navigableId}] No navigation found to stop for url: ${url}`
240 if (navigation.finished) {
242 `[${navigableId}] Navigation already marked as finished, navigationId: ${navigation.navigationId}`
248 lazy.truncate`[${navigableId}] Navigation finished for url: ${url} (${navigation.navigationId})`
251 navigation.finished = true;
253 this.emit("navigation-stopped", {
254 navigationId: navigation.navigationId,
262 #getContextFromContextDetails(contextDetails) {
263 if (contextDetails.context) {
264 return contextDetails.context;
267 return contextDetails.isTopBrowsingContext
268 ? BrowsingContext.getCurrentTopByBrowserId(contextDetails.browserId)
269 : BrowsingContext.get(contextDetails.browsingContextId);
273 // Create a private NavigationRegistry singleton.
274 const navigationRegistry = new NavigationRegistry();
277 * See NavigationRegistry.notifyLocationChanged.
279 * This entry point is only intended to be called from NavigationListenerParent,
280 * to avoid setting up observers or listeners, which are unnecessary since
281 * NavigationRegistry has to be a singleton.
283 export function notifyLocationChanged(data) {
284 return navigationRegistry.notifyLocationChanged(data);
288 * See NavigationRegistry.notifyNavigationStarted.
290 * This entry point is only intended to be called from NavigationListenerParent,
291 * to avoid setting up observers or listeners, which are unnecessary since
292 * NavigationRegistry has to be a singleton.
294 export function notifyNavigationStarted(data) {
295 return navigationRegistry.notifyNavigationStarted(data);
299 * See NavigationRegistry.notifyNavigationStopped.
301 * This entry point is only intended to be called from NavigationListenerParent,
302 * to avoid setting up observers or listeners, which are unnecessary since
303 * NavigationRegistry has to be a singleton.
305 export function notifyNavigationStopped(data) {
306 return navigationRegistry.notifyNavigationStopped(data);
310 * The NavigationManager exposes the NavigationRegistry data via a class which
311 * needs to be individually instantiated by each consumer. This allow to track
312 * how many consumers need navigation data at any point so that the
313 * NavigationRegistry can register or unregister the underlying JSWindowActors
316 * @fires navigation-started
317 * The NavigationManager emits "navigation-started" when a new navigation is
318 * detected, with the following object as payload:
319 * - {string} navigationId - The UUID for the navigation.
320 * - {string} navigableId - The UUID for the navigable.
321 * - {string} url - The target url for the navigation.
322 * @fires navigation-stopped
323 * The NavigationManager emits "navigation-stopped" when a known navigation
324 * is stopped, with the following object as payload:
325 * - {string} navigationId - The UUID for the navigation.
326 * - {string} navigableId - The UUID for the navigable.
327 * - {string} url - The target url for the navigation.
329 export class NavigationManager extends EventEmitter {
335 this.#monitoring = false;
339 this.stopMonitoring();
342 getNavigationForBrowsingContext(context) {
343 return navigationRegistry.getNavigationForBrowsingContext(context);
347 if (this.#monitoring) {
351 this.#monitoring = true;
352 navigationRegistry.startMonitoring(this);
353 navigationRegistry.on("navigation-started", this.#onNavigationEvent);
354 navigationRegistry.on("location-changed", this.#onNavigationEvent);
355 navigationRegistry.on("navigation-stopped", this.#onNavigationEvent);
359 if (!this.#monitoring) {
363 this.#monitoring = false;
364 navigationRegistry.stopMonitoring(this);
365 navigationRegistry.off("navigation-started", this.#onNavigationEvent);
366 navigationRegistry.off("location-changed", this.#onNavigationEvent);
367 navigationRegistry.off("navigation-stopped", this.#onNavigationEvent);
370 #onNavigationEvent = (eventName, data) => {
371 this.emit(eventName, data);