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 {
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();
86 * Retrieve the last known navigation data for a given browsing context.
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.
93 getNavigationForBrowsingContext(context) {
94 if (!lazy.TabManager.isValidCanonicalBrowsingContext(context)) {
95 // Bail out if the provided context is not a valid CanonicalBrowsingContext
100 const navigable = lazy.TabManager.getNavigableForBrowsingContext(context);
101 if (!this.#navigations.has(navigable)) {
105 return this.#navigations.get(navigable);
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.
113 startMonitoring(listener) {
114 if (this.#managers.size == 0) {
115 lazy.registerNavigationListenerActor();
118 this.#managers.add(listener);
122 * Stop monitoring navigations. This will unregister the NavigationListener
123 * JSWindowActor and clear the information collected about navigations so far.
125 stopMonitoring(listener) {
126 if (!this.#managers.has(listener)) {
130 this.#managers.delete(listener);
131 if (this.#managers.size == 0) {
132 lazy.unregisterNavigationListenerActor();
134 this.#navigations = new WeakMap();
139 * Called when a same-document navigation is recorded from the
140 * NavigationListener actors.
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.
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.
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 });
172 * Called when a navigation-started event is recorded from the
173 * NavigationListener actors.
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.
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.
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.
200 `[${navigableId}] Skipping already tracked navigation, navigationId: ${navigation.navigationId}`
205 const navigationId = this.#getOrCreateNavigationId(navigableId);
206 navigation = { finished: false, navigationId, url };
207 this.#navigations.set(navigable, navigation);
210 lazy.truncate`[${navigableId}] Navigation started for url: ${url} (${navigationId})`
213 this.emit("navigation-started", { navigationId, navigableId, url });
219 * Called when a navigation-stopped event is recorded from the
220 * NavigationListener actors.
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.
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);
240 lazy.truncate`[${navigableId}] No navigation found to stop for url: ${url}`
245 if (navigation.finished) {
247 `[${navigableId}] Navigation already marked as finished, navigationId: ${navigation.navigationId}`
253 lazy.truncate`[${navigableId}] Navigation finished for url: ${url} (${navigation.navigationId})`
256 navigation.finished = true;
258 this.emit("navigation-stopped", {
259 navigationId: navigation.navigationId,
268 * Register a navigation id to be used for the next navigation for the
269 * provided browsing context details.
271 * @param {object} data
272 * @param {BrowsingContextDetails} data.contextDetails
273 * The details about the browsing context for this navigation.
275 * The UUID created the upcoming navigation.
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);
288 #getContextFromContextDetails(contextDetails) {
289 if (contextDetails.context) {
290 return contextDetails.context;
293 return contextDetails.isTopBrowsingContext
294 ? BrowsingContext.getCurrentTopByBrowserId(contextDetails.browserId)
295 : BrowsingContext.get(contextDetails.browsingContextId);
298 #getOrCreateNavigationId(navigableId) {
300 if (this.#navigationIds.has(navigableId)) {
301 navigationId = this.#navigationIds.get(navigableId, navigationId);
302 this.#navigationIds.delete(navigableId);
304 navigationId = lazy.generateUUID();
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.
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.
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.
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
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.
370 export class NavigationManager extends EventEmitter {
376 this.#monitoring = false;
380 this.stopMonitoring();
383 getNavigationForBrowsingContext(context) {
384 return navigationRegistry.getNavigationForBrowsingContext(context);
388 if (this.#monitoring) {
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);
400 if (!this.#monitoring) {
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);
411 #onNavigationEvent = (eventName, data) => {
412 this.emit(eventName, data);