Backed out 8 changesets (bug 1963014) for causing mochitests failures in browser_UsageTelemetry.js. CLOSED TREE

Backed out changeset 10cd387da114 (bug 1963014)
Backed out changeset db1cc23f2502 (bug 1963014)
Backed out changeset 076cbc895e0c (bug 1963014)
Backed out changeset 4df46947d96f (bug 1963014)
Backed out changeset 8692782e408c (bug 1963014)
Backed out changeset ddbecd248a02 (bug 1963014)
Backed out changeset f25d7077fec6 (bug 1963014)
Backed out changeset 96e088ca29d2 (bug 1963014)
This commit is contained in:
Stanca Serban
2025-04-28 23:08:13 +03:00
parent c1f0141c1a
commit 4f0eb0f875
11 changed files with 394 additions and 508 deletions

View File

@@ -391,7 +391,7 @@ const rollouts = [
"toolkit/components/workerloader/require.js", "toolkit/components/workerloader/require.js",
"toolkit/content/**", "toolkit/content/**",
"toolkit/crashreporter/**", "toolkit/crashreporter/**",
"toolkit/modules/{C,Da,E10SUtils,F,G,In,J,Ke,L,N,P,Rem,S,Up,W}*.sys.mjs", "toolkit/modules/{A,C,D,E10SUtils,F,G,I,J,K,L,N,O,P,R,S,U,W}*.sys.mjs",
"toolkit/modules/sessionstore/**", "toolkit/modules/sessionstore/**",
"toolkit/modules/subprocess/**", "toolkit/modules/subprocess/**",
"toolkit/modules/tests/**", "toolkit/modules/tests/**",
@@ -555,10 +555,7 @@ const rollouts = [
"toolkit/components/workerloader/require.js", "toolkit/components/workerloader/require.js",
"toolkit/content/**", "toolkit/content/**",
"toolkit/crashreporter/**", "toolkit/crashreporter/**",
"toolkit/modules/{Asy,B,C,Da,E10SUtils,F,G,In,JS,Ke,L,Ne,P,Rem,S,Up,W}*.sys.mjs", "toolkit/modules/**",
"toolkit/modules/sessionstore/**",
"toolkit/modules/subprocess/**",
"toolkit/modules/tests/**",
"toolkit/mozapps/downloads/**", "toolkit/mozapps/downloads/**",
"toolkit/mozapps/extensions/**", "toolkit/mozapps/extensions/**",
"toolkit/mozapps/handling/**", "toolkit/mozapps/handling/**",

View File

@@ -4,12 +4,6 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this * License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
const Timer = Components.Constructor(
"@mozilla.org/timer;1",
"nsITimer",
"initWithCallback"
);
/** /**
* Sets up a function or an asynchronous task whose execution can be triggered * Sets up a function or an asynchronous task whose execution can be triggered
* after a defined delay. Multiple attempts to run the task before the delay * after a defined delay. Multiple attempts to run the task before the delay
@@ -81,71 +75,70 @@ const Timer = Components.Constructor(
* saveDeferredTask.finalize().then(() => OS.File.remove(...)) * saveDeferredTask.finalize().then(() => OS.File.remove(...))
* .then(null, Components.utils.reportError); * .then(null, Components.utils.reportError);
*/ */
export class DeferredTask {
/**
* Sets up a task whose execution can be triggered after a delay.
*
* @param {Function} taskFn
* Function to execute. If the function returns a promise, the task is not
* considered complete until that promise resolves. This task is never
* re-entered while running.
* @param {number} delayMs
* Time between executions, in milliseconds. Multiple attempts to run the
* task before the delay has passed are coalesced. This time of inactivity
* is guaranteed to pass between multiple executions of the task, except on
* finalization, when the task may restart immediately after the previous
* execution finished.
* @param {number} [idleTimeoutMs]
* The maximum time to wait for an idle slot on the main thread after
* aDelayMs have elapsed. If omitted, waits indefinitely for an idle
* callback.
*/
constructor(taskFn, delayMs, idleTimeoutMs) {
this.#taskFn = taskFn;
this.#delayMs = delayMs;
this.#idleTimeoutMs = idleTimeoutMs;
this.#caller = new Error().stack.split("\n", 2)[1];
let markerString = `delay: ${delayMs}ms`;
if (idleTimeoutMs) {
markerString += `, idle timeout: ${idleTimeoutMs}`;
}
ChromeUtils.addProfilerMarker(
"DeferredTask",
{ captureStack: true },
markerString
);
}
// Globals
const Timer = Components.Constructor(
"@mozilla.org/timer;1",
"nsITimer",
"initWithCallback"
);
// DeferredTask
/**
* Sets up a task whose execution can be triggered after a delay.
*
* @param aTaskFn
* Function to execute. If the function returns a promise, the task is
* not considered complete until that promise resolves. This
* task is never re-entered while running.
* @param aDelayMs
* Time between executions, in milliseconds. Multiple attempts to run
* the task before the delay has passed are coalesced. This time of
* inactivity is guaranteed to pass between multiple executions of the
* task, except on finalization, when the task may restart immediately
* after the previous execution finished.
* @param aIdleTimeoutMs
* The maximum time to wait for an idle slot on the main thread after
* aDelayMs have elapsed. If omitted, waits indefinitely for an idle
* callback.
*/
export var DeferredTask = function (aTaskFn, aDelayMs, aIdleTimeoutMs) {
this._taskFn = aTaskFn;
this._delayMs = aDelayMs;
this._timeoutMs = aIdleTimeoutMs;
this._caller = new Error().stack.split("\n", 2)[1];
let markerString = `delay: ${aDelayMs}ms`;
if (aIdleTimeoutMs) {
markerString += `, idle timeout: ${aIdleTimeoutMs}`;
}
ChromeUtils.addProfilerMarker(
"DeferredTask",
{ captureStack: true },
markerString
);
};
DeferredTask.prototype = {
/** /**
* Function to execute. * Function to execute.
*/ */
#taskFn; _taskFn: null,
/** /**
* Time between executions, in milliseconds. * Time between executions, in milliseconds.
*/ */
#delayMs; _delayMs: null,
/**
* The idle timeout wait.
*
* @type {number|undefined}
*/
#idleTimeoutMs = undefined;
/**
* The name of the caller that created the deferred task.
*/
#caller;
/** /**
* Indicates whether the task is currently requested to start again later, * Indicates whether the task is currently requested to start again later,
* regardless of whether it is currently running. * regardless of whether it is currently running.
*/ */
get isArmed() { get isArmed() {
return this.#armed; return this._armed;
} },
#armed = false; _armed: false,
/** /**
* Indicates whether the task is currently running. This is always true when * Indicates whether the task is currently running. This is always true when
@@ -154,59 +147,50 @@ export class DeferredTask {
*/ */
get isRunning() { get isRunning() {
return !!this._runningPromise; return !!this._runningPromise;
} },
/** /**
* Promise resolved when the current execution of the task terminates, or null * Promise resolved when the current execution of the task terminates, or null
* if the task is not currently running. * if the task is not currently running.
*
* May be accessed for tests.
*
* @type {Promise<void>|undefined}
*/ */
_runningPromise = undefined; _runningPromise: null,
/** /**
* nsITimer used for triggering the task after a delay, or null in case the * nsITimer used for triggering the task after a delay, or null in case the
* task is running or there is no task scheduled for execution. * task is running or there is no task scheduled for execution.
*
* @type {nsITimer|null}
*/ */
#timer = null; _timer: null,
/** /**
* Actually starts the timer with the delay specified on construction. * Actually starts the timer with the delay specified on construction.
*/ */
#startTimer() { _startTimer() {
let callback, timer; let callback, timer;
if (this.#idleTimeoutMs === 0) { if (this._timeoutMs === 0) {
callback = () => this.#timerCallback(); callback = () => this._timerCallback();
} else { } else {
callback = () => { callback = () => {
this._startIdleDispatch(() => { this._startIdleDispatch(() => {
// #timer could have changed by now: // _timer could have changed by now:
// - to null if disarm() or finalize() has been called. // - to null if disarm() or finalize() has been called.
// - to a new nsITimer if disarm() was called, followed by arm(). // - to a new nsITimer if disarm() was called, followed by arm().
// In either case, don't invoke #timerCallback any more. // In either case, don't invoke _timerCallback any more.
if (this.#timer === timer) { if (this._timer === timer) {
this.#timerCallback(); this._timerCallback();
} }
}, this.#idleTimeoutMs); }, this._timeoutMs);
}; };
} }
timer = new Timer(callback, this.#delayMs, Ci.nsITimer.TYPE_ONE_SHOT); timer = new Timer(callback, this._delayMs, Ci.nsITimer.TYPE_ONE_SHOT);
this.#timer = timer; this._timer = timer;
} },
/** /**
* Dispatches idle task. Can be overridden for testing by test_DeferredTask. * Dispatches idle task. Can be overridden for testing by test_DeferredTask.
*
* @param {IdleRequestCallback} callback
* @param {number} timeout
*/ */
_startIdleDispatch(callback, timeout) { _startIdleDispatch(callback, timeout) {
ChromeUtils.idleDispatch(callback, { timeout }); ChromeUtils.idleDispatch(callback, { timeout });
} },
/** /**
* Requests the execution of the task after the delay specified on * Requests the execution of the task after the delay specified on
@@ -218,7 +202,7 @@ export class DeferredTask {
* within the same tick of the event loop are guaranteed to result in a single * within the same tick of the event loop are guaranteed to result in a single
* execution of the task. * execution of the task.
* *
* Note: By design, this method doesn't provide a way for the caller to detect * @note By design, this method doesn't provide a way for the caller to detect
* when the next execution terminates, or collect a result. In fact, * when the next execution terminates, or collect a result. In fact,
* doing that would often result in duplicate processing or logging. If * doing that would often result in duplicate processing or logging. If
* a special operation or error logging is needed on completion, it can * a special operation or error logging is needed on completion, it can
@@ -227,19 +211,19 @@ export class DeferredTask {
* used in the common case of waiting for completion on shutdown. * used in the common case of waiting for completion on shutdown.
*/ */
arm() { arm() {
if (this.#finalized) { if (this._finalized) {
throw new Error("Unable to arm timer, the object has been finalized."); throw new Error("Unable to arm timer, the object has been finalized.");
} }
this.#armed = true; this._armed = true;
// In case the timer callback is running, do not create the timer now, // In case the timer callback is running, do not create the timer now,
// because this will be handled by the timer callback itself. Also, the // because this will be handled by the timer callback itself. Also, the
// timer is not restarted in case it is already running. // timer is not restarted in case it is already running.
if (!this._runningPromise && !this.#timer) { if (!this._runningPromise && !this._timer) {
this.#startTimer(); this._startTimer();
} }
} },
/** /**
* Cancels any request for a delayed the execution of the task, though the * Cancels any request for a delayed the execution of the task, though the
@@ -249,15 +233,15 @@ export class DeferredTask {
* from its original value in case the "arm" method is called again. * from its original value in case the "arm" method is called again.
*/ */
disarm() { disarm() {
this.#armed = false; this._armed = false;
if (this.#timer) { if (this._timer) {
// Calling the "cancel" method and discarding the timer reference makes // Calling the "cancel" method and discarding the timer reference makes
// sure that the timer callback will not be called later, even if the // sure that the timer callback will not be called later, even if the
// timer thread has already posted the timer event on the main thread. // timer thread has already posted the timer event on the main thread.
this.#timer.cancel(); this._timer.cancel();
this.#timer = null; this._timer = null;
} }
} },
/** /**
* Ensures that any pending task is executed from start to finish, while * Ensures that any pending task is executed from start to finish, while
@@ -274,21 +258,22 @@ export class DeferredTask {
* - If the task is not running and the timer is not armed, the method returns * - If the task is not running and the timer is not armed, the method returns
* a resolved promise. * a resolved promise.
* *
* @returns {Promise<void>} * @return {Promise}
* Resolves when the last execution of the task is finished. * @resolves After the last execution of the task is finished.
* @rejects Never.
*/ */
finalize() { finalize() {
if (this.#finalized) { if (this._finalized) {
throw new Error("The object has been already finalized."); throw new Error("The object has been already finalized.");
} }
this.#finalized = true; this._finalized = true;
// If the timer is armed, it means that the task is not running but it is // If the timer is armed, it means that the task is not running but it is
// scheduled for execution. Cancel the timer and run the task immediately, // scheduled for execution. Cancel the timer and run the task immediately,
// so we don't risk blocking async shutdown longer than necessary. // so we don't risk blocking async shutdown longer than necessary.
if (this.#timer) { if (this._timer) {
this.disarm(); this.disarm();
this.#timerCallback(); this._timerCallback();
} }
// Wait for the operation to be completed, or resolve immediately. // Wait for the operation to be completed, or resolve immediately.
@@ -296,20 +281,20 @@ export class DeferredTask {
return this._runningPromise; return this._runningPromise;
} }
return Promise.resolve(); return Promise.resolve();
} },
#finalized = false; _finalized: false,
/** /**
* Whether the DeferredTask has been finalized, and it cannot be armed anymore. * Whether the DeferredTask has been finalized, and it cannot be armed anymore.
*/ */
get isFinalized() { get isFinalized() {
return this.#finalized; return this._finalized;
} },
/** /**
* Timer callback used to run the delayed task. * Timer callback used to run the delayed task.
*/ */
#timerCallback() { _timerCallback() {
let runningDeferred = Promise.withResolvers(); let runningDeferred = Promise.withResolvers();
// All these state changes must occur at the same time directly inside the // All these state changes must occur at the same time directly inside the
@@ -317,26 +302,26 @@ export class DeferredTask {
// methods behave consistently even if called from inside the task. This // methods behave consistently even if called from inside the task. This
// means that the assignment of "this._runningPromise" must complete before // means that the assignment of "this._runningPromise" must complete before
// the task gets a chance to start. // the task gets a chance to start.
this.#timer = null; this._timer = null;
this.#armed = false; this._armed = false;
this._runningPromise = runningDeferred.promise; this._runningPromise = runningDeferred.promise;
runningDeferred.resolve( runningDeferred.resolve(
(async () => { (async () => {
// Execute the provided function asynchronously. // Execute the provided function asynchronously.
await this.#runTask(); await this._runTask();
// Now that the task has finished, we check the state of the object to // Now that the task has finished, we check the state of the object to
// determine if we should restart the task again. // determine if we should restart the task again.
if (this.#armed) { if (this._armed) {
if (!this.#finalized) { if (!this._finalized) {
this.#startTimer(); this._startTimer();
} else { } else {
// Execute the task again immediately, for the last time. The isArmed // Execute the task again immediately, for the last time. The isArmed
// property should return false while the task is running, and should // property should return false while the task is running, and should
// remain false after the last execution terminates. // remain false after the last execution terminates.
this.#armed = false; this._armed = false;
await this.#runTask(); await this._runTask();
} }
} }
@@ -345,23 +330,23 @@ export class DeferredTask {
this._runningPromise = null; this._runningPromise = null;
})().catch(console.error) })().catch(console.error)
); );
} },
/** /**
* Executes the associated task and catches exceptions. * Executes the associated task and catches exceptions.
*/ */
async #runTask() { async _runTask() {
let startTime = Cu.now(); let startTime = Cu.now();
try { try {
await this.#taskFn(); await this._taskFn();
} catch (ex) { } catch (ex) {
console.error(ex); console.error(ex);
} finally { } finally {
ChromeUtils.addProfilerMarker( ChromeUtils.addProfilerMarker(
"DeferredTask", "DeferredTask",
{ startTime }, { startTime },
this.#caller this._caller
); );
} }
} },
} };

View File

@@ -10,75 +10,43 @@ ChromeUtils.defineESModuleGetters(lazy, {
"resource://services-settings/RemoteSettingsClient.sys.mjs", "resource://services-settings/RemoteSettingsClient.sys.mjs",
}); });
/**
* @typedef {import("../../services/settings/RemoteSettingsClient.sys.mjs").RemoteSettingsClient} RemoteSettingsClient
*/
const SETTINGS_IGNORELIST_KEY = "hijack-blocklists"; const SETTINGS_IGNORELIST_KEY = "hijack-blocklists";
/**
* A remote settings wrapper for the ignore lists from the hijack-blocklists
* collection.
*/
class IgnoreListsManager { class IgnoreListsManager {
/** async init() {
* @type {RemoteSettingsClient} if (!this._ignoreListSettings) {
*/ this._ignoreListSettings = lazy.RemoteSettings(SETTINGS_IGNORELIST_KEY);
#ignoreListSettings;
/**
* Initializes the manager, if it is not already initialised.
*/
#init() {
if (!this.#ignoreListSettings) {
this.#ignoreListSettings = lazy.RemoteSettings(SETTINGS_IGNORELIST_KEY);
} }
} }
/**
* Gets the current collection, subscribing to the collection after the
* get has been completed.
*
* @param {Function} listener
*/
async getAndSubscribe(listener) { async getAndSubscribe(listener) {
this.#init(); await this.init();
// Trigger a get of the initial value. // Trigger a get of the initial value.
const settings = await this.#getIgnoreList(); const settings = await this._getIgnoreList();
// Listen for future updates after we first get the values. // Listen for future updates after we first get the values.
this.#ignoreListSettings.on("sync", listener); this._ignoreListSettings.on("sync", listener);
return settings; return settings;
} }
/**
* Unsubscribes from updates to the collection.
*
* @param {Function} listener
*/
unsubscribe(listener) { unsubscribe(listener) {
if (!this.#ignoreListSettings) { if (!this._ignoreListSettings) {
return; return;
} }
this.#ignoreListSettings.off("sync", listener); this._ignoreListSettings.off("sync", listener);
} }
/** async _getIgnoreList() {
* @type {Promise<object[]>} if (this._getSettingsPromise) {
*/ return this._getSettingsPromise;
#getSettingsPromise;
async #getIgnoreList() {
if (this.#getSettingsPromise) {
return this.#getSettingsPromise;
} }
const settings = await (this.#getSettingsPromise = const settings = await (this._getSettingsPromise =
this.#getIgnoreListSettings()); this._getIgnoreListSettings());
this.#getSettingsPromise = undefined; delete this._getSettingsPromise;
return settings; return settings;
} }
@@ -94,14 +62,14 @@ class IgnoreListsManager {
* *
* @param {boolean} [firstTime] * @param {boolean} [firstTime]
* Internal boolean to indicate if this is the first time check or not. * Internal boolean to indicate if this is the first time check or not.
* @returns {Promise<object[]>} * @returns {array}
* An array of objects in the database, or an empty array if none * An array of objects in the database, or an empty array if none
* could be obtained. * could be obtained.
*/ */
async #getIgnoreListSettings(firstTime = true) { async _getIgnoreListSettings(firstTime = true) {
let result = []; let result = [];
try { try {
result = await this.#ignoreListSettings.get({ result = await this._ignoreListSettings.get({
verifySignature: true, verifySignature: true,
}); });
} catch (ex) { } catch (ex) {
@@ -110,9 +78,9 @@ class IgnoreListsManager {
firstTime firstTime
) { ) {
// The local database is invalid, try and reset it. // The local database is invalid, try and reset it.
await this.#ignoreListSettings.db.clear(); await this._ignoreListSettings.db.clear();
// Now call this again. // Now call this again.
return this.#getIgnoreListSettings(false); return this._getIgnoreListSettings(false);
} }
// Don't throw an error just log it, just continue with no data, and hopefully // Don't throw an error just log it, just continue with no data, and hopefully
// a sync will fix things later on. // a sync will fix things later on.
@@ -122,8 +90,4 @@ class IgnoreListsManager {
} }
} }
/**
* A remote settings wrapper for the ignore lists from the hijack-blocklists
* collection.
*/
export const IgnoreLists = new IgnoreListsManager(); export const IgnoreLists = new IgnoreListsManager();

View File

@@ -45,8 +45,7 @@ export var OSKeyStore = {
/** /**
* Consider the module is initialized as locked. OS might unlock without a * Consider the module is initialized as locked. OS might unlock without a
* prompt. * prompt.
* * @type {Boolean}
* @type {boolean}
*/ */
_isLocked: true, _isLocked: true,
@@ -153,7 +152,7 @@ export var OSKeyStore = {
* the key storage. If we start creating keys on macOS by running * the key storage. If we start creating keys on macOS by running
* this code we'll potentially have to do extra work to cleanup * this code we'll potentially have to do extra work to cleanup
* the mess later. * the mess later.
* @returns {Promise<object>} Object with the following properties: * @returns {Promise<Object>} Object with the following properties:
* authenticated: {boolean} Set to true if the user successfully authenticated. * authenticated: {boolean} Set to true if the user successfully authenticated.
* auth_details: {String?} Details of the authentication result. * auth_details: {String?} Details of the authentication result.
*/ */

View File

@@ -19,20 +19,36 @@ export var ObjectUtils = {
* `JSON.stringify` is not designed to be used for this purpose; objects may * `JSON.stringify` is not designed to be used for this purpose; objects may
* have ambiguous `toJSON()` implementations that would influence the test. * have ambiguous `toJSON()` implementations that would influence the test.
* *
* @param {any} a * @param a (mixed) Object or value to be compared.
* Object or value to be compared. * @param b (mixed) Object or value to be compared.
* @param {any} b * @return Boolean Whether the objects are deep equal.
* Object or value to be compared.
*/ */
deepEqual(a, b) { deepEqual(a, b) {
return _deepEqual(a, b); return _deepEqual(a, b);
}, },
/**
* A thin wrapper on an object, designed to prevent client code from
* accessing non-existent properties because of typos.
*
* // Without `strict`
* let foo = { myProperty: 1 };
* foo.MyProperty; // undefined
*
* // With `strict`
* let strictFoo = ObjectUtils.strict(foo);
* strictFoo.myProperty; // 1
* strictFoo.MyProperty; // TypeError: No such property "MyProperty"
*
* Note that `strict` has no effect in non-DEBUG mode.
*/
strict(obj) {
return _strict(obj);
},
/** /**
* Returns `true` if `obj` is an array without elements, an object without * Returns `true` if `obj` is an array without elements, an object without
* enumerable properties, or a falsy primitive; `false` otherwise. * enumerable properties, or a falsy primitive; `false` otherwise.
*
* @param {any} obj
*/ */
isEmpty(obj) { isEmpty(obj) {
if (!obj) { if (!obj) {
@@ -56,12 +72,6 @@ export var ObjectUtils = {
// Copyright (c) 2009 Thomas Robinson <280north.com> // Copyright (c) 2009 Thomas Robinson <280north.com>
// MIT license: http://opensource.org/licenses/MIT // MIT license: http://opensource.org/licenses/MIT
/**
* Tests objects & values for deep equality.
*
* @param {any} a
* @param {any} b
*/
function _deepEqual(a, b) { function _deepEqual(a, b) {
// The numbering below refers to sections in the CommonJS spec. // The numbering below refers to sections in the CommonJS spec.
@@ -112,41 +122,18 @@ function _deepEqual(a, b) {
return objEquiv(a, b); return objEquiv(a, b);
} }
/**
* Tests to see if an object is a particular instance of the given type.
*
* @param {object} object
* @param {string} type
*/
function instanceOf(object, type) { function instanceOf(object, type) {
return Object.prototype.toString.call(object) == "[object " + type + "]"; return Object.prototype.toString.call(object) == "[object " + type + "]";
} }
/**
* Checks is see if the value is undefined or null.
*
* @param {any} value
*/
function isUndefinedOrNull(value) { function isUndefinedOrNull(value) {
return value === null || value === undefined; return value === null || value === undefined;
} }
/**
* Checks to see if the object is an arguments object.
*
* @param {object} object
*/
function isArguments(object) { function isArguments(object) {
return instanceOf(object, "Arguments"); return instanceOf(object, "Arguments");
} }
/**
* Compares objects for equivalence.
*
* @param {object} a
* @param {object} b
* @returns {boolean}
*/
function objEquiv(a, b) { function objEquiv(a, b) {
if (isUndefinedOrNull(a) || isUndefinedOrNull(b)) { if (isUndefinedOrNull(a) || isUndefinedOrNull(b)) {
return false; return false;
@@ -208,3 +195,21 @@ function objEquiv(a, b) {
} }
// ... End of previously MIT-licensed code. // ... End of previously MIT-licensed code.
function _strict(obj) {
if (typeof obj != "object") {
throw new TypeError("Expected an object");
}
return new Proxy(obj, {
get(target, name) {
if (name in obj) {
return obj[name];
}
let error = new TypeError(`No such property: "${name}"`);
Promise.reject(error); // Cause an xpcshell/mochitest failure.
throw error;
},
});
}

View File

@@ -12,10 +12,6 @@ ChromeUtils.defineESModuleGetters(lazy, {
setTimeout: "resource://gre/modules/Timer.sys.mjs", setTimeout: "resource://gre/modules/Timer.sys.mjs",
}); });
/**
* @typedef {import("../../services/settings/RemoteSettingsClient.sys.mjs").RemoteSettingsClient} RemoteSettingsClient
*/
XPCOMUtils.defineLazyPreferenceGetter( XPCOMUtils.defineLazyPreferenceGetter(
lazy, lazy,
"wifiScanningEnabled", "wifiScanningEnabled",
@@ -117,42 +113,22 @@ let inChildProcess =
* specific customisations. * specific customisations.
*/ */
class RegionDetector { class RegionDetector {
/** // The users home location.
* The users home location. Accessible to tests. Production code should use
* the `home` getter.
*
* @type {string}
*/
_home = null; _home = null;
/** // The most recent location the user was detected.
* The most recent location the user was detected. Production code should use
* the `current` getter.
*
* @type {string}
*/
_current = null; _current = null;
/** // The RemoteSettings client used to sync region files.
* The RemoteSettings client used to sync region files. _rsClient = null;
* // Keep track of the wifi data across listener events.
* @type {RemoteSettingsClient} _wifiDataPromise = null;
*/ // Keep track of how many times we have tried to fetch
#rsClient; // the users region during failure.
/**
* The resolver for when wifi data is received.
*
* @type {?(value: any) => void}
*/
#wifiDataPromiseResolver = null;
/**
* Keeps track of how many times we have tried to fetch the users region during
* failure. Exposed for tests
*/
_retryCount = 0; _retryCount = 0;
/** /**
* @type {Promise} * @type {Promise}
* Allow tests to wait for init to be complete. * Allow tests to wait for init to be complete.
*/ */
#initPromise = null; _initPromise = null;
// Topic for Observer events fired by Region.sys.mjs. // Topic for Observer events fired by Region.sys.mjs.
REGION_TOPIC = "browser-region-updated"; REGION_TOPIC = "browser-region-updated";
// Values for telemetry. // Values for telemetry.
@@ -175,8 +151,8 @@ class RegionDetector {
return Promise.resolve(); return Promise.resolve();
} }
if (this.#initPromise) { if (this._initPromise) {
return this.#initPromise; return this._initPromise;
} }
if (lazy.cacheBustEnabled) { if (lazy.cacheBustEnabled) {
Services.tm.idleDispatchToMainThread(() => { Services.tm.idleDispatchToMainThread(() => {
@@ -193,12 +169,12 @@ class RegionDetector {
// On startup, ensure the Glean probe knows the home region from preferences. // On startup, ensure the Glean probe knows the home region from preferences.
Glean.region.homeRegion.set(this._home); Glean.region.homeRegion.set(this._home);
} else { } else {
promises.push(this.#idleDispatch(() => this._fetchRegion())); promises.push(this._idleDispatch(() => this._fetchRegion()));
} }
if (lazy.localGeocodingEnabled) { if (lazy.localGeocodingEnabled) {
promises.push(this.#idleDispatch(() => this.#setupRemoteSettings())); promises.push(this._idleDispatch(() => this._setupRemoteSettings()));
} }
return (this.#initPromise = Promise.all(promises)); return (this._initPromise = Promise.all(promises));
} }
/** /**
@@ -222,18 +198,21 @@ class RegionDetector {
} }
/** /**
* Triggers fetching of the users current region. Exposed for tests. * Fetch the users current region.
*
* @returns {string}
* The country_code defining users current region.
*/ */
async _fetchRegion() { async _fetchRegion() {
if (this._retryCount >= MAX_RETRIES) { if (this._retryCount >= MAX_RETRIES) {
return; return null;
} }
let startTime = Date.now(); let startTime = Date.now();
let telemetryResult = this.TELEMETRY.SUCCESS; let telemetryResult = this.TELEMETRY.SUCCESS;
let result = null; let result = null;
try { try {
result = await this.#getRegion(); result = await this._getRegion();
} catch (err) { } catch (err) {
telemetryResult = this.TELEMETRY[err.message] || this.TELEMETRY.ERROR; telemetryResult = this.TELEMETRY[err.message] || this.TELEMETRY.ERROR;
log.error("Failed to fetch region", err); log.error("Failed to fetch region", err);
@@ -247,11 +226,13 @@ class RegionDetector {
let took = Date.now() - startTime; let took = Date.now() - startTime;
if (result) { if (result) {
await this.#storeRegion(result); await this._storeRegion(result);
} }
Glean.region.fetchTime.accumulateSingleSample(took); Glean.region.fetchTime.accumulateSingleSample(took);
Glean.region.fetchResult.accumulateSingleSample(telemetryResult); Glean.region.fetchResult.accumulateSingleSample(telemetryResult);
return result;
} }
/** /**
@@ -260,16 +241,16 @@ class RegionDetector {
* @param {string} region * @param {string} region
* The region to store. * The region to store.
*/ */
async #storeRegion(region) { async _storeRegion(region) {
let isTimezoneUS = this._isUSTimezone(); let isTimezoneUS = this._isUSTimezone();
// If it's a US region, but not a US timezone, we don't store // If it's a US region, but not a US timezone, we don't store
// the value. This works because no region defaults to // the value. This works because no region defaults to
// ZZ (unknown) in nsURLFormatter // ZZ (unknown) in nsURLFormatter
if (region != "US") { if (region != "US") {
this._setCurrentRegion(region); this._setCurrentRegion(region, true);
Glean.region.storeRegionResult.setForRestOfWorld.add(); Glean.region.storeRegionResult.setForRestOfWorld.add();
} else if (isTimezoneUS) { } else if (isTimezoneUS) {
this._setCurrentRegion(region); this._setCurrentRegion(region, true);
Glean.region.storeRegionResult.setForUnitedStates.add(); Glean.region.storeRegionResult.setForUnitedStates.add();
} else { } else {
Glean.region.storeRegionResult.ignoredUnitedStatesIncorrectTimezone.add(); Glean.region.storeRegionResult.ignoredUnitedStatesIncorrectTimezone.add();
@@ -278,7 +259,7 @@ class RegionDetector {
/** /**
* Save the update current region and check if the home region * Save the update current region and check if the home region
* also needs an update. Exposed for tests. * also needs an update.
* *
* @param {string} region * @param {string} region
* The region to store. * The region to store.
@@ -321,12 +302,8 @@ class RegionDetector {
} }
} }
/** // Wrap a string as a nsISupports.
* Wrap a string as a nsISupports. _createSupportsString(data) {
*
* @param {string} data
*/
#createSupportsString(data) {
let string = Cc["@mozilla.org/supports-string;1"].createInstance( let string = Cc["@mozilla.org/supports-string;1"].createInstance(
Ci.nsISupportsString Ci.nsISupportsString
); );
@@ -335,7 +312,7 @@ class RegionDetector {
} }
/** /**
* Save the updated home region and notify observers. Exposed for tests. * Save the updated home region and notify observers.
* *
* @param {string} region * @param {string} region
* The region to store. * The region to store.
@@ -353,7 +330,7 @@ class RegionDetector {
Glean.region.homeRegion.set(region); Glean.region.homeRegion.set(region);
if (notify) { if (notify) {
Services.obs.notifyObservers( Services.obs.notifyObservers(
this.#createSupportsString(region), this._createSupportsString(region),
this.REGION_TOPIC this.REGION_TOPIC
); );
} }
@@ -362,14 +339,14 @@ class RegionDetector {
/** /**
* Make the request to fetch the region from the configured service. * Make the request to fetch the region from the configured service.
*/ */
async #getRegion() { async _getRegion() {
log.info("#getRegion called"); log.info("_getRegion called");
let fetchOpts = { let fetchOpts = {
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
credentials: "omit", credentials: "omit",
}; };
if (lazy.wifiScanningEnabled) { if (lazy.wifiScanningEnabled) {
let wifiData = await this.#fetchWifiData(); let wifiData = await this._fetchWifiData();
if (wifiData) { if (wifiData) {
let postData = JSON.stringify({ wifiAccessPoints: wifiData }); let postData = JSON.stringify({ wifiAccessPoints: wifiData });
log.info("Sending wifi details: ", wifiData); log.info("Sending wifi details: ", wifiData);
@@ -378,17 +355,16 @@ class RegionDetector {
} }
} }
let url = Services.urlFormatter.formatURLPref("browser.region.network.url"); let url = Services.urlFormatter.formatURLPref("browser.region.network.url");
log.info("#getRegion url is: ", url); log.info("_getRegion url is: ", url);
if (!url) { if (!url) {
return null; return null;
} }
try { try {
let req = await this.#fetchTimeout(url, fetchOpts, lazy.networkTimeout); let req = await this._fetchTimeout(url, fetchOpts, lazy.networkTimeout);
// @ts-ignore let res = await req.json();
let res = /** @type {{country_code:string}} */ (await req.json()); log.info("_getRegion returning ", res.country_code);
log.info("_#getRegion returning ", res.country_code);
return res.country_code; return res.country_code;
} catch (err) { } catch (err) {
log.error("Error fetching region", err); log.error("Error fetching region", err);
@@ -401,11 +377,11 @@ class RegionDetector {
* Setup the RemoteSetting client + sync listener and ensure * Setup the RemoteSetting client + sync listener and ensure
* the map files are downloaded. * the map files are downloaded.
*/ */
async #setupRemoteSettings() { async _setupRemoteSettings() {
log.info("#setupRemoteSettings"); log.info("_setupRemoteSettings");
this.#rsClient = lazy.RemoteSettings(COLLECTION_ID); this._rsClient = lazy.RemoteSettings(COLLECTION_ID);
this.#rsClient.on("sync", this._onRegionFilesSync.bind(this)); this._rsClient.on("sync", this._onRegionFilesSync.bind(this));
await this.#ensureRegionFilesDownloaded(); await this._ensureRegionFilesDownloaded();
// Start listening to geolocation events only after // Start listening to geolocation events only after
// we know the maps are downloded. // we know the maps are downloded.
Services.obs.addObserver(this, GEOLOCATION_TOPIC); Services.obs.addObserver(this, GEOLOCATION_TOPIC);
@@ -415,19 +391,17 @@ class RegionDetector {
* Called when RemoteSettings syncs new data, clean up any * Called when RemoteSettings syncs new data, clean up any
* stale attachments and download any new ones. * stale attachments and download any new ones.
* *
* @param {object} syncData * @param {Object} syncData
* Object describing the data that has just been synced. * Object describing the data that has just been synced.
* @param {object} syncData.data
* @param {object[]} syncData.data.deleted
*/ */
async _onRegionFilesSync({ data: { deleted } }) { async _onRegionFilesSync({ data: { deleted } }) {
log.info("_onRegionFilesSync"); log.info("_onRegionFilesSync");
const toDelete = deleted.filter(d => d.attachment); const toDelete = deleted.filter(d => d.attachment);
// Remove local files of deleted records // Remove local files of deleted records
await Promise.all( await Promise.all(
toDelete.map(entry => this.#rsClient.attachments.deleteDownloaded(entry)) toDelete.map(entry => this._rsClient.attachments.deleteDownloaded(entry))
); );
await this.#ensureRegionFilesDownloaded(); await this._ensureRegionFilesDownloaded();
} }
/** /**
@@ -435,52 +409,52 @@ class RegionDetector {
* successfully downloaded set a flag so we can start using them * successfully downloaded set a flag so we can start using them
* for geocoding. * for geocoding.
*/ */
async #ensureRegionFilesDownloaded() { async _ensureRegionFilesDownloaded() {
log.info("#ensureRegionFilesDownloaded"); log.info("_ensureRegionFilesDownloaded");
let records = (await this.#rsClient.get()).filter(d => d.attachment); let records = (await this._rsClient.get()).filter(d => d.attachment);
log.info("#ensureRegionFilesDownloaded", records); log.info("_ensureRegionFilesDownloaded", records);
if (!records.length) { if (!records.length) {
log.info("#ensureRegionFilesDownloaded: Nothing to download"); log.info("_ensureRegionFilesDownloaded: Nothing to download");
return; return;
} }
await Promise.all(records.map(r => this.#rsClient.attachments.download(r))); await Promise.all(records.map(r => this._rsClient.attachments.download(r)));
log.info("#ensureRegionFilesDownloaded complete"); log.info("_ensureRegionFilesDownloaded complete");
this._regionFilesReady = true; this._regionFilesReady = true;
} }
/** /**
* Fetch an attachment from RemoteSettings. * Fetch an attachment from RemoteSettings.
* *
* @param {string} id * @param {String} id
* The id of the record to fetch the attachment from. * The id of the record to fetch the attachment from.
*/ */
async #fetchAttachment(id) { async _fetchAttachment(id) {
let record = (await this.#rsClient.get({ filters: { id } })).pop(); let record = (await this._rsClient.get({ filters: { id } })).pop();
let { buffer } = await this.#rsClient.attachments.download(record); let { buffer } = await this._rsClient.attachments.download(record);
let text = new TextDecoder("utf-8").decode(buffer); let text = new TextDecoder("utf-8").decode(buffer);
return JSON.parse(text); return JSON.parse(text);
} }
/** /**
* Get a map of the world with region definitions. Exposed for tests. * Get a map of the world with region definitions.
*/ */
async _getPlainMap() { async _getPlainMap() {
return this.#fetchAttachment("world"); return this._fetchAttachment("world");
} }
/** /**
* Get a map with the regions expanded by a few km to help * Get a map with the regions expanded by a few km to help
* fallback lookups when a location is not within a region. Exposed for tests. * fallback lookups when a location is not within a region.
*/ */
async _getBufferedMap() { async _getBufferedMap() {
return this.#fetchAttachment("world-buffered"); return this._fetchAttachment("world-buffered");
} }
/** /**
* Gets the users current location using the same reverse IP * Gets the users current location using the same reverse IP
* request that is used for GeoLocation requests. Exposed for tests. * request that is used for GeoLocation requests.
* *
* @returns {Promise<object>} location * @returns {Object} location
* Object representing the user location, with a location key * Object representing the user location, with a location key
* that contains the lat / lng coordinates. * that contains the lat / lng coordinates.
*/ */
@@ -488,22 +462,22 @@ class RegionDetector {
log.info("_getLocation called"); log.info("_getLocation called");
let fetchOpts = { headers: { "Content-Type": "application/json" } }; let fetchOpts = { headers: { "Content-Type": "application/json" } };
let url = Services.urlFormatter.formatURLPref("geo.provider.network.url"); let url = Services.urlFormatter.formatURLPref("geo.provider.network.url");
let req = await this.#fetchTimeout(url, fetchOpts, lazy.networkTimeout); let req = await this._fetchTimeout(url, fetchOpts, lazy.networkTimeout);
let result = await req.json(); let result = await req.json();
log.info("_getLocation returning", result); log.info("_getLocation returning", result);
return result; return result;
} }
/** /**
* Return the users current region using request that is used for GeoLocation * Return the users current region using
* requests. Exposed for tests. * request that is used for GeoLocation requests.
* *
* @returns {Promise<string>} * @returns {String}
* A 2 character string representing a region. * A 2 character string representing a region.
*/ */
async _getRegionLocally() { async _getRegionLocally() {
let { location } = await this._getLocation(); let { location } = await this._getLocation();
return this.#geoCode(location); return this._geoCode(location);
} }
/** /**
@@ -511,28 +485,28 @@ class RegionDetector {
* by looking up the coordinates in geojson map files. * by looking up the coordinates in geojson map files.
* Inspired by https://github.com/mozilla/ichnaea/blob/874e8284f0dfa1868e79aae64e14707eed660efe/ichnaea/geocode.py#L114 * Inspired by https://github.com/mozilla/ichnaea/blob/874e8284f0dfa1868e79aae64e14707eed660efe/ichnaea/geocode.py#L114
* *
* @param {object} location * @param {Object} location
* A location object containing lat + lng coordinates. * A location object containing lat + lng coordinates.
* *
* @returns {Promise<string>} * @returns {String}
* A 2 character string representing a region. * A 2 character string representing a region.
*/ */
async #geoCode(location) { async _geoCode(location) {
let plainMap = await this._getPlainMap(); let plainMap = await this._getPlainMap();
let polygons = this.#getPolygonsContainingPoint(location, plainMap); let polygons = this._getPolygonsContainingPoint(location, plainMap);
if (polygons.length == 1) { if (polygons.length == 1) {
log.info("Found in single exact region"); log.info("Found in single exact region");
return polygons[0].properties.alpha2; return polygons[0].properties.alpha2;
} }
if (polygons.length) { if (polygons.length) {
log.info("Found in ", polygons.length, "overlapping exact regions"); log.info("Found in ", polygons.length, "overlapping exact regions");
return this.#findFurthest(location, polygons); return this._findFurthest(location, polygons);
} }
// We haven't found a match in the exact map, use the buffered map // We haven't found a match in the exact map, use the buffered map
// to see if the point is close to a region. // to see if the point is close to a region.
let bufferedMap = await this._getBufferedMap(); let bufferedMap = await this._getBufferedMap();
polygons = this.#getPolygonsContainingPoint(location, bufferedMap); polygons = this._getPolygonsContainingPoint(location, bufferedMap);
if (polygons.length === 1) { if (polygons.length === 1) {
log.info("Found in single buffered region"); log.info("Found in single buffered region");
@@ -547,7 +521,7 @@ class RegionDetector {
let unBufferedRegions = plainMap.features.filter(feature => let unBufferedRegions = plainMap.features.filter(feature =>
regions.includes(feature.properties.alpha2) regions.includes(feature.properties.alpha2)
); );
return this.#findClosest(location, unBufferedRegions); return this._findClosest(location, unBufferedRegions);
} }
return null; return null;
} }
@@ -557,9 +531,9 @@ class RegionDetector {
* an array of those polygons along with the region that * an array of those polygons along with the region that
* they define * they define
* *
* @param {object} point * @param {Object} point
* A lat + lng coordinate. * A lat + lng coordinate.
* @param {object} map * @param {Object} map
* Geojson object that defined seperate regions with a list * Geojson object that defined seperate regions with a list
* of polygons. * of polygons.
* *
@@ -567,17 +541,17 @@ class RegionDetector {
* An array of polygons that contain the point, along with the * An array of polygons that contain the point, along with the
* region they define. * region they define.
*/ */
#getPolygonsContainingPoint(point, map) { _getPolygonsContainingPoint(point, map) {
let polygons = []; let polygons = [];
for (const feature of map.features) { for (const feature of map.features) {
let coords = feature.geometry.coordinates; let coords = feature.geometry.coordinates;
if (feature.geometry.type === "Polygon") { if (feature.geometry.type === "Polygon") {
if (this.#polygonInPoint(point, coords[0])) { if (this._polygonInPoint(point, coords[0])) {
polygons.push(feature); polygons.push(feature);
} }
} else if (feature.geometry.type === "MultiPolygon") { } else if (feature.geometry.type === "MultiPolygon") {
for (const innerCoords of coords) { for (const innerCoords of coords) {
if (this.#polygonInPoint(point, innerCoords[0])) { if (this._polygonInPoint(point, innerCoords[0])) {
polygons.push(feature); polygons.push(feature);
} }
} }
@@ -590,18 +564,18 @@ class RegionDetector {
* Find the largest distance between a point and any of the points that * Find the largest distance between a point and any of the points that
* make up an array of regions. * make up an array of regions.
* *
* @param {object} location * @param {Object} location
* A lat + lng coordinate. * A lat + lng coordinate.
* @param {Array} regions * @param {Array} regions
* An array of GeoJSON region definitions. * An array of GeoJSON region definitions.
* *
* @returns {string} * @returns {String}
* A 2 character string representing a region. * A 2 character string representing a region.
*/ */
#findFurthest(location, regions) { _findFurthest(location, regions) {
let max = { distance: 0, region: null }; let max = { distance: 0, region: null };
this.#traverse(regions, ({ lat, lng, region }) => { this._traverse(regions, ({ lat, lng, region }) => {
let distance = this.#distanceBetween(location, { lng, lat }); let distance = this._distanceBetween(location, { lng, lat });
if (distance > max.distance) { if (distance > max.distance) {
max = { distance, region }; max = { distance, region };
} }
@@ -613,18 +587,18 @@ class RegionDetector {
* Find the smallest distance between a point and any of the points that * Find the smallest distance between a point and any of the points that
* make up an array of regions. * make up an array of regions.
* *
* @param {object} location * @param {Object} location
* A lat + lng coordinate. * A lat + lng coordinate.
* @param {Array} regions * @param {Array} regions
* An array of GeoJSON region definitions. * An array of GeoJSON region definitions.
* *
* @returns {string} * @returns {String}
* A 2 character string representing a region. * A 2 character string representing a region.
*/ */
#findClosest(location, regions) { _findClosest(location, regions) {
let min = { distance: Infinity, region: null }; let min = { distance: Infinity, region: null };
this.#traverse(regions, ({ lat, lng, region }) => { this._traverse(regions, ({ lat, lng, region }) => {
let distance = this.#distanceBetween(location, { lng, lat }); let distance = this._distanceBetween(location, { lng, lat });
if (distance < min.distance) { if (distance < min.distance) {
min = { distance, region }; min = { distance, region };
} }
@@ -641,7 +615,7 @@ class RegionDetector {
* @param {Function} fun * @param {Function} fun
* Function to call on individual coordinates. * Function to call on individual coordinates.
*/ */
#traverse(regions, fun) { _traverse(regions, fun) {
for (const region of regions) { for (const region of regions) {
if (region.geometry.type === "Polygon") { if (region.geometry.type === "Polygon") {
for (const [lng, lat] of region.geometry.coordinates[0]) { for (const [lng, lat] of region.geometry.coordinates[0]) {
@@ -665,16 +639,15 @@ class RegionDetector {
* that ray intersects with the polygons borders, if it is * that ray intersects with the polygons borders, if it is
* an odd number of times the point is inside the polygon. * an odd number of times the point is inside the polygon.
* *
* @param {object} location * @param {Object} location
* A lat + lng coordinate. * A lat + lng coordinate.
* @param {number} location.lng * @param {Object} polygon
* @param {number} location.lat
* @param {object} poly
* Array of coordinates that define the boundaries of a polygon. * Array of coordinates that define the boundaries of a polygon.
*
* @returns {boolean} * @returns {boolean}
* Whether the point is within the polygon. * Whether the point is within the polygon.
*/ */
#polygonInPoint({ lng, lat }, poly) { _polygonInPoint({ lng, lat }, poly) {
let inside = false; let inside = false;
// For each edge of the polygon. // For each edge of the polygon.
for (let i = 0, j = poly.length - 1; i < poly.length; j = i++) { for (let i = 0, j = poly.length - 1; i < poly.length; j = i++) {
@@ -697,15 +670,15 @@ class RegionDetector {
/** /**
* Find the distance between 2 points. * Find the distance between 2 points.
* *
* @param {object} p1 * @param {Object} p1
* A lat + lng coordinate. * A lat + lng coordinate.
* @param {object} p2 * @param {Object} p2
* A lat + lng coordinate. * A lat + lng coordinate.
* *
* @returns {number} * @returns {int}
* The distance between the 2 points. * The distance between the 2 points.
*/ */
#distanceBetween(p1, p2) { _distanceBetween(p1, p2) {
return Math.hypot(p2.lng - p1.lng, p2.lat - p1.lat); return Math.hypot(p2.lng - p1.lng, p2.lat - p1.lat);
} }
@@ -713,22 +686,17 @@ class RegionDetector {
* A wrapper around fetch that implements a timeout, will throw * A wrapper around fetch that implements a timeout, will throw
* a TIMEOUT error if the request is not completed in time. * a TIMEOUT error if the request is not completed in time.
* *
* @param {string} url * @param {String} url
* The time url to fetch. * The time url to fetch.
* @param {object} opts * @param {Object} opts
* The options object passed to the call to fetch. * The options object passed to the call to fetch.
* @param {number} timeout * @param {int} timeout
* The time in ms to wait for the request to complete. * The time in ms to wait for the request to complete.
* @returns {Promise<Response>}
*/ */
async #fetchTimeout(url, opts, timeout) { async _fetchTimeout(url, opts, timeout) {
let controller = new AbortController(); let controller = new AbortController();
opts.signal = controller.signal; opts.signal = controller.signal;
// Casted to Promise<Response> because `this.#timeout` will not return void, return Promise.race([fetch(url, opts), this._timeout(timeout, controller)]);
// but reject if it wins the race.
return /** @type {Promise<Response>} */ (
Promise.race([fetch(url, opts), this.#timeout(timeout, controller)])
);
} }
/** /**
@@ -736,13 +704,13 @@ class RegionDetector {
* all network requests, but the error will only be returned if it * all network requests, but the error will only be returned if it
* completes first. * completes first.
* *
* @param {number} timeout * @param {int} timeout
* The time in ms to wait for the request to complete. * The time in ms to wait for the request to complete.
* @param {object} controller * @param {Object} controller
* The AbortController passed to the fetch request that * The AbortController passed to the fetch request that
* allows us to abort the request. * allows us to abort the request.
*/ */
async #timeout(timeout, controller) { async _timeout(timeout, controller) {
await new Promise(resolve => lazy.setTimeout(resolve, timeout)); await new Promise(resolve => lazy.setTimeout(resolve, timeout));
if (controller) { if (controller) {
// Yield so it is the TIMEOUT that is returned and not // Yield so it is the TIMEOUT that is returned and not
@@ -752,7 +720,7 @@ class RegionDetector {
throw new Error("TIMEOUT"); throw new Error("TIMEOUT");
} }
async #fetchWifiData() { async _fetchWifiData() {
log.info("fetchWifiData called"); log.info("fetchWifiData called");
this.wifiService = Cc["@mozilla.org/wifi/monitor;1"].getService( this.wifiService = Cc["@mozilla.org/wifi/monitor;1"].getService(
Ci.nsIWifiMonitor Ci.nsIWifiMonitor
@@ -760,7 +728,7 @@ class RegionDetector {
this.wifiService.startWatching(this, false); this.wifiService.startWatching(this, false);
return new Promise(resolve => { return new Promise(resolve => {
this.#wifiDataPromiseResolver = resolve; this._wifiDataPromise = resolve;
}); });
} }
@@ -768,10 +736,10 @@ class RegionDetector {
* If the user is using geolocation then we will see frequent updates * If the user is using geolocation then we will see frequent updates
* debounce those so we aren't processing them constantly. * debounce those so we aren't processing them constantly.
* *
* @returns {boolean} * @returns {bool}
* Whether we should continue the update check. * Whether we should continue the update check.
*/ */
#needsUpdateCheck() { _needsUpdateCheck() {
let sinceUpdate = Math.round(Date.now() / 1000) - lazy.lastUpdated; let sinceUpdate = Math.round(Date.now() / 1000) - lazy.lastUpdated;
let needsUpdate = sinceUpdate >= lazy.updateDebounce; let needsUpdate = sinceUpdate >= lazy.updateDebounce;
if (!needsUpdate) { if (!needsUpdate) {
@@ -783,10 +751,8 @@ class RegionDetector {
/** /**
* Dispatch a promise returning function to the main thread and * Dispatch a promise returning function to the main thread and
* resolve when it is completed. * resolve when it is completed.
*
* @param {() => Promise<void>} fun
*/ */
#idleDispatch(fun) { _idleDispatch(fun) {
return new Promise(resolve => { return new Promise(resolve => {
Services.tm.idleDispatchToMainThread(fun().then(resolve)); Services.tm.idleDispatchToMainThread(fun().then(resolve));
}); });
@@ -794,10 +760,10 @@ class RegionDetector {
/** /**
* timerManager will call this periodically to update the region * timerManager will call this periodically to update the region
* in case the user never users geolocation. Exposed for tests. * in case the user never users geolocation.
*/ */
async _updateTimer() { async _updateTimer() {
if (this.#needsUpdateCheck()) { if (this._needsUpdateCheck()) {
await this._fetchRegion(); await this._fetchRegion();
} }
} }
@@ -806,13 +772,14 @@ class RegionDetector {
* Called when we see geolocation updates. * Called when we see geolocation updates.
* in case the user never users geolocation. * in case the user never users geolocation.
* *
* @param {object} location * @param {Object} location
* A location object containing lat + lng coordinates. * A location object containing lat + lng coordinates.
*
*/ */
async #seenLocation(location) { async _seenLocation(location) {
log.info(`Got location update: ${location.lat}:${location.lng}`); log.info(`Got location update: ${location.lat}:${location.lng}`);
if (this.#needsUpdateCheck()) { if (this._needsUpdateCheck()) {
let region = await this.#geoCode(location); let region = await this._geoCode(location);
if (region) { if (region) {
this._setCurrentRegion(region); this._setCurrentRegion(region);
} }
@@ -821,7 +788,7 @@ class RegionDetector {
onChange(accessPoints) { onChange(accessPoints) {
log.info("onChange called"); log.info("onChange called");
if (!accessPoints || !this.#wifiDataPromiseResolver) { if (!accessPoints || !this._wifiDataPromise) {
return; return;
} }
@@ -830,18 +797,13 @@ class RegionDetector {
this.wifiService = null; this.wifiService = null;
} }
if (this.#wifiDataPromiseResolver) { if (this._wifiDataPromise) {
let data = lazy.LocationHelper.formatWifiAccessPoints(accessPoints); let data = lazy.LocationHelper.formatWifiAccessPoints(accessPoints);
this.#wifiDataPromiseResolver(data); this._wifiDataPromise(data);
this.#wifiDataPromiseResolver = null; this._wifiDataPromise = null;
} }
} }
/**
* Implemented for nsIWifiListener.
*/
onError() {}
/** /**
* A method that tries to determine if this user is in a US geography according * A method that tries to determine if this user is in a US geography according
* to their timezones. * to their timezones.
@@ -871,17 +833,16 @@ class RegionDetector {
observe(aSubject, aTopic) { observe(aSubject, aTopic) {
log.info(`Observed ${aTopic}`); log.info(`Observed ${aTopic}`);
switch (aTopic) { switch (aTopic) {
case GEOLOCATION_TOPIC: { case GEOLOCATION_TOPIC:
// aSubject from GeoLocation.cpp will be a GeoPosition // aSubject from GeoLocation.cpp will be a GeoPosition
// DOM Object, but from tests we will receive a // DOM Object, but from tests we will receive a
// wrappedJSObject so handle both here. // wrappedJSObject so handle both here.
let coords = aSubject.coords || aSubject.wrappedJSObject.coords; let coords = aSubject.coords || aSubject.wrappedJSObject.coords;
this.#seenLocation({ this._seenLocation({
lat: coords.latitude, lat: coords.latitude,
lng: coords.longitude, lng: coords.longitude,
}); });
break; break;
}
} }
} }

View File

@@ -14,7 +14,6 @@ ChromeUtils.defineLazyGetter(lazy, "MigrationUtils", () => {
try { try {
let { MigrationUtils } = ChromeUtils.importESModule( let { MigrationUtils } = ChromeUtils.importESModule(
// eslint-disable-next-line mozilla/no-browser-refs-in-toolkit
"resource:///modules/MigrationUtils.sys.mjs" "resource:///modules/MigrationUtils.sys.mjs"
); );
return MigrationUtils; return MigrationUtils;
@@ -30,7 +29,7 @@ export var ResetProfile = {
/** /**
* Check if reset is supported for the currently running profile. * Check if reset is supported for the currently running profile.
* *
* @returns {boolean} whether reset is supported. * @return boolean whether reset is supported.
*/ */
resetSupported() { resetSupported() {
if (Services.policies && !Services.policies.isAllowed("profileRefresh")) { if (Services.policies && !Services.policies.isAllowed("profileRefresh")) {
@@ -67,8 +66,6 @@ export var ResetProfile = {
/** /**
* Ask the user if they wish to restart the application to reset the profile. * Ask the user if they wish to restart the application to reset the profile.
*
* @param {Window} window
*/ */
async openConfirmationDialog(window) { async openConfirmationDialog(window) {
let win = window; let win = window;

View File

@@ -9,15 +9,7 @@
// This gives us >=2^30 unique timer IDs, enough for 1 per ms for 12.4 days. // This gives us >=2^30 unique timer IDs, enough for 1 per ms for 12.4 days.
var gNextId = 1; // setTimeout and setInterval must return a positive integer var gNextId = 1; // setTimeout and setInterval must return a positive integer
/** var gTimerTable = new Map(); // int -> nsITimer or idleCallback
* @type {Map<number, nsITimer>}
*/
var gTimerTable = new Map();
/**
* @type {Map<number, () => void>}
*/
var gIdleTable = new Map();
// Don't generate this for every timer. // Don't generate this for every timer.
var setTimeout_timerCallbackQI = ChromeUtils.generateQI([ var setTimeout_timerCallbackQI = ChromeUtils.generateQI([
@@ -25,172 +17,123 @@ var setTimeout_timerCallbackQI = ChromeUtils.generateQI([
"nsINamed", "nsINamed",
]); ]);
/**
* @template {any[]} T
*
* @param {(...args: T) => any} callback
* @param {number} milliseconds
* @param {boolean} [isInterval]
* @param {nsIEventTarget} [target]
* @param {T} [args]
*/
function _setTimeoutOrIsInterval( function _setTimeoutOrIsInterval(
callback, aCallback,
milliseconds, aMilliseconds,
isInterval, aIsInterval,
target, aTarget,
args aArgs
) { ) {
if (typeof callback !== "function") { if (typeof aCallback !== "function") {
throw new Error( throw new Error(
`callback is not a function in ${ `callback is not a function in ${
isInterval ? "setInterval" : "setTimeout" aIsInterval ? "setInterval" : "setTimeout"
}` }`
); );
} }
let id = gNextId++; let id = gNextId++;
let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
if (target) { if (aTarget) {
timer.target = target; timer.target = aTarget;
} }
let callbackObj = { let callback = {
QueryInterface: setTimeout_timerCallbackQI, QueryInterface: setTimeout_timerCallbackQI,
// nsITimerCallback // nsITimerCallback
notify() { notify() {
if (!isInterval) { if (!aIsInterval) {
gTimerTable.delete(id); gTimerTable.delete(id);
} }
callback.apply(null, args); aCallback.apply(null, aArgs);
}, },
// nsINamed // nsINamed
get name() { get name() {
return `${ return `${
isInterval ? "setInterval" : "setTimeout" aIsInterval ? "setInterval" : "setTimeout"
}() for ${Cu.getDebugName(callback)}`; }() for ${Cu.getDebugName(aCallback)}`;
}, },
}; };
timer.initWithCallback( timer.initWithCallback(
callbackObj, callback,
milliseconds, aMilliseconds,
isInterval ? timer.TYPE_REPEATING_SLACK : timer.TYPE_ONE_SHOT aIsInterval ? timer.TYPE_REPEATING_SLACK : timer.TYPE_ONE_SHOT
); );
gTimerTable.set(id, timer); gTimerTable.set(id, timer);
return id; return id;
} }
/** export function setTimeout(aCallback, aMilliseconds, ...aArgs) {
* Sets a timeout. return _setTimeoutOrIsInterval(aCallback, aMilliseconds, false, null, aArgs);
*
* @template {any[]} T
*
* @param {(...args: T) => any} callback
* @param {number} milliseconds
* @param {T} [args]
*/
export function setTimeout(callback, milliseconds, ...args) {
return _setTimeoutOrIsInterval(callback, milliseconds, false, null, args);
} }
/** export function setTimeoutWithTarget(
* Sets a timeout with a given event target. aCallback,
* aMilliseconds,
* @template {any[]} T aTarget,
* ...aArgs
* @param {(...args: T) => any} callback ) {
* @param {number} milliseconds return _setTimeoutOrIsInterval(
* @param {nsIEventTarget} target aCallback,
* @param {T} [args] aMilliseconds,
*/ false,
export function setTimeoutWithTarget(callback, milliseconds, target, ...args) { aTarget,
return _setTimeoutOrIsInterval(callback, milliseconds, false, target, args); aArgs
);
} }
/** export function setInterval(aCallback, aMilliseconds, ...aArgs) {
* Sets an interval timer. return _setTimeoutOrIsInterval(aCallback, aMilliseconds, true, null, aArgs);
*
* @template {any[]} T
*
* @param {(...args: T) => any} callback
* @param {number} milliseconds
* @param {T} [args]
*/
export function setInterval(callback, milliseconds, ...args) {
return _setTimeoutOrIsInterval(callback, milliseconds, true, null, args);
} }
/** export function setIntervalWithTarget(
* Sets an interval timer. aCallback,
* aMilliseconds,
* @template {any[]} T aTarget,
* ...aArgs
* @param {(...args: T) => any} callback ) {
* @param {number} milliseconds return _setTimeoutOrIsInterval(
* @param {nsIEventTarget} target aCallback,
* @param {T} [args] aMilliseconds,
*/ true,
export function setIntervalWithTarget(callback, milliseconds, target, ...args) { aTarget,
return _setTimeoutOrIsInterval(callback, milliseconds, true, target, args); aArgs
);
} }
/** function clear(aId) {
* Clears the given timer. if (gTimerTable.has(aId)) {
* gTimerTable.get(aId).cancel();
* @param {number} id gTimerTable.delete(aId);
*/
function clear(id) {
if (gTimerTable.has(id)) {
gTimerTable.get(id).cancel();
gTimerTable.delete(id);
} }
} }
/**
* Clears the given timer.
*/
export var clearInterval = clear; export var clearInterval = clear;
/**
* Clears the given timer.
*/
export var clearTimeout = clear; export var clearTimeout = clear;
/** export function requestIdleCallback(aCallback, aOptions) {
* Dispatches the given callback to the main thread when it would be otherwise if (typeof aCallback !== "function") {
* idle. The callback may be canceled via `cancelIdleCallback` - the idle
* dispatch will still happen but it won't be called.
*
* @param {() => void} callback
* @param {object} options
*/
export function requestIdleCallback(callback, options) {
if (typeof callback !== "function") {
throw new Error("callback is not a function in requestIdleCallback"); throw new Error("callback is not a function in requestIdleCallback");
} }
let id = gNextId++; let id = gNextId++;
ChromeUtils.idleDispatch(() => { let callback = (...aArgs) => {
if (gIdleTable.has(id)) { if (gTimerTable.has(id)) {
gIdleTable.delete(id); gTimerTable.delete(id);
callback(); aCallback(...aArgs);
} }
}, options); };
gIdleTable.set(id, callback);
ChromeUtils.idleDispatch(callback, aOptions);
gTimerTable.set(id, callback);
return id; return id;
} }
/** export function cancelIdleCallback(aId) {
* Cancels the given idle callback if (gTimerTable.has(aId)) {
* gTimerTable.delete(aId);
* @param {number} id
*/
export function cancelIdleCallback(id) {
if (gIdleTable.has(id)) {
gIdleTable.delete(id);
} }
} }

View File

@@ -0,0 +1,29 @@
"use strict";
var { ObjectUtils } = ChromeUtils.importESModule(
"resource://gre/modules/ObjectUtils.sys.mjs"
);
var { PromiseTestUtils } = ChromeUtils.importESModule(
"resource://testing-common/PromiseTestUtils.sys.mjs"
);
add_task(async function test_strict() {
let loose = { a: 1 };
let strict = ObjectUtils.strict(loose);
loose.a; // Should not throw.
loose.b || undefined; // Should not throw.
strict.a; // Should not throw.
PromiseTestUtils.expectUncaughtRejection(/No such property: "b"/);
Assert.throws(() => strict.b, /No such property: "b"/);
"b" in strict; // Should not throw.
strict.b = 2;
strict.b; // Should not throw.
PromiseTestUtils.expectUncaughtRejection(/No such property: "c"/);
Assert.throws(() => strict.c, /No such property: "c"/);
"c" in strict; // Should not throw.
loose.c = 3;
strict.c; // Should not throw.
});

View File

@@ -112,7 +112,8 @@ add_task(async function test_invalid_url() {
RegionTestUtils.REGION_URL_PREF, RegionTestUtils.REGION_URL_PREF,
"http://localhost:0" "http://localhost:0"
); );
await Region._fetchRegion(); let result = await Region._fetchRegion();
Assert.ok(!result, "Should return no result");
await checkTelemetry(Region.TELEMETRY.NO_RESULT); await checkTelemetry(Region.TELEMETRY.NO_RESULT);
}); });
@@ -122,7 +123,8 @@ add_task(async function test_invalid_json() {
RegionTestUtils.REGION_URL_PREF, RegionTestUtils.REGION_URL_PREF,
'data:application/json,{"country_code"' 'data:application/json,{"country_code"'
); );
await Region._fetchRegion(); let result = await Region._fetchRegion();
Assert.ok(!result, "Should return no result");
await checkTelemetry(Region.TELEMETRY.NO_RESULT); await checkTelemetry(Region.TELEMETRY.NO_RESULT);
}); });
@@ -139,7 +141,9 @@ add_task(async function test_timeout() {
}); });
}); });
await Region._fetchRegion(); let result = await Region._fetchRegion();
Assert.equal(result, null, "Region fetch should return null");
await checkTelemetry(Region.TELEMETRY.TIMEOUT); await checkTelemetry(Region.TELEMETRY.TIMEOUT);
await cleanup(srv); await cleanup(srv);
}); });

View File

@@ -87,6 +87,8 @@ skip-if = ["os == 'android'"]
["test_ObjectUtils.js"] ["test_ObjectUtils.js"]
["test_ObjectUtils_strict.js"]
["test_PermissionsUtils.js"] ["test_PermissionsUtils.js"]
["test_Preferences.js"] ["test_Preferences.js"]