Backed out 2 changesets (bug 1855992) for causing talos failures @ mozilla::net:...
[gecko.git] / remote / shared / NavigationManager.sys.mjs
blobca9bf186ea5ecdabb9f7538f65a0ce53f1a4c98d
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";
7 const lazy = {};
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",
18 });
20 ChromeUtils.defineLazyGetter(lazy, "logger", () => lazy.Log.get());
22 /**
23  * @typedef {object} BrowsingContextDetails
24  * @property {string} browsingContextId - The browsing context id.
25  * @property {string} browserId - The id of the Browser owning the browsing
26  *     context.
27  * @property {BrowsingContext=} context - The BrowsingContext itself, if
28  *     available.
29  * @property {boolean} isTopBrowsingContext - Whether the browsing context is
30  *     top level.
31  */
33 /**
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.
39  */
41 /**
42  * The NavigationRegistry is responsible for monitoring all navigations happening
43  * in the browser.
44  *
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
50  *
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.
54  *
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.
59  *
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.
63  *
64  * @class NavigationRegistry
65  */
66 class NavigationRegistry extends EventEmitter {
67   #managers;
68   #navigations;
70   constructor() {
71     super();
73     // Set of NavigationManager instances currently used.
74     this.#managers = new Set();
76     // Maps navigable to NavigationInfo.
77     this.#navigations = new WeakMap();
78   }
80   /**
81    * Retrieve the last known navigation data for a given browsing context.
82    *
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.
87    */
88   getNavigationForBrowsingContext(context) {
89     if (!lazy.TabManager.isValidCanonicalBrowsingContext(context)) {
90       // Bail out if the provided context is not a valid CanonicalBrowsingContext
91       // instance.
92       return null;
93     }
95     const navigable = lazy.TabManager.getNavigableForBrowsingContext(context);
96     if (!this.#navigations.has(navigable)) {
97       return null;
98     }
100     return this.#navigations.get(navigable);
101   }
103   /**
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.
107    */
108   startMonitoring(listener) {
109     if (this.#managers.size == 0) {
110       lazy.registerNavigationListenerActor();
111     }
113     this.#managers.add(listener);
114   }
116   /**
117    * Stop monitoring navigations. This will unregister the NavigationListener
118    * JSWindowActor and clear the information collected about navigations so far.
119    */
120   stopMonitoring(listener) {
121     if (!this.#managers.has(listener)) {
122       return;
123     }
125     this.#managers.delete(listener);
126     if (this.#managers.size == 0) {
127       lazy.unregisterNavigationListenerActor();
128       // Clear the map.
129       this.#navigations = new WeakMap();
130     }
131   }
133   /**
134    * Called when a same-document navigation is recorded from the
135    * NavigationListener actors.
136    *
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.
140    *
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.
148    */
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 });
163     return navigation;
164   }
166   /**
167    * Called when a navigation-started event is recorded from the
168    * NavigationListener actors.
169    *
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.
173    *
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.
181    */
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.
194       lazy.logger.trace(
195         `[${navigableId}] Skipping already tracked navigation, navigationId: ${navigation.navigationId}`
196       );
197       return navigation;
198     }
200     const navigationId = lazy.generateUUID();
201     lazy.logger.trace(
202       lazy.truncate`[${navigableId}] Navigation started for url: ${url} (${navigationId})`
203     );
205     navigation = { finished: false, navigationId, url };
206     this.#navigations.set(navigable, navigation);
208     this.emit("navigation-started", { navigationId, navigableId, url });
210     return navigation;
211   }
213   /**
214    * Called when a navigation-stopped event is recorded from the
215    * NavigationListener actors.
216    *
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.
224    */
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);
233     if (!navigation) {
234       lazy.logger.trace(
235         lazy.truncate`[${navigableId}] No navigation found to stop for url: ${url}`
236       );
237       return null;
238     }
240     if (navigation.finished) {
241       lazy.logger.trace(
242         `[${navigableId}] Navigation already marked as finished, navigationId: ${navigation.navigationId}`
243       );
244       return navigation;
245     }
247     lazy.logger.trace(
248       lazy.truncate`[${navigableId}] Navigation finished for url: ${url} (${navigation.navigationId})`
249     );
251     navigation.finished = true;
253     this.emit("navigation-stopped", {
254       navigationId: navigation.navigationId,
255       navigableId,
256       url,
257     });
259     return navigation;
260   }
262   #getContextFromContextDetails(contextDetails) {
263     if (contextDetails.context) {
264       return contextDetails.context;
265     }
267     return contextDetails.isTopBrowsingContext
268       ? BrowsingContext.getCurrentTopByBrowserId(contextDetails.browserId)
269       : BrowsingContext.get(contextDetails.browsingContextId);
270   }
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.
282  */
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.
293  */
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.
304  */
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
314  * correctly.
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.
328  */
329 export class NavigationManager extends EventEmitter {
330   #monitoring;
332   constructor() {
333     super();
335     this.#monitoring = false;
336   }
338   destroy() {
339     this.stopMonitoring();
340   }
342   getNavigationForBrowsingContext(context) {
343     return navigationRegistry.getNavigationForBrowsingContext(context);
344   }
346   startMonitoring() {
347     if (this.#monitoring) {
348       return;
349     }
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);
356   }
358   stopMonitoring() {
359     if (!this.#monitoring) {
360       return;
361     }
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);
368   }
370   #onNavigationEvent = (eventName, data) => {
371     this.emit(eventName, data);
372   };