Bug 1760890 [wpt PR 33308] - Revert "Create property tree nodes for will-change only...
[gecko.git] / remote / shared / Navigate.jsm
blob4bab332bd01b9b4e0fc7974f4fc02314e0b62b90
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 file,
3  * You can obtain one at http://mozilla.org/MPL/2.0/. */
5 "use strict";
7 const EXPORTED_SYMBOLS = [
8   "ProgressListener",
9   "waitForInitialNavigationCompleted",
12 const { XPCOMUtils } = ChromeUtils.import(
13   "resource://gre/modules/XPCOMUtils.jsm"
16 XPCOMUtils.defineLazyModuleGetters(this, {
17   Log: "chrome://remote/content/shared/Log.jsm",
18   truncate: "chrome://remote/content/shared/Format.jsm",
19 });
21 XPCOMUtils.defineLazyGetter(this, "logger", () =>
22   Log.get(Log.TYPES.REMOTE_AGENT)
25 // Used to keep weak references of webProgressListeners alive.
26 const webProgressListeners = new Set();
28 /**
29  * Wait until the initial load of the given WebProgress is done.
30  *
31  * @param {WebProgress} webProgress
32  *     The WebProgress instance to observe.
33  * @param {Object=} options
34  * @param {Boolean=} options.resolveWhenStarted
35  *     Flag to indicate that the Promise has to be resolved when the
36  *     page load has been started. Otherwise wait until the page has
37  *     finished loading. Defaults to `false`.
38  *
39  * @returns {Promise}
40  *     Promise which resolves when the page load is in the expected state.
41  *     Values as returned:
42  *       - {nsIURI} currentURI The current URI of the page
43  *       - {nsIURI} targetURI Target URI of the navigation
44  */
45 async function waitForInitialNavigationCompleted(webProgress, options = {}) {
46   const { resolveWhenStarted = false } = options;
48   const browsingContext = webProgress.browsingContext;
50   // Start the listener right away to avoid race conditions.
51   const listener = new ProgressListener(webProgress, { resolveWhenStarted });
52   const navigated = listener.start();
54   // Right after a browsing context has been attached it could happen that
55   // no window global has been set yet. Consider this as nothing has been
56   // loaded yet.
57   let isInitial = true;
58   if (browsingContext.currentWindowGlobal) {
59     isInitial = browsingContext.currentWindowGlobal.isInitialDocument;
60   }
62   // If the current document is not the initial "about:blank" and is also
63   // no longer loading, assume the navigation is done and return.
64   if (!isInitial && !listener.isLoadingDocument) {
65     logger.trace(
66       truncate`[${browsingContext.id}] Document already finished loading: ${browsingContext.currentURI?.spec}`
67     );
69     // Will resolve the navigated promise.
70     listener.stop();
71   }
73   await navigated;
75   return {
76     currentURI: listener.currentURI,
77     targetURI: listener.targetURI,
78   };
81 /**
82  * WebProgressListener to observe for page loads.
83  */
84 class ProgressListener {
85   #resolveWhenStarted;
86   #unloadTimeout;
87   #waitForExplicitStart;
88   #webProgress;
90   #resolve;
91   #seenStartFlag;
92   #targetURI;
93   #unloadTimer;
95   /**
96    * Create a new WebProgressListener instance.
97    *
98    * @param {WebProgress} webProgress
99    *     The web progress to attach the listener to.
100    * @param {Object=} options
101    * @param {Boolean=} options.resolveWhenStarted
102    *     Flag to indicate that the Promise has to be resolved when the
103    *     page load has been started. Otherwise wait until the page has
104    *     finished loading. Defaults to `false`.
105    * @param {Number=} options.unloadTimeout
106    *     Time to allow before the page gets unloaded. Defaults to 200ms.
107    * @param {Boolean=} options.waitForExplicitStart
108    *     Flag to indicate that the Promise can only resolve after receiving a
109    *     STATE_START state change. In other words, if the webProgress is already
110    *     navigating, the Promise will only resolve for the next navigation.
111    *     Defaults to `false`.
112    */
113   constructor(webProgress, options = {}) {
114     const {
115       resolveWhenStarted = false,
116       unloadTimeout = 200,
117       waitForExplicitStart = false,
118     } = options;
120     this.#resolveWhenStarted = resolveWhenStarted;
121     this.#unloadTimeout = unloadTimeout;
122     this.#waitForExplicitStart = waitForExplicitStart;
123     this.#webProgress = webProgress;
125     this.#resolve = null;
126     this.#seenStartFlag = false;
127     this.#targetURI = null;
128     this.#unloadTimer = null;
129   }
131   get browsingContext() {
132     return this.#webProgress.browsingContext;
133   }
135   get currentURI() {
136     return this.#webProgress.browsingContext.currentURI;
137   }
139   get targetURI() {
140     return this.#targetURI;
141   }
143   get isLoadingDocument() {
144     return this.#webProgress.isLoadingDocument;
145   }
147   #checkLoadingState(request, options = {}) {
148     const { isStart = false, isStop = false } = options;
150     if (isStart && !this.#seenStartFlag) {
151       this.#seenStartFlag = true;
153       this.#targetURI = this.#getTargetURI(request);
155       logger.trace(
156         truncate`[${this.browsingContext.id}] ${this.constructor.name} state=start: ${this.targetURI?.spec}`
157       );
159       if (this.#unloadTimer) {
160         this.#unloadTimer.cancel();
161         this.#unloadTimer = null;
162       }
164       if (this.#resolveWhenStarted) {
165         // Return immediately when the load should not be awaited.
166         this.stop();
167         return;
168       }
169     }
171     if (isStop && this.#seenStartFlag) {
172       logger.trace(
173         truncate`[${this.browsingContext.id}] ${this.constructor.name} state=stop: ${this.currentURI.spec}`
174       );
176       this.stop();
177     }
178   }
180   #getTargetURI(request) {
181     try {
182       return request.QueryInterface(Ci.nsIChannel).originalURI;
183     } catch (e) {}
185     return null;
186   }
188   onStateChange(progress, request, flag, status) {
189     this.#checkLoadingState(request, {
190       isStart: flag & Ci.nsIWebProgressListener.STATE_START,
191       isStop: flag & Ci.nsIWebProgressListener.STATE_STOP,
192     });
193   }
195   /**
196    * Start observing web progress changes.
197    *
198    * @returns {Promise}
199    *     A promise that will resolve when the navigation has been finished.
200    */
201   start() {
202     if (this.#resolve) {
203       throw new Error(`Progress listener already started`);
204     }
206     if (this.#webProgress.isLoadingDocument) {
207       this.#targetURI = this.#getTargetURI(this.#webProgress.documentRequest);
209       if (this.#resolveWhenStarted) {
210         // Resolve immediately when the page is already loading and there
211         // is no requirement to wait for it to finish.
212         return Promise.resolve();
213       }
214     }
216     const promise = new Promise(resolve => (this.#resolve = resolve));
218     // Enable all state notifications to get informed about an upcoming load
219     // as early as possible.
220     this.#webProgress.addProgressListener(
221       this,
222       Ci.nsIWebProgress.NOTIFY_STATE_ALL
223     );
225     webProgressListeners.add(this);
227     if (this.#webProgress.isLoadingDocument && !this.#waitForExplicitStart) {
228       this.#checkLoadingState(this.#webProgress.documentRequest, {
229         isStart: true,
230       });
231     } else {
232       // If the document is not loading yet wait some time for the navigation
233       // to be started.
234       this.#unloadTimer = Cc["@mozilla.org/timer;1"].createInstance(
235         Ci.nsITimer
236       );
237       this.#unloadTimer.initWithCallback(
238         () => {
239           logger.trace(
240             truncate`[${this.browsingContext.id}] No navigation detected: ${this.currentURI?.spec}`
241           );
242           // Assume the target is the currently loaded URI.
243           this.#targetURI = this.currentURI;
244           this.stop();
245         },
246         this.#unloadTimeout,
247         Ci.nsITimer.TYPE_ONE_SHOT
248       );
249     }
251     return promise;
252   }
254   /**
255    * Stop observing web progress changes.
256    */
257   stop() {
258     if (!this.#resolve) {
259       throw new Error(`Progress listener not yet started`);
260     }
262     this.#unloadTimer?.cancel();
263     this.#unloadTimer = null;
265     this.#webProgress.removeProgressListener(
266       this,
267       Ci.nsIWebProgress.NOTIFY_STATE_ALL
268     );
269     webProgressListeners.delete(this);
271     if (!this.#targetURI) {
272       // If no target URI has been set yet it should be the current URI
273       this.#targetURI = this.browsingContext.currentURI;
274     }
276     this.#resolve();
277     this.#resolve = null;
278   }
280   toString() {
281     return `[object ${this.constructor.name}]`;
282   }
284   get QueryInterface() {
285     return ChromeUtils.generateQI([
286       "nsIWebProgressListener",
287       "nsISupportsWeakReference",
288     ]);
289   }