Bug 1869043 add a main thread record of track audio outputs r=padenot
[gecko.git] / remote / shared / NavigationManager.sys.mjs
blob1f19ef3c0df65e6c10f9cbf81c6eea71ef590324
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;
69   #navigationIds;
71   constructor() {
72     super();
74     // Set of NavigationManager instances currently used.
75     this.#managers = new Set();
77     // Maps navigable to NavigationInfo.
78     this.#navigations = new WeakMap();
80     // Maps navigable id to navigation id. Only used to pre-register navigation
81     // ids before the actual event is detected.
82     this.#navigationIds = new Map();
83   }
85   /**
86    * Retrieve the last known navigation data for a given browsing context.
87    *
88    * @param {BrowsingContext} context
89    *     The browsing context for which the navigation event was recorded.
90    * @returns {NavigationInfo|null}
91    *     The last known navigation data, or null.
92    */
93   getNavigationForBrowsingContext(context) {
94     if (!lazy.TabManager.isValidCanonicalBrowsingContext(context)) {
95       // Bail out if the provided context is not a valid CanonicalBrowsingContext
96       // instance.
97       return null;
98     }
100     const navigable = lazy.TabManager.getNavigableForBrowsingContext(context);
101     if (!this.#navigations.has(navigable)) {
102       return null;
103     }
105     return this.#navigations.get(navigable);
106   }
108   /**
109    * Start monitoring navigations in all browsing contexts. This will register
110    * the NavigationListener JSWindowActor and will initialize them in all
111    * existing browsing contexts.
112    */
113   startMonitoring(listener) {
114     if (this.#managers.size == 0) {
115       lazy.registerNavigationListenerActor();
116     }
118     this.#managers.add(listener);
119   }
121   /**
122    * Stop monitoring navigations. This will unregister the NavigationListener
123    * JSWindowActor and clear the information collected about navigations so far.
124    */
125   stopMonitoring(listener) {
126     if (!this.#managers.has(listener)) {
127       return;
128     }
130     this.#managers.delete(listener);
131     if (this.#managers.size == 0) {
132       lazy.unregisterNavigationListenerActor();
133       // Clear the map.
134       this.#navigations = new WeakMap();
135     }
136   }
138   /**
139    * Called when a same-document navigation is recorded from the
140    * NavigationListener actors.
141    *
142    * This entry point is only intended to be called from
143    * NavigationListenerParent, to avoid setting up observers or listeners,
144    * which are unnecessary since NavigationManager has to be a singleton.
145    *
146    * @param {object} data
147    * @param {BrowsingContext} data.context
148    *     The browsing context for which the navigation event was recorded.
149    * @param {string} data.url
150    *     The URL as string for the navigation.
151    * @returns {NavigationInfo}
152    *     The navigation created for this same-document navigation.
153    */
154   notifyLocationChanged(data) {
155     const { contextDetails, url } = data;
157     const context = this.#getContextFromContextDetails(contextDetails);
158     const navigable = lazy.TabManager.getNavigableForBrowsingContext(context);
159     const navigableId = lazy.TabManager.getIdForBrowsingContext(context);
161     const navigationId = this.#getOrCreateNavigationId(navigableId);
162     const navigation = { finished: true, navigationId, url };
163     this.#navigations.set(navigable, navigation);
165     // Same document navigations are immediately done, fire a single event.
166     this.emit("location-changed", { navigationId, navigableId, url });
168     return navigation;
169   }
171   /**
172    * Called when a navigation-started event is recorded from the
173    * NavigationListener actors.
174    *
175    * This entry point is only intended to be called from
176    * NavigationListenerParent, to avoid setting up observers or listeners,
177    * which are unnecessary since NavigationManager has to be a singleton.
178    *
179    * @param {object} data
180    * @param {BrowsingContextDetails} data.contextDetails
181    *     The details about the browsing context for this navigation.
182    * @param {string} data.url
183    *     The URL as string for the navigation.
184    * @returns {NavigationInfo}
185    *     The created navigation or the ongoing navigation, if applicable.
186    */
187   notifyNavigationStarted(data) {
188     const { contextDetails, url } = data;
190     const context = this.#getContextFromContextDetails(contextDetails);
191     const navigable = lazy.TabManager.getNavigableForBrowsingContext(context);
192     const navigableId = lazy.TabManager.getIdForBrowsingContext(context);
194     let navigation = this.#navigations.get(navigable);
195     if (navigation && !navigation.finished) {
196       // If we are already monitoring a navigation for this navigable, for which
197       // we did not receive a navigation-stopped event, this navigation
198       // is already tracked and we don't want to create another id & event.
199       lazy.logger.trace(
200         `[${navigableId}] Skipping already tracked navigation, navigationId: ${navigation.navigationId}`
201       );
202       return navigation;
203     }
205     const navigationId = this.#getOrCreateNavigationId(navigableId);
206     navigation = { finished: false, navigationId, url };
207     this.#navigations.set(navigable, navigation);
209     lazy.logger.trace(
210       lazy.truncate`[${navigableId}] Navigation started for url: ${url} (${navigationId})`
211     );
213     this.emit("navigation-started", { navigationId, navigableId, url });
215     return navigation;
216   }
218   /**
219    * Called when a navigation-stopped event is recorded from the
220    * NavigationListener actors.
221    *
222    * @param {object} data
223    * @param {BrowsingContextDetails} data.contextDetails
224    *     The details about the browsing context for this navigation.
225    * @param {string} data.url
226    *     The URL as string for the navigation.
227    * @returns {NavigationInfo}
228    *     The stopped navigation if any, or null.
229    */
230   notifyNavigationStopped(data) {
231     const { contextDetails, url } = data;
233     const context = this.#getContextFromContextDetails(contextDetails);
234     const navigable = lazy.TabManager.getNavigableForBrowsingContext(context);
235     const navigableId = lazy.TabManager.getIdForBrowsingContext(context);
237     const navigation = this.#navigations.get(navigable);
238     if (!navigation) {
239       lazy.logger.trace(
240         lazy.truncate`[${navigableId}] No navigation found to stop for url: ${url}`
241       );
242       return null;
243     }
245     if (navigation.finished) {
246       lazy.logger.trace(
247         `[${navigableId}] Navigation already marked as finished, navigationId: ${navigation.navigationId}`
248       );
249       return navigation;
250     }
252     lazy.logger.trace(
253       lazy.truncate`[${navigableId}] Navigation finished for url: ${url} (${navigation.navigationId})`
254     );
256     navigation.finished = true;
258     this.emit("navigation-stopped", {
259       navigationId: navigation.navigationId,
260       navigableId,
261       url,
262     });
264     return navigation;
265   }
267   /**
268    * Register a navigation id to be used for the next navigation for the
269    * provided browsing context details.
270    *
271    * @param {object} data
272    * @param {BrowsingContextDetails} data.contextDetails
273    *     The details about the browsing context for this navigation.
274    * @returns {string}
275    *     The UUID created the upcoming navigation.
276    */
277   registerNavigationId(data) {
278     const { contextDetails } = data;
279     const context = this.#getContextFromContextDetails(contextDetails);
280     const navigableId = lazy.TabManager.getIdForBrowsingContext(context);
282     const navigationId = lazy.generateUUID();
283     this.#navigationIds.set(navigableId, navigationId);
285     return navigationId;
286   }
288   #getContextFromContextDetails(contextDetails) {
289     if (contextDetails.context) {
290       return contextDetails.context;
291     }
293     return contextDetails.isTopBrowsingContext
294       ? BrowsingContext.getCurrentTopByBrowserId(contextDetails.browserId)
295       : BrowsingContext.get(contextDetails.browsingContextId);
296   }
298   #getOrCreateNavigationId(navigableId) {
299     let navigationId;
300     if (this.#navigationIds.has(navigableId)) {
301       navigationId = this.#navigationIds.get(navigableId, navigationId);
302       this.#navigationIds.delete(navigableId);
303     } else {
304       navigationId = lazy.generateUUID();
305     }
306     return navigationId;
307   }
310 // Create a private NavigationRegistry singleton.
311 const navigationRegistry = new NavigationRegistry();
314  * See NavigationRegistry.notifyLocationChanged.
316  * This entry point is only intended to be called from NavigationListenerParent,
317  * to avoid setting up observers or listeners, which are unnecessary since
318  * NavigationRegistry has to be a singleton.
319  */
320 export function notifyLocationChanged(data) {
321   return navigationRegistry.notifyLocationChanged(data);
325  * See NavigationRegistry.notifyNavigationStarted.
327  * This entry point is only intended to be called from NavigationListenerParent,
328  * to avoid setting up observers or listeners, which are unnecessary since
329  * NavigationRegistry has to be a singleton.
330  */
331 export function notifyNavigationStarted(data) {
332   return navigationRegistry.notifyNavigationStarted(data);
336  * See NavigationRegistry.notifyNavigationStopped.
338  * This entry point is only intended to be called from NavigationListenerParent,
339  * to avoid setting up observers or listeners, which are unnecessary since
340  * NavigationRegistry has to be a singleton.
341  */
342 export function notifyNavigationStopped(data) {
343   return navigationRegistry.notifyNavigationStopped(data);
346 export function registerNavigationId(data) {
347   return navigationRegistry.registerNavigationId(data);
351  * The NavigationManager exposes the NavigationRegistry data via a class which
352  * needs to be individually instantiated by each consumer. This allow to track
353  * how many consumers need navigation data at any point so that the
354  * NavigationRegistry can register or unregister the underlying JSWindowActors
355  * correctly.
357  * @fires navigation-started
358  *    The NavigationManager emits "navigation-started" when a new navigation is
359  *    detected, with the following object as payload:
360  *      - {string} navigationId - The UUID for the navigation.
361  *      - {string} navigableId - The UUID for the navigable.
362  *      - {string} url - The target url for the navigation.
363  * @fires navigation-stopped
364  *    The NavigationManager emits "navigation-stopped" when a known navigation
365  *    is stopped, with the following object as payload:
366  *      - {string} navigationId - The UUID for the navigation.
367  *      - {string} navigableId - The UUID for the navigable.
368  *      - {string} url - The target url for the navigation.
369  */
370 export class NavigationManager extends EventEmitter {
371   #monitoring;
373   constructor() {
374     super();
376     this.#monitoring = false;
377   }
379   destroy() {
380     this.stopMonitoring();
381   }
383   getNavigationForBrowsingContext(context) {
384     return navigationRegistry.getNavigationForBrowsingContext(context);
385   }
387   startMonitoring() {
388     if (this.#monitoring) {
389       return;
390     }
392     this.#monitoring = true;
393     navigationRegistry.startMonitoring(this);
394     navigationRegistry.on("navigation-started", this.#onNavigationEvent);
395     navigationRegistry.on("location-changed", this.#onNavigationEvent);
396     navigationRegistry.on("navigation-stopped", this.#onNavigationEvent);
397   }
399   stopMonitoring() {
400     if (!this.#monitoring) {
401       return;
402     }
404     this.#monitoring = false;
405     navigationRegistry.stopMonitoring(this);
406     navigationRegistry.off("navigation-started", this.#onNavigationEvent);
407     navigationRegistry.off("location-changed", this.#onNavigationEvent);
408     navigationRegistry.off("navigation-stopped", this.#onNavigationEvent);
409   }
411   #onNavigationEvent = (eventName, data) => {
412     this.emit(eventName, data);
413   };