sync.sys.mjs (12875B)
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/. */ 4 5 const lazy = {}; 6 7 ChromeUtils.defineESModuleGetters(lazy, { 8 error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs", 9 Log: "chrome://remote/content/shared/Log.sys.mjs", 10 }); 11 12 ChromeUtils.defineLazyGetter(lazy, "logger", () => 13 lazy.Log.get(lazy.Log.TYPES.MARIONETTE) 14 ); 15 16 const { TYPE_ONE_SHOT, TYPE_REPEATING_SLACK } = Ci.nsITimer; 17 18 /** 19 * Runs a Promise-like function off the main thread until it is resolved 20 * through ``resolve`` or ``rejected`` callbacks. The function is 21 * guaranteed to be run at least once, regardless of the timeout. 22 * 23 * The ``func`` is evaluated every ``interval`` for as long as its 24 * runtime duration does not exceed ``interval``. Evaluations occur 25 * sequentially, meaning that evaluations of ``func`` are queued if 26 * the runtime evaluation duration of ``func`` is greater than ``interval``. 27 * 28 * ``func`` is given two arguments, ``resolve`` and ``reject``, 29 * of which one must be called for the evaluation to complete. 30 * Calling ``resolve`` with an argument indicates that the expected 31 * wait condition was met and will return the passed value to the 32 * caller. Conversely, calling ``reject`` will evaluate ``func`` 33 * again until the ``timeout`` duration has elapsed or ``func`` throws. 34 * The passed value to ``reject`` will also be returned to the caller 35 * once the wait has expired. 36 * 37 * Usage:: 38 * 39 * let els = new PollPromise((resolve, reject) => { 40 * let res = document.querySelectorAll("p"); 41 * if (res.length > 0) { 42 * resolve(Array.from(res)); 43 * } else { 44 * reject([]); 45 * } 46 * }, {timeout: 1000}); 47 * 48 * @param {Condition} func 49 * Function to run off the main thread. 50 * @param {object=} options 51 * @param {number=} options.timeout 52 * Desired timeout if wanted. If 0 or less than the runtime evaluation 53 * time of ``func``, ``func`` is guaranteed to run at least once. 54 * Defaults to using no timeout. 55 * @param {number=} options.interval 56 * Duration between each poll of ``func`` in milliseconds. 57 * Defaults to 10 milliseconds. 58 * 59 * @returns {Promise.<*>} 60 * Yields the value passed to ``func``'s 61 * ``resolve`` or ``reject`` callbacks. 62 * 63 * @throws {*} 64 * If ``func`` throws, its error is propagated. 65 * @throws {TypeError} 66 * If `timeout` or `interval`` are not numbers. 67 * @throws {RangeError} 68 * If `timeout` or `interval` are not unsigned integers. 69 */ 70 export function PollPromise(func, { timeout = null, interval = 10 } = {}) { 71 const timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); 72 73 if (typeof func != "function") { 74 throw new TypeError(); 75 } 76 if (timeout != null && typeof timeout != "number") { 77 throw new TypeError(); 78 } 79 if (typeof interval != "number") { 80 throw new TypeError(); 81 } 82 if ( 83 (timeout && (!Number.isInteger(timeout) || timeout < 0)) || 84 !Number.isInteger(interval) || 85 interval < 0 86 ) { 87 throw new RangeError(); 88 } 89 90 return new Promise((resolve, reject) => { 91 let start, end; 92 93 if (Number.isInteger(timeout)) { 94 start = new Date().getTime(); 95 end = start + timeout; 96 } 97 98 let evalFn = () => { 99 new Promise(func) 100 .then(resolve, rejected => { 101 if (lazy.error.isError(rejected)) { 102 throw rejected; 103 } 104 105 // return if there is a timeout and set to 0, 106 // allowing |func| to be evaluated at least once 107 if ( 108 typeof end != "undefined" && 109 (start == end || new Date().getTime() >= end) 110 ) { 111 resolve(rejected); 112 } 113 }) 114 .catch(reject); 115 }; 116 117 // the repeating slack timer waits |interval| 118 // before invoking |evalFn| 119 evalFn(); 120 121 timer.init(evalFn, interval, TYPE_REPEATING_SLACK); 122 }).then( 123 res => { 124 timer.cancel(); 125 return res; 126 }, 127 err => { 128 timer.cancel(); 129 throw err; 130 } 131 ); 132 } 133 134 /** 135 * Pauses for the given duration. 136 * 137 * @param {number} timeout 138 * Duration to wait before fulfilling promise in milliseconds. 139 * 140 * @returns {Promise} 141 * Promise that fulfills when the `timeout` is elapsed. 142 * 143 * @throws {TypeError} 144 * If `timeout` is not a number. 145 * @throws {RangeError} 146 * If `timeout` is not an unsigned integer. 147 */ 148 export function Sleep(timeout) { 149 if (typeof timeout != "number") { 150 throw new TypeError(); 151 } 152 if (!Number.isInteger(timeout) || timeout < 0) { 153 throw new RangeError(); 154 } 155 156 return new Promise(resolve => { 157 const timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); 158 timer.init( 159 () => { 160 // Bug 1663880 - Explicitly cancel the timer for now to prevent a hang 161 timer.cancel(); 162 resolve(); 163 }, 164 timeout, 165 TYPE_ONE_SHOT 166 ); 167 }); 168 } 169 170 /** 171 * Detects when the specified message manager has been destroyed. 172 * 173 * One can observe the removal and detachment of a content browser 174 * (`<xul:browser>`) or a chrome window by its message manager 175 * disconnecting. 176 * 177 * When a browser is associated with a tab, this is safer than only 178 * relying on the event `TabClose` which signalises the _intent to_ 179 * remove a tab and consequently would lead to the destruction of 180 * the content browser and its browser message manager. 181 * 182 * When closing a chrome window it is safer than only relying on 183 * the event 'unload' which signalises the _intent to_ close the 184 * chrome window and consequently would lead to the destruction of 185 * the window and its window message manager. 186 * 187 * @param {MessageListenerManager} messageManager 188 * The message manager to observe for its disconnect state. 189 * Use the browser message manager when closing a content browser, 190 * and the window message manager when closing a chrome window. 191 * 192 * @returns {Promise} 193 * A promise that resolves when the message manager has been destroyed. 194 */ 195 export function MessageManagerDestroyedPromise(messageManager) { 196 return new Promise(resolve => { 197 function observe(subject, topic) { 198 lazy.logger.trace(`Received observer notification ${topic}`); 199 200 if (subject == messageManager) { 201 Services.obs.removeObserver(this, "message-manager-disconnect"); 202 resolve(); 203 } 204 } 205 206 Services.obs.addObserver(observe, "message-manager-disconnect"); 207 }); 208 } 209 210 /** 211 * Wraps a callback function, that, as long as it continues to be 212 * invoked, will not be triggered. The given function will be 213 * called after the timeout duration is reached, after no more 214 * events fire. 215 * 216 * This class implements the {@link EventListener} interface, 217 * which means it can be used interchangeably with `addEventHandler`. 218 * 219 * Debouncing events can be useful when dealing with e.g. DOM events 220 * that fire at a high rate. It is generally advisable to avoid 221 * computationally expensive operations such as DOM modifications 222 * under these circumstances. 223 * 224 * One such high frequenecy event is `resize` that can fire multiple 225 * times before the window reaches its final dimensions. In order 226 * to delay an operation until the window has completed resizing, 227 * it is possible to use this technique to only invoke the callback 228 * after the last event has fired:: 229 * 230 * let cb = new DebounceCallback(event => { 231 * // fires after the final resize event 232 * console.log("resize", event); 233 * }); 234 * window.addEventListener("resize", cb); 235 * 236 * Note that it is not possible to use this synchronisation primitive 237 * with `addEventListener(..., {once: true})`. 238 * 239 * @param {function(Event): void} fn 240 * Callback function that is guaranteed to be invoked once only, 241 * after `timeout`. 242 * @param {number=} [timeout = 250] timeout 243 * Time since last event firing, before `fn` will be invoked. 244 */ 245 export class DebounceCallback { 246 constructor(fn, { timeout = 250 } = {}) { 247 if (typeof fn != "function" || typeof timeout != "number") { 248 throw new TypeError(); 249 } 250 if (!Number.isInteger(timeout) || timeout < 0) { 251 throw new RangeError(); 252 } 253 254 this.fn = fn; 255 this.timeout = timeout; 256 this.timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); 257 } 258 259 handleEvent(ev) { 260 this.timer.cancel(); 261 this.timer.initWithCallback( 262 () => { 263 this.timer.cancel(); 264 this.fn(ev); 265 }, 266 this.timeout, 267 TYPE_ONE_SHOT 268 ); 269 } 270 } 271 272 /** 273 * Wait for a message to be fired from a particular message manager. 274 * 275 * This method has been duplicated from BrowserTestUtils.sys.mjs. 276 * 277 * @param {nsIMessageManager} messageManager 278 * The message manager that should be used. 279 * @param {string} messageName 280 * The message to wait for. 281 * @param {object=} options 282 * Extra options. 283 * @param {function(Message): boolean=} options.checkFn 284 * Called with the ``Message`` object as argument, should return ``true`` 285 * if the message is the expected one, or ``false`` if it should be 286 * ignored and listening should continue. If not specified, the first 287 * message with the specified name resolves the returned promise. 288 * 289 * @returns {Promise.<object>} 290 * Promise which resolves to the data property of the received 291 * ``Message``. 292 */ 293 export function waitForMessage( 294 messageManager, 295 messageName, 296 { checkFn = undefined } = {} 297 ) { 298 if (messageManager == null || !("addMessageListener" in messageManager)) { 299 throw new TypeError(); 300 } 301 if (typeof messageName != "string") { 302 throw new TypeError(); 303 } 304 if (checkFn && typeof checkFn != "function") { 305 throw new TypeError(); 306 } 307 308 return new Promise(resolve => { 309 messageManager.addMessageListener(messageName, function onMessage(msg) { 310 lazy.logger.trace(`Received ${messageName} for ${msg.target}`); 311 if (checkFn && !checkFn(msg)) { 312 return; 313 } 314 messageManager.removeMessageListener(messageName, onMessage); 315 resolve(msg.data); 316 }); 317 }); 318 } 319 320 /** 321 * Wait for the specified observer topic to be observed. 322 * 323 * This method has been duplicated from TestUtils.sys.mjs. 324 * 325 * Because this function is intended for testing, any error in checkFn 326 * will cause the returned promise to be rejected instead of waiting for 327 * the next notification, since this is probably a bug in the test. 328 * 329 * @param {string} topic 330 * The topic to observe. 331 * @param {object=} options 332 * Extra options. 333 * @param {function(string, object): boolean=} options.checkFn 334 * Called with ``subject``, and ``data`` as arguments, should return true 335 * if the notification is the expected one, or false if it should be 336 * ignored and listening should continue. If not specified, the first 337 * notification for the specified topic resolves the returned promise. 338 * @param {number=} options.timeout 339 * Timeout duration in milliseconds, if provided. 340 * If specified, then the returned promise will be rejected with 341 * TimeoutError, if not already resolved, after this duration has elapsed. 342 * If not specified, then no timeout is used. Defaults to null. 343 * 344 * @returns {Promise.<Array<string, object>>} 345 * Promise which is either resolved to an array of ``subject``, and ``data`` 346 * from the observed notification, or rejected with TimeoutError after 347 * options.timeout milliseconds if specified. 348 * 349 * @throws {TypeError} 350 * @throws {RangeError} 351 */ 352 export function waitForObserverTopic(topic, options = {}) { 353 const { checkFn = null, timeout = null } = options; 354 if (typeof topic != "string") { 355 throw new TypeError(); 356 } 357 if ( 358 (checkFn != null && typeof checkFn != "function") || 359 (timeout !== null && typeof timeout != "number") 360 ) { 361 throw new TypeError(); 362 } 363 if (timeout && (!Number.isInteger(timeout) || timeout < 0)) { 364 throw new RangeError(); 365 } 366 367 return new Promise((resolve, reject) => { 368 let timer; 369 370 function cleanUp() { 371 Services.obs.removeObserver(observer, topic); 372 timer?.cancel(); 373 } 374 375 function observer(subject, _topic, data) { 376 lazy.logger.trace(`Received observer notification ${_topic}`); 377 try { 378 if (checkFn && !checkFn(subject, data)) { 379 return; 380 } 381 cleanUp(); 382 resolve({ subject, data }); 383 } catch (ex) { 384 cleanUp(); 385 reject(ex); 386 } 387 } 388 389 Services.obs.addObserver(observer, topic); 390 391 if (timeout !== null) { 392 timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); 393 timer.init( 394 () => { 395 cleanUp(); 396 reject( 397 new lazy.error.TimeoutError( 398 `waitForObserverTopic timed out after ${timeout} ms` 399 ) 400 ); 401 }, 402 timeout, 403 TYPE_ONE_SHOT 404 ); 405 } 406 }); 407 }