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:
@@ -391,7 +391,7 @@ const rollouts = [
|
||||
"toolkit/components/workerloader/require.js",
|
||||
"toolkit/content/**",
|
||||
"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/subprocess/**",
|
||||
"toolkit/modules/tests/**",
|
||||
@@ -555,10 +555,7 @@ const rollouts = [
|
||||
"toolkit/components/workerloader/require.js",
|
||||
"toolkit/content/**",
|
||||
"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/sessionstore/**",
|
||||
"toolkit/modules/subprocess/**",
|
||||
"toolkit/modules/tests/**",
|
||||
"toolkit/modules/**",
|
||||
"toolkit/mozapps/downloads/**",
|
||||
"toolkit/mozapps/extensions/**",
|
||||
"toolkit/mozapps/handling/**",
|
||||
|
||||
@@ -4,12 +4,6 @@
|
||||
* 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/. */
|
||||
|
||||
const Timer = Components.Constructor(
|
||||
"@mozilla.org/timer;1",
|
||||
"nsITimer",
|
||||
"initWithCallback"
|
||||
);
|
||||
|
||||
/**
|
||||
* 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
|
||||
@@ -81,71 +75,70 @@ const Timer = Components.Constructor(
|
||||
* saveDeferredTask.finalize().then(() => OS.File.remove(...))
|
||||
* .then(null, Components.utils.reportError);
|
||||
*/
|
||||
export class DeferredTask {
|
||||
/**
|
||||
|
||||
// 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 {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]
|
||||
* @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.
|
||||
*/
|
||||
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}`;
|
||||
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.
|
||||
*/
|
||||
#taskFn;
|
||||
_taskFn: null,
|
||||
|
||||
/**
|
||||
* Time between executions, in milliseconds.
|
||||
*/
|
||||
#delayMs;
|
||||
|
||||
/**
|
||||
* The idle timeout wait.
|
||||
*
|
||||
* @type {number|undefined}
|
||||
*/
|
||||
#idleTimeoutMs = undefined;
|
||||
|
||||
/**
|
||||
* The name of the caller that created the deferred task.
|
||||
*/
|
||||
#caller;
|
||||
_delayMs: null,
|
||||
|
||||
/**
|
||||
* Indicates whether the task is currently requested to start again later,
|
||||
* regardless of whether it is currently running.
|
||||
*/
|
||||
get isArmed() {
|
||||
return this.#armed;
|
||||
}
|
||||
#armed = false;
|
||||
return this._armed;
|
||||
},
|
||||
_armed: false,
|
||||
|
||||
/**
|
||||
* Indicates whether the task is currently running. This is always true when
|
||||
@@ -154,59 +147,50 @@ export class DeferredTask {
|
||||
*/
|
||||
get isRunning() {
|
||||
return !!this._runningPromise;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Promise resolved when the current execution of the task terminates, or null
|
||||
* 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
|
||||
* 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.
|
||||
*/
|
||||
#startTimer() {
|
||||
_startTimer() {
|
||||
let callback, timer;
|
||||
if (this.#idleTimeoutMs === 0) {
|
||||
callback = () => this.#timerCallback();
|
||||
if (this._timeoutMs === 0) {
|
||||
callback = () => this._timerCallback();
|
||||
} else {
|
||||
callback = () => {
|
||||
this._startIdleDispatch(() => {
|
||||
// #timer could have changed by now:
|
||||
// _timer could have changed by now:
|
||||
// - to null if disarm() or finalize() has been called.
|
||||
// - to a new nsITimer if disarm() was called, followed by arm().
|
||||
// In either case, don't invoke #timerCallback any more.
|
||||
if (this.#timer === timer) {
|
||||
this.#timerCallback();
|
||||
// In either case, don't invoke _timerCallback any more.
|
||||
if (this._timer === timer) {
|
||||
this._timerCallback();
|
||||
}
|
||||
}, this.#idleTimeoutMs);
|
||||
}, this._timeoutMs);
|
||||
};
|
||||
}
|
||||
timer = new Timer(callback, this.#delayMs, Ci.nsITimer.TYPE_ONE_SHOT);
|
||||
this.#timer = timer;
|
||||
}
|
||||
timer = new Timer(callback, this._delayMs, Ci.nsITimer.TYPE_ONE_SHOT);
|
||||
this._timer = timer;
|
||||
},
|
||||
|
||||
/**
|
||||
* Dispatches idle task. Can be overridden for testing by test_DeferredTask.
|
||||
*
|
||||
* @param {IdleRequestCallback} callback
|
||||
* @param {number} timeout
|
||||
*/
|
||||
_startIdleDispatch(callback, timeout) {
|
||||
ChromeUtils.idleDispatch(callback, { timeout });
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 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
|
||||
* 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,
|
||||
* doing that would often result in duplicate processing or logging. If
|
||||
* 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.
|
||||
*/
|
||||
arm() {
|
||||
if (this.#finalized) {
|
||||
if (this._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,
|
||||
// because this will be handled by the timer callback itself. Also, the
|
||||
// timer is not restarted in case it is already running.
|
||||
if (!this._runningPromise && !this.#timer) {
|
||||
this.#startTimer();
|
||||
}
|
||||
if (!this._runningPromise && !this._timer) {
|
||||
this._startTimer();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
disarm() {
|
||||
this.#armed = false;
|
||||
if (this.#timer) {
|
||||
this._armed = false;
|
||||
if (this._timer) {
|
||||
// Calling the "cancel" method and discarding the timer reference makes
|
||||
// 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.
|
||||
this.#timer.cancel();
|
||||
this.#timer = null;
|
||||
}
|
||||
this._timer.cancel();
|
||||
this._timer = null;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 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
|
||||
* a resolved promise.
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
* Resolves when the last execution of the task is finished.
|
||||
* @return {Promise}
|
||||
* @resolves After the last execution of the task is finished.
|
||||
* @rejects Never.
|
||||
*/
|
||||
finalize() {
|
||||
if (this.#finalized) {
|
||||
if (this._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
|
||||
// scheduled for execution. Cancel the timer and run the task immediately,
|
||||
// so we don't risk blocking async shutdown longer than necessary.
|
||||
if (this.#timer) {
|
||||
if (this._timer) {
|
||||
this.disarm();
|
||||
this.#timerCallback();
|
||||
this._timerCallback();
|
||||
}
|
||||
|
||||
// Wait for the operation to be completed, or resolve immediately.
|
||||
@@ -296,20 +281,20 @@ export class DeferredTask {
|
||||
return this._runningPromise;
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
#finalized = false;
|
||||
},
|
||||
_finalized: false,
|
||||
|
||||
/**
|
||||
* Whether the DeferredTask has been finalized, and it cannot be armed anymore.
|
||||
*/
|
||||
get isFinalized() {
|
||||
return this.#finalized;
|
||||
}
|
||||
return this._finalized;
|
||||
},
|
||||
|
||||
/**
|
||||
* Timer callback used to run the delayed task.
|
||||
*/
|
||||
#timerCallback() {
|
||||
_timerCallback() {
|
||||
let runningDeferred = Promise.withResolvers();
|
||||
|
||||
// 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
|
||||
// means that the assignment of "this._runningPromise" must complete before
|
||||
// the task gets a chance to start.
|
||||
this.#timer = null;
|
||||
this.#armed = false;
|
||||
this._timer = null;
|
||||
this._armed = false;
|
||||
this._runningPromise = runningDeferred.promise;
|
||||
|
||||
runningDeferred.resolve(
|
||||
(async () => {
|
||||
// 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
|
||||
// determine if we should restart the task again.
|
||||
if (this.#armed) {
|
||||
if (!this.#finalized) {
|
||||
this.#startTimer();
|
||||
if (this._armed) {
|
||||
if (!this._finalized) {
|
||||
this._startTimer();
|
||||
} else {
|
||||
// Execute the task again immediately, for the last time. The isArmed
|
||||
// property should return false while the task is running, and should
|
||||
// remain false after the last execution terminates.
|
||||
this.#armed = false;
|
||||
await this.#runTask();
|
||||
this._armed = false;
|
||||
await this._runTask();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -345,23 +330,23 @@ export class DeferredTask {
|
||||
this._runningPromise = null;
|
||||
})().catch(console.error)
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Executes the associated task and catches exceptions.
|
||||
*/
|
||||
async #runTask() {
|
||||
async _runTask() {
|
||||
let startTime = Cu.now();
|
||||
try {
|
||||
await this.#taskFn();
|
||||
await this._taskFn();
|
||||
} catch (ex) {
|
||||
console.error(ex);
|
||||
} finally {
|
||||
ChromeUtils.addProfilerMarker(
|
||||
"DeferredTask",
|
||||
{ startTime },
|
||||
this.#caller
|
||||
this._caller
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -10,75 +10,43 @@ ChromeUtils.defineESModuleGetters(lazy, {
|
||||
"resource://services-settings/RemoteSettingsClient.sys.mjs",
|
||||
});
|
||||
|
||||
/**
|
||||
* @typedef {import("../../services/settings/RemoteSettingsClient.sys.mjs").RemoteSettingsClient} RemoteSettingsClient
|
||||
*/
|
||||
|
||||
const SETTINGS_IGNORELIST_KEY = "hijack-blocklists";
|
||||
|
||||
/**
|
||||
* A remote settings wrapper for the ignore lists from the hijack-blocklists
|
||||
* collection.
|
||||
*/
|
||||
class IgnoreListsManager {
|
||||
/**
|
||||
* @type {RemoteSettingsClient}
|
||||
*/
|
||||
#ignoreListSettings;
|
||||
|
||||
/**
|
||||
* Initializes the manager, if it is not already initialised.
|
||||
*/
|
||||
#init() {
|
||||
if (!this.#ignoreListSettings) {
|
||||
this.#ignoreListSettings = lazy.RemoteSettings(SETTINGS_IGNORELIST_KEY);
|
||||
async 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) {
|
||||
this.#init();
|
||||
await this.init();
|
||||
|
||||
// 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.
|
||||
this.#ignoreListSettings.on("sync", listener);
|
||||
this._ignoreListSettings.on("sync", listener);
|
||||
|
||||
return settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribes from updates to the collection.
|
||||
*
|
||||
* @param {Function} listener
|
||||
*/
|
||||
unsubscribe(listener) {
|
||||
if (!this.#ignoreListSettings) {
|
||||
if (!this._ignoreListSettings) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.#ignoreListSettings.off("sync", listener);
|
||||
this._ignoreListSettings.off("sync", listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {Promise<object[]>}
|
||||
*/
|
||||
#getSettingsPromise;
|
||||
|
||||
async #getIgnoreList() {
|
||||
if (this.#getSettingsPromise) {
|
||||
return this.#getSettingsPromise;
|
||||
async _getIgnoreList() {
|
||||
if (this._getSettingsPromise) {
|
||||
return this._getSettingsPromise;
|
||||
}
|
||||
|
||||
const settings = await (this.#getSettingsPromise =
|
||||
this.#getIgnoreListSettings());
|
||||
this.#getSettingsPromise = undefined;
|
||||
const settings = await (this._getSettingsPromise =
|
||||
this._getIgnoreListSettings());
|
||||
delete this._getSettingsPromise;
|
||||
return settings;
|
||||
}
|
||||
|
||||
@@ -94,14 +62,14 @@ class IgnoreListsManager {
|
||||
*
|
||||
* @param {boolean} [firstTime]
|
||||
* 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
|
||||
* could be obtained.
|
||||
*/
|
||||
async #getIgnoreListSettings(firstTime = true) {
|
||||
async _getIgnoreListSettings(firstTime = true) {
|
||||
let result = [];
|
||||
try {
|
||||
result = await this.#ignoreListSettings.get({
|
||||
result = await this._ignoreListSettings.get({
|
||||
verifySignature: true,
|
||||
});
|
||||
} catch (ex) {
|
||||
@@ -110,9 +78,9 @@ class IgnoreListsManager {
|
||||
firstTime
|
||||
) {
|
||||
// The local database is invalid, try and reset it.
|
||||
await this.#ignoreListSettings.db.clear();
|
||||
await this._ignoreListSettings.db.clear();
|
||||
// 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
|
||||
// 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();
|
||||
|
||||
@@ -45,8 +45,7 @@ export var OSKeyStore = {
|
||||
/**
|
||||
* Consider the module is initialized as locked. OS might unlock without a
|
||||
* prompt.
|
||||
*
|
||||
* @type {boolean}
|
||||
* @type {Boolean}
|
||||
*/
|
||||
_isLocked: true,
|
||||
|
||||
@@ -153,7 +152,7 @@ export var OSKeyStore = {
|
||||
* the key storage. If we start creating keys on macOS by running
|
||||
* this code we'll potentially have to do extra work to cleanup
|
||||
* 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.
|
||||
* auth_details: {String?} Details of the authentication result.
|
||||
*/
|
||||
|
||||
@@ -19,20 +19,36 @@ export var ObjectUtils = {
|
||||
* `JSON.stringify` is not designed to be used for this purpose; objects may
|
||||
* have ambiguous `toJSON()` implementations that would influence the test.
|
||||
*
|
||||
* @param {any} a
|
||||
* Object or value to be compared.
|
||||
* @param {any} b
|
||||
* Object or value to be compared.
|
||||
* @param a (mixed) Object or value to be compared.
|
||||
* @param b (mixed) Object or value to be compared.
|
||||
* @return Boolean Whether the objects are deep equal.
|
||||
*/
|
||||
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
|
||||
* enumerable properties, or a falsy primitive; `false` otherwise.
|
||||
*
|
||||
* @param {any} obj
|
||||
*/
|
||||
isEmpty(obj) {
|
||||
if (!obj) {
|
||||
@@ -56,12 +72,6 @@ export var ObjectUtils = {
|
||||
// Copyright (c) 2009 Thomas Robinson <280north.com>
|
||||
// MIT license: http://opensource.org/licenses/MIT
|
||||
|
||||
/**
|
||||
* Tests objects & values for deep equality.
|
||||
*
|
||||
* @param {any} a
|
||||
* @param {any} b
|
||||
*/
|
||||
function _deepEqual(a, b) {
|
||||
// The numbering below refers to sections in the CommonJS spec.
|
||||
|
||||
@@ -112,41 +122,18 @@ function _deepEqual(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) {
|
||||
return Object.prototype.toString.call(object) == "[object " + type + "]";
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks is see if the value is undefined or null.
|
||||
*
|
||||
* @param {any} value
|
||||
*/
|
||||
function isUndefinedOrNull(value) {
|
||||
return value === null || value === undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks to see if the object is an arguments object.
|
||||
*
|
||||
* @param {object} object
|
||||
*/
|
||||
function isArguments(object) {
|
||||
return instanceOf(object, "Arguments");
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares objects for equivalence.
|
||||
*
|
||||
* @param {object} a
|
||||
* @param {object} b
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function objEquiv(a, b) {
|
||||
if (isUndefinedOrNull(a) || isUndefinedOrNull(b)) {
|
||||
return false;
|
||||
@@ -208,3 +195,21 @@ function objEquiv(a, b) {
|
||||
}
|
||||
|
||||
// ... 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;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -12,10 +12,6 @@ ChromeUtils.defineESModuleGetters(lazy, {
|
||||
setTimeout: "resource://gre/modules/Timer.sys.mjs",
|
||||
});
|
||||
|
||||
/**
|
||||
* @typedef {import("../../services/settings/RemoteSettingsClient.sys.mjs").RemoteSettingsClient} RemoteSettingsClient
|
||||
*/
|
||||
|
||||
XPCOMUtils.defineLazyPreferenceGetter(
|
||||
lazy,
|
||||
"wifiScanningEnabled",
|
||||
@@ -117,42 +113,22 @@ let inChildProcess =
|
||||
* specific customisations.
|
||||
*/
|
||||
class RegionDetector {
|
||||
/**
|
||||
* The users home location. Accessible to tests. Production code should use
|
||||
* the `home` getter.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
// The users home location.
|
||||
_home = null;
|
||||
/**
|
||||
* The most recent location the user was detected. Production code should use
|
||||
* the `current` getter.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
// The most recent location the user was detected.
|
||||
_current = null;
|
||||
/**
|
||||
* The RemoteSettings client used to sync region files.
|
||||
*
|
||||
* @type {RemoteSettingsClient}
|
||||
*/
|
||||
#rsClient;
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
// The RemoteSettings client used to sync region files.
|
||||
_rsClient = null;
|
||||
// Keep track of the wifi data across listener events.
|
||||
_wifiDataPromise = null;
|
||||
// Keep track of how many times we have tried to fetch
|
||||
// the users region during failure.
|
||||
_retryCount = 0;
|
||||
/**
|
||||
* @type {Promise}
|
||||
* Allow tests to wait for init to be complete.
|
||||
*/
|
||||
#initPromise = null;
|
||||
_initPromise = null;
|
||||
// Topic for Observer events fired by Region.sys.mjs.
|
||||
REGION_TOPIC = "browser-region-updated";
|
||||
// Values for telemetry.
|
||||
@@ -175,8 +151,8 @@ class RegionDetector {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
if (this.#initPromise) {
|
||||
return this.#initPromise;
|
||||
if (this._initPromise) {
|
||||
return this._initPromise;
|
||||
}
|
||||
if (lazy.cacheBustEnabled) {
|
||||
Services.tm.idleDispatchToMainThread(() => {
|
||||
@@ -193,12 +169,12 @@ class RegionDetector {
|
||||
// On startup, ensure the Glean probe knows the home region from preferences.
|
||||
Glean.region.homeRegion.set(this._home);
|
||||
} else {
|
||||
promises.push(this.#idleDispatch(() => this._fetchRegion()));
|
||||
promises.push(this._idleDispatch(() => this._fetchRegion()));
|
||||
}
|
||||
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() {
|
||||
if (this._retryCount >= MAX_RETRIES) {
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
let startTime = Date.now();
|
||||
let telemetryResult = this.TELEMETRY.SUCCESS;
|
||||
let result = null;
|
||||
|
||||
try {
|
||||
result = await this.#getRegion();
|
||||
result = await this._getRegion();
|
||||
} catch (err) {
|
||||
telemetryResult = this.TELEMETRY[err.message] || this.TELEMETRY.ERROR;
|
||||
log.error("Failed to fetch region", err);
|
||||
@@ -247,11 +226,13 @@ class RegionDetector {
|
||||
|
||||
let took = Date.now() - startTime;
|
||||
if (result) {
|
||||
await this.#storeRegion(result);
|
||||
await this._storeRegion(result);
|
||||
}
|
||||
Glean.region.fetchTime.accumulateSingleSample(took);
|
||||
|
||||
Glean.region.fetchResult.accumulateSingleSample(telemetryResult);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -260,16 +241,16 @@ class RegionDetector {
|
||||
* @param {string} region
|
||||
* The region to store.
|
||||
*/
|
||||
async #storeRegion(region) {
|
||||
async _storeRegion(region) {
|
||||
let isTimezoneUS = this._isUSTimezone();
|
||||
// If it's a US region, but not a US timezone, we don't store
|
||||
// the value. This works because no region defaults to
|
||||
// ZZ (unknown) in nsURLFormatter
|
||||
if (region != "US") {
|
||||
this._setCurrentRegion(region);
|
||||
this._setCurrentRegion(region, true);
|
||||
Glean.region.storeRegionResult.setForRestOfWorld.add();
|
||||
} else if (isTimezoneUS) {
|
||||
this._setCurrentRegion(region);
|
||||
this._setCurrentRegion(region, true);
|
||||
Glean.region.storeRegionResult.setForUnitedStates.add();
|
||||
} else {
|
||||
Glean.region.storeRegionResult.ignoredUnitedStatesIncorrectTimezone.add();
|
||||
@@ -278,7 +259,7 @@ class RegionDetector {
|
||||
|
||||
/**
|
||||
* 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
|
||||
* The region to store.
|
||||
@@ -321,12 +302,8 @@ class RegionDetector {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap a string as a nsISupports.
|
||||
*
|
||||
* @param {string} data
|
||||
*/
|
||||
#createSupportsString(data) {
|
||||
// Wrap a string as a nsISupports.
|
||||
_createSupportsString(data) {
|
||||
let string = Cc["@mozilla.org/supports-string;1"].createInstance(
|
||||
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
|
||||
* The region to store.
|
||||
@@ -353,7 +330,7 @@ class RegionDetector {
|
||||
Glean.region.homeRegion.set(region);
|
||||
if (notify) {
|
||||
Services.obs.notifyObservers(
|
||||
this.#createSupportsString(region),
|
||||
this._createSupportsString(region),
|
||||
this.REGION_TOPIC
|
||||
);
|
||||
}
|
||||
@@ -362,14 +339,14 @@ class RegionDetector {
|
||||
/**
|
||||
* Make the request to fetch the region from the configured service.
|
||||
*/
|
||||
async #getRegion() {
|
||||
log.info("#getRegion called");
|
||||
async _getRegion() {
|
||||
log.info("_getRegion called");
|
||||
let fetchOpts = {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
credentials: "omit",
|
||||
};
|
||||
if (lazy.wifiScanningEnabled) {
|
||||
let wifiData = await this.#fetchWifiData();
|
||||
let wifiData = await this._fetchWifiData();
|
||||
if (wifiData) {
|
||||
let postData = JSON.stringify({ wifiAccessPoints: wifiData });
|
||||
log.info("Sending wifi details: ", wifiData);
|
||||
@@ -378,17 +355,16 @@ class RegionDetector {
|
||||
}
|
||||
}
|
||||
let url = Services.urlFormatter.formatURLPref("browser.region.network.url");
|
||||
log.info("#getRegion url is: ", url);
|
||||
log.info("_getRegion url is: ", url);
|
||||
|
||||
if (!url) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
let req = await this.#fetchTimeout(url, fetchOpts, lazy.networkTimeout);
|
||||
// @ts-ignore
|
||||
let res = /** @type {{country_code:string}} */ (await req.json());
|
||||
log.info("_#getRegion returning ", res.country_code);
|
||||
let req = await this._fetchTimeout(url, fetchOpts, lazy.networkTimeout);
|
||||
let res = await req.json();
|
||||
log.info("_getRegion returning ", res.country_code);
|
||||
return res.country_code;
|
||||
} catch (err) {
|
||||
log.error("Error fetching region", err);
|
||||
@@ -401,11 +377,11 @@ class RegionDetector {
|
||||
* Setup the RemoteSetting client + sync listener and ensure
|
||||
* the map files are downloaded.
|
||||
*/
|
||||
async #setupRemoteSettings() {
|
||||
log.info("#setupRemoteSettings");
|
||||
this.#rsClient = lazy.RemoteSettings(COLLECTION_ID);
|
||||
this.#rsClient.on("sync", this._onRegionFilesSync.bind(this));
|
||||
await this.#ensureRegionFilesDownloaded();
|
||||
async _setupRemoteSettings() {
|
||||
log.info("_setupRemoteSettings");
|
||||
this._rsClient = lazy.RemoteSettings(COLLECTION_ID);
|
||||
this._rsClient.on("sync", this._onRegionFilesSync.bind(this));
|
||||
await this._ensureRegionFilesDownloaded();
|
||||
// Start listening to geolocation events only after
|
||||
// we know the maps are downloded.
|
||||
Services.obs.addObserver(this, GEOLOCATION_TOPIC);
|
||||
@@ -415,19 +391,17 @@ class RegionDetector {
|
||||
* Called when RemoteSettings syncs new data, clean up any
|
||||
* stale attachments and download any new ones.
|
||||
*
|
||||
* @param {object} syncData
|
||||
* @param {Object} syncData
|
||||
* Object describing the data that has just been synced.
|
||||
* @param {object} syncData.data
|
||||
* @param {object[]} syncData.data.deleted
|
||||
*/
|
||||
async _onRegionFilesSync({ data: { deleted } }) {
|
||||
log.info("_onRegionFilesSync");
|
||||
const toDelete = deleted.filter(d => d.attachment);
|
||||
// Remove local files of deleted records
|
||||
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
|
||||
* for geocoding.
|
||||
*/
|
||||
async #ensureRegionFilesDownloaded() {
|
||||
log.info("#ensureRegionFilesDownloaded");
|
||||
let records = (await this.#rsClient.get()).filter(d => d.attachment);
|
||||
log.info("#ensureRegionFilesDownloaded", records);
|
||||
async _ensureRegionFilesDownloaded() {
|
||||
log.info("_ensureRegionFilesDownloaded");
|
||||
let records = (await this._rsClient.get()).filter(d => d.attachment);
|
||||
log.info("_ensureRegionFilesDownloaded", records);
|
||||
if (!records.length) {
|
||||
log.info("#ensureRegionFilesDownloaded: Nothing to download");
|
||||
log.info("_ensureRegionFilesDownloaded: Nothing to download");
|
||||
return;
|
||||
}
|
||||
await Promise.all(records.map(r => this.#rsClient.attachments.download(r)));
|
||||
log.info("#ensureRegionFilesDownloaded complete");
|
||||
await Promise.all(records.map(r => this._rsClient.attachments.download(r)));
|
||||
log.info("_ensureRegionFilesDownloaded complete");
|
||||
this._regionFilesReady = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch an attachment from RemoteSettings.
|
||||
*
|
||||
* @param {string} id
|
||||
* @param {String} id
|
||||
* The id of the record to fetch the attachment from.
|
||||
*/
|
||||
async #fetchAttachment(id) {
|
||||
let record = (await this.#rsClient.get({ filters: { id } })).pop();
|
||||
let { buffer } = await this.#rsClient.attachments.download(record);
|
||||
async _fetchAttachment(id) {
|
||||
let record = (await this._rsClient.get({ filters: { id } })).pop();
|
||||
let { buffer } = await this._rsClient.attachments.download(record);
|
||||
let text = new TextDecoder("utf-8").decode(buffer);
|
||||
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() {
|
||||
return this.#fetchAttachment("world");
|
||||
return this._fetchAttachment("world");
|
||||
}
|
||||
|
||||
/**
|
||||
* 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() {
|
||||
return this.#fetchAttachment("world-buffered");
|
||||
return this._fetchAttachment("world-buffered");
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* that contains the lat / lng coordinates.
|
||||
*/
|
||||
@@ -488,22 +462,22 @@ class RegionDetector {
|
||||
log.info("_getLocation called");
|
||||
let fetchOpts = { headers: { "Content-Type": "application/json" } };
|
||||
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();
|
||||
log.info("_getLocation returning", result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the users current region using request that is used for GeoLocation
|
||||
* requests. Exposed for tests.
|
||||
* Return the users current region using
|
||||
* request that is used for GeoLocation requests.
|
||||
*
|
||||
* @returns {Promise<string>}
|
||||
* @returns {String}
|
||||
* A 2 character string representing a region.
|
||||
*/
|
||||
async _getRegionLocally() {
|
||||
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.
|
||||
* 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.
|
||||
*
|
||||
* @returns {Promise<string>}
|
||||
* @returns {String}
|
||||
* A 2 character string representing a region.
|
||||
*/
|
||||
async #geoCode(location) {
|
||||
async _geoCode(location) {
|
||||
let plainMap = await this._getPlainMap();
|
||||
let polygons = this.#getPolygonsContainingPoint(location, plainMap);
|
||||
let polygons = this._getPolygonsContainingPoint(location, plainMap);
|
||||
if (polygons.length == 1) {
|
||||
log.info("Found in single exact region");
|
||||
return polygons[0].properties.alpha2;
|
||||
}
|
||||
if (polygons.length) {
|
||||
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
|
||||
// to see if the point is close to a region.
|
||||
let bufferedMap = await this._getBufferedMap();
|
||||
polygons = this.#getPolygonsContainingPoint(location, bufferedMap);
|
||||
polygons = this._getPolygonsContainingPoint(location, bufferedMap);
|
||||
|
||||
if (polygons.length === 1) {
|
||||
log.info("Found in single buffered region");
|
||||
@@ -547,7 +521,7 @@ class RegionDetector {
|
||||
let unBufferedRegions = plainMap.features.filter(feature =>
|
||||
regions.includes(feature.properties.alpha2)
|
||||
);
|
||||
return this.#findClosest(location, unBufferedRegions);
|
||||
return this._findClosest(location, unBufferedRegions);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -557,9 +531,9 @@ class RegionDetector {
|
||||
* an array of those polygons along with the region that
|
||||
* they define
|
||||
*
|
||||
* @param {object} point
|
||||
* @param {Object} point
|
||||
* A lat + lng coordinate.
|
||||
* @param {object} map
|
||||
* @param {Object} map
|
||||
* Geojson object that defined seperate regions with a list
|
||||
* of polygons.
|
||||
*
|
||||
@@ -567,17 +541,17 @@ class RegionDetector {
|
||||
* An array of polygons that contain the point, along with the
|
||||
* region they define.
|
||||
*/
|
||||
#getPolygonsContainingPoint(point, map) {
|
||||
_getPolygonsContainingPoint(point, map) {
|
||||
let polygons = [];
|
||||
for (const feature of map.features) {
|
||||
let coords = feature.geometry.coordinates;
|
||||
if (feature.geometry.type === "Polygon") {
|
||||
if (this.#polygonInPoint(point, coords[0])) {
|
||||
if (this._polygonInPoint(point, coords[0])) {
|
||||
polygons.push(feature);
|
||||
}
|
||||
} else if (feature.geometry.type === "MultiPolygon") {
|
||||
for (const innerCoords of coords) {
|
||||
if (this.#polygonInPoint(point, innerCoords[0])) {
|
||||
if (this._polygonInPoint(point, innerCoords[0])) {
|
||||
polygons.push(feature);
|
||||
}
|
||||
}
|
||||
@@ -590,18 +564,18 @@ class RegionDetector {
|
||||
* Find the largest distance between a point and any of the points that
|
||||
* make up an array of regions.
|
||||
*
|
||||
* @param {object} location
|
||||
* @param {Object} location
|
||||
* A lat + lng coordinate.
|
||||
* @param {Array} regions
|
||||
* An array of GeoJSON region definitions.
|
||||
*
|
||||
* @returns {string}
|
||||
* @returns {String}
|
||||
* A 2 character string representing a region.
|
||||
*/
|
||||
#findFurthest(location, regions) {
|
||||
_findFurthest(location, regions) {
|
||||
let max = { distance: 0, region: null };
|
||||
this.#traverse(regions, ({ lat, lng, region }) => {
|
||||
let distance = this.#distanceBetween(location, { lng, lat });
|
||||
this._traverse(regions, ({ lat, lng, region }) => {
|
||||
let distance = this._distanceBetween(location, { lng, lat });
|
||||
if (distance > max.distance) {
|
||||
max = { distance, region };
|
||||
}
|
||||
@@ -613,18 +587,18 @@ class RegionDetector {
|
||||
* Find the smallest distance between a point and any of the points that
|
||||
* make up an array of regions.
|
||||
*
|
||||
* @param {object} location
|
||||
* @param {Object} location
|
||||
* A lat + lng coordinate.
|
||||
* @param {Array} regions
|
||||
* An array of GeoJSON region definitions.
|
||||
*
|
||||
* @returns {string}
|
||||
* @returns {String}
|
||||
* A 2 character string representing a region.
|
||||
*/
|
||||
#findClosest(location, regions) {
|
||||
_findClosest(location, regions) {
|
||||
let min = { distance: Infinity, region: null };
|
||||
this.#traverse(regions, ({ lat, lng, region }) => {
|
||||
let distance = this.#distanceBetween(location, { lng, lat });
|
||||
this._traverse(regions, ({ lat, lng, region }) => {
|
||||
let distance = this._distanceBetween(location, { lng, lat });
|
||||
if (distance < min.distance) {
|
||||
min = { distance, region };
|
||||
}
|
||||
@@ -641,7 +615,7 @@ class RegionDetector {
|
||||
* @param {Function} fun
|
||||
* Function to call on individual coordinates.
|
||||
*/
|
||||
#traverse(regions, fun) {
|
||||
_traverse(regions, fun) {
|
||||
for (const region of regions) {
|
||||
if (region.geometry.type === "Polygon") {
|
||||
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
|
||||
* an odd number of times the point is inside the polygon.
|
||||
*
|
||||
* @param {object} location
|
||||
* @param {Object} location
|
||||
* A lat + lng coordinate.
|
||||
* @param {number} location.lng
|
||||
* @param {number} location.lat
|
||||
* @param {object} poly
|
||||
* @param {Object} polygon
|
||||
* Array of coordinates that define the boundaries of a polygon.
|
||||
*
|
||||
* @returns {boolean}
|
||||
* Whether the point is within the polygon.
|
||||
*/
|
||||
#polygonInPoint({ lng, lat }, poly) {
|
||||
_polygonInPoint({ lng, lat }, poly) {
|
||||
let inside = false;
|
||||
// For each edge of the polygon.
|
||||
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.
|
||||
*
|
||||
* @param {object} p1
|
||||
* @param {Object} p1
|
||||
* A lat + lng coordinate.
|
||||
* @param {object} p2
|
||||
* @param {Object} p2
|
||||
* A lat + lng coordinate.
|
||||
*
|
||||
* @returns {number}
|
||||
* @returns {int}
|
||||
* The distance between the 2 points.
|
||||
*/
|
||||
#distanceBetween(p1, p2) {
|
||||
_distanceBetween(p1, p2) {
|
||||
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 TIMEOUT error if the request is not completed in time.
|
||||
*
|
||||
* @param {string} url
|
||||
* @param {String} url
|
||||
* The time url to fetch.
|
||||
* @param {object} opts
|
||||
* @param {Object} opts
|
||||
* 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.
|
||||
* @returns {Promise<Response>}
|
||||
*/
|
||||
async #fetchTimeout(url, opts, timeout) {
|
||||
async _fetchTimeout(url, opts, timeout) {
|
||||
let controller = new AbortController();
|
||||
opts.signal = controller.signal;
|
||||
// Casted to Promise<Response> because `this.#timeout` will not return void,
|
||||
// but reject if it wins the race.
|
||||
return /** @type {Promise<Response>} */ (
|
||||
Promise.race([fetch(url, opts), this.#timeout(timeout, controller)])
|
||||
);
|
||||
return 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
|
||||
* completes first.
|
||||
*
|
||||
* @param {number} timeout
|
||||
* @param {int} timeout
|
||||
* 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
|
||||
* allows us to abort the request.
|
||||
*/
|
||||
async #timeout(timeout, controller) {
|
||||
async _timeout(timeout, controller) {
|
||||
await new Promise(resolve => lazy.setTimeout(resolve, timeout));
|
||||
if (controller) {
|
||||
// Yield so it is the TIMEOUT that is returned and not
|
||||
@@ -752,7 +720,7 @@ class RegionDetector {
|
||||
throw new Error("TIMEOUT");
|
||||
}
|
||||
|
||||
async #fetchWifiData() {
|
||||
async _fetchWifiData() {
|
||||
log.info("fetchWifiData called");
|
||||
this.wifiService = Cc["@mozilla.org/wifi/monitor;1"].getService(
|
||||
Ci.nsIWifiMonitor
|
||||
@@ -760,7 +728,7 @@ class RegionDetector {
|
||||
this.wifiService.startWatching(this, false);
|
||||
|
||||
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
|
||||
* debounce those so we aren't processing them constantly.
|
||||
*
|
||||
* @returns {boolean}
|
||||
* @returns {bool}
|
||||
* Whether we should continue the update check.
|
||||
*/
|
||||
#needsUpdateCheck() {
|
||||
_needsUpdateCheck() {
|
||||
let sinceUpdate = Math.round(Date.now() / 1000) - lazy.lastUpdated;
|
||||
let needsUpdate = sinceUpdate >= lazy.updateDebounce;
|
||||
if (!needsUpdate) {
|
||||
@@ -783,10 +751,8 @@ class RegionDetector {
|
||||
/**
|
||||
* Dispatch a promise returning function to the main thread and
|
||||
* resolve when it is completed.
|
||||
*
|
||||
* @param {() => Promise<void>} fun
|
||||
*/
|
||||
#idleDispatch(fun) {
|
||||
_idleDispatch(fun) {
|
||||
return new Promise(resolve => {
|
||||
Services.tm.idleDispatchToMainThread(fun().then(resolve));
|
||||
});
|
||||
@@ -794,10 +760,10 @@ class RegionDetector {
|
||||
|
||||
/**
|
||||
* 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() {
|
||||
if (this.#needsUpdateCheck()) {
|
||||
if (this._needsUpdateCheck()) {
|
||||
await this._fetchRegion();
|
||||
}
|
||||
}
|
||||
@@ -806,13 +772,14 @@ class RegionDetector {
|
||||
* Called when we see geolocation updates.
|
||||
* in case the user never users geolocation.
|
||||
*
|
||||
* @param {object} location
|
||||
* @param {Object} location
|
||||
* A location object containing lat + lng coordinates.
|
||||
*
|
||||
*/
|
||||
async #seenLocation(location) {
|
||||
async _seenLocation(location) {
|
||||
log.info(`Got location update: ${location.lat}:${location.lng}`);
|
||||
if (this.#needsUpdateCheck()) {
|
||||
let region = await this.#geoCode(location);
|
||||
if (this._needsUpdateCheck()) {
|
||||
let region = await this._geoCode(location);
|
||||
if (region) {
|
||||
this._setCurrentRegion(region);
|
||||
}
|
||||
@@ -821,7 +788,7 @@ class RegionDetector {
|
||||
|
||||
onChange(accessPoints) {
|
||||
log.info("onChange called");
|
||||
if (!accessPoints || !this.#wifiDataPromiseResolver) {
|
||||
if (!accessPoints || !this._wifiDataPromise) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -830,18 +797,13 @@ class RegionDetector {
|
||||
this.wifiService = null;
|
||||
}
|
||||
|
||||
if (this.#wifiDataPromiseResolver) {
|
||||
if (this._wifiDataPromise) {
|
||||
let data = lazy.LocationHelper.formatWifiAccessPoints(accessPoints);
|
||||
this.#wifiDataPromiseResolver(data);
|
||||
this.#wifiDataPromiseResolver = null;
|
||||
this._wifiDataPromise(data);
|
||||
this._wifiDataPromise = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implemented for nsIWifiListener.
|
||||
*/
|
||||
onError() {}
|
||||
|
||||
/**
|
||||
* A method that tries to determine if this user is in a US geography according
|
||||
* to their timezones.
|
||||
@@ -871,19 +833,18 @@ class RegionDetector {
|
||||
observe(aSubject, aTopic) {
|
||||
log.info(`Observed ${aTopic}`);
|
||||
switch (aTopic) {
|
||||
case GEOLOCATION_TOPIC: {
|
||||
case GEOLOCATION_TOPIC:
|
||||
// aSubject from GeoLocation.cpp will be a GeoPosition
|
||||
// DOM Object, but from tests we will receive a
|
||||
// wrappedJSObject so handle both here.
|
||||
let coords = aSubject.coords || aSubject.wrappedJSObject.coords;
|
||||
this.#seenLocation({
|
||||
this._seenLocation({
|
||||
lat: coords.latitude,
|
||||
lng: coords.longitude,
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// For tests to create blank new instances.
|
||||
newInstance() {
|
||||
|
||||
@@ -14,7 +14,6 @@ ChromeUtils.defineLazyGetter(lazy, "MigrationUtils", () => {
|
||||
|
||||
try {
|
||||
let { MigrationUtils } = ChromeUtils.importESModule(
|
||||
// eslint-disable-next-line mozilla/no-browser-refs-in-toolkit
|
||||
"resource:///modules/MigrationUtils.sys.mjs"
|
||||
);
|
||||
return MigrationUtils;
|
||||
@@ -30,7 +29,7 @@ export var ResetProfile = {
|
||||
/**
|
||||
* Check if reset is supported for the currently running profile.
|
||||
*
|
||||
* @returns {boolean} whether reset is supported.
|
||||
* @return boolean whether reset is supported.
|
||||
*/
|
||||
resetSupported() {
|
||||
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.
|
||||
*
|
||||
* @param {Window} window
|
||||
*/
|
||||
async openConfirmationDialog(window) {
|
||||
let win = window;
|
||||
|
||||
@@ -9,15 +9,7 @@
|
||||
// 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
|
||||
|
||||
/**
|
||||
* @type {Map<number, nsITimer>}
|
||||
*/
|
||||
var gTimerTable = new Map();
|
||||
|
||||
/**
|
||||
* @type {Map<number, () => void>}
|
||||
*/
|
||||
var gIdleTable = new Map();
|
||||
var gTimerTable = new Map(); // int -> nsITimer or idleCallback
|
||||
|
||||
// Don't generate this for every timer.
|
||||
var setTimeout_timerCallbackQI = ChromeUtils.generateQI([
|
||||
@@ -25,172 +17,123 @@ var setTimeout_timerCallbackQI = ChromeUtils.generateQI([
|
||||
"nsINamed",
|
||||
]);
|
||||
|
||||
/**
|
||||
* @template {any[]} T
|
||||
*
|
||||
* @param {(...args: T) => any} callback
|
||||
* @param {number} milliseconds
|
||||
* @param {boolean} [isInterval]
|
||||
* @param {nsIEventTarget} [target]
|
||||
* @param {T} [args]
|
||||
*/
|
||||
function _setTimeoutOrIsInterval(
|
||||
callback,
|
||||
milliseconds,
|
||||
isInterval,
|
||||
target,
|
||||
args
|
||||
aCallback,
|
||||
aMilliseconds,
|
||||
aIsInterval,
|
||||
aTarget,
|
||||
aArgs
|
||||
) {
|
||||
if (typeof callback !== "function") {
|
||||
if (typeof aCallback !== "function") {
|
||||
throw new Error(
|
||||
`callback is not a function in ${
|
||||
isInterval ? "setInterval" : "setTimeout"
|
||||
aIsInterval ? "setInterval" : "setTimeout"
|
||||
}`
|
||||
);
|
||||
}
|
||||
let id = gNextId++;
|
||||
let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
|
||||
|
||||
if (target) {
|
||||
timer.target = target;
|
||||
if (aTarget) {
|
||||
timer.target = aTarget;
|
||||
}
|
||||
|
||||
let callbackObj = {
|
||||
let callback = {
|
||||
QueryInterface: setTimeout_timerCallbackQI,
|
||||
|
||||
// nsITimerCallback
|
||||
notify() {
|
||||
if (!isInterval) {
|
||||
if (!aIsInterval) {
|
||||
gTimerTable.delete(id);
|
||||
}
|
||||
callback.apply(null, args);
|
||||
aCallback.apply(null, aArgs);
|
||||
},
|
||||
|
||||
// nsINamed
|
||||
get name() {
|
||||
return `${
|
||||
isInterval ? "setInterval" : "setTimeout"
|
||||
}() for ${Cu.getDebugName(callback)}`;
|
||||
aIsInterval ? "setInterval" : "setTimeout"
|
||||
}() for ${Cu.getDebugName(aCallback)}`;
|
||||
},
|
||||
};
|
||||
|
||||
timer.initWithCallback(
|
||||
callbackObj,
|
||||
milliseconds,
|
||||
isInterval ? timer.TYPE_REPEATING_SLACK : timer.TYPE_ONE_SHOT
|
||||
callback,
|
||||
aMilliseconds,
|
||||
aIsInterval ? timer.TYPE_REPEATING_SLACK : timer.TYPE_ONE_SHOT
|
||||
);
|
||||
|
||||
gTimerTable.set(id, timer);
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a timeout.
|
||||
*
|
||||
* @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 setTimeout(aCallback, aMilliseconds, ...aArgs) {
|
||||
return _setTimeoutOrIsInterval(aCallback, aMilliseconds, false, null, aArgs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a timeout with a given event target.
|
||||
*
|
||||
* @template {any[]} T
|
||||
*
|
||||
* @param {(...args: T) => any} callback
|
||||
* @param {number} milliseconds
|
||||
* @param {nsIEventTarget} target
|
||||
* @param {T} [args]
|
||||
*/
|
||||
export function setTimeoutWithTarget(callback, milliseconds, target, ...args) {
|
||||
return _setTimeoutOrIsInterval(callback, milliseconds, false, target, args);
|
||||
export function setTimeoutWithTarget(
|
||||
aCallback,
|
||||
aMilliseconds,
|
||||
aTarget,
|
||||
...aArgs
|
||||
) {
|
||||
return _setTimeoutOrIsInterval(
|
||||
aCallback,
|
||||
aMilliseconds,
|
||||
false,
|
||||
aTarget,
|
||||
aArgs
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets an interval timer.
|
||||
*
|
||||
* @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 setInterval(aCallback, aMilliseconds, ...aArgs) {
|
||||
return _setTimeoutOrIsInterval(aCallback, aMilliseconds, true, null, aArgs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets an interval timer.
|
||||
*
|
||||
* @template {any[]} T
|
||||
*
|
||||
* @param {(...args: T) => any} callback
|
||||
* @param {number} milliseconds
|
||||
* @param {nsIEventTarget} target
|
||||
* @param {T} [args]
|
||||
*/
|
||||
export function setIntervalWithTarget(callback, milliseconds, target, ...args) {
|
||||
return _setTimeoutOrIsInterval(callback, milliseconds, true, target, args);
|
||||
export function setIntervalWithTarget(
|
||||
aCallback,
|
||||
aMilliseconds,
|
||||
aTarget,
|
||||
...aArgs
|
||||
) {
|
||||
return _setTimeoutOrIsInterval(
|
||||
aCallback,
|
||||
aMilliseconds,
|
||||
true,
|
||||
aTarget,
|
||||
aArgs
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the given timer.
|
||||
*
|
||||
* @param {number} id
|
||||
*/
|
||||
function clear(id) {
|
||||
if (gTimerTable.has(id)) {
|
||||
gTimerTable.get(id).cancel();
|
||||
gTimerTable.delete(id);
|
||||
function clear(aId) {
|
||||
if (gTimerTable.has(aId)) {
|
||||
gTimerTable.get(aId).cancel();
|
||||
gTimerTable.delete(aId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the given timer.
|
||||
*/
|
||||
export var clearInterval = clear;
|
||||
|
||||
/**
|
||||
* Clears the given timer.
|
||||
*/
|
||||
export var clearTimeout = clear;
|
||||
|
||||
/**
|
||||
* Dispatches the given callback to the main thread when it would be otherwise
|
||||
* 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") {
|
||||
export function requestIdleCallback(aCallback, aOptions) {
|
||||
if (typeof aCallback !== "function") {
|
||||
throw new Error("callback is not a function in requestIdleCallback");
|
||||
}
|
||||
let id = gNextId++;
|
||||
|
||||
ChromeUtils.idleDispatch(() => {
|
||||
if (gIdleTable.has(id)) {
|
||||
gIdleTable.delete(id);
|
||||
callback();
|
||||
let callback = (...aArgs) => {
|
||||
if (gTimerTable.has(id)) {
|
||||
gTimerTable.delete(id);
|
||||
aCallback(...aArgs);
|
||||
}
|
||||
}, options);
|
||||
gIdleTable.set(id, callback);
|
||||
};
|
||||
|
||||
ChromeUtils.idleDispatch(callback, aOptions);
|
||||
gTimerTable.set(id, callback);
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels the given idle callback
|
||||
*
|
||||
* @param {number} id
|
||||
*/
|
||||
export function cancelIdleCallback(id) {
|
||||
if (gIdleTable.has(id)) {
|
||||
gIdleTable.delete(id);
|
||||
export function cancelIdleCallback(aId) {
|
||||
if (gTimerTable.has(aId)) {
|
||||
gTimerTable.delete(aId);
|
||||
}
|
||||
}
|
||||
|
||||
29
toolkit/modules/tests/xpcshell/test_ObjectUtils_strict.js
Normal file
29
toolkit/modules/tests/xpcshell/test_ObjectUtils_strict.js
Normal 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.
|
||||
});
|
||||
@@ -112,7 +112,8 @@ add_task(async function test_invalid_url() {
|
||||
RegionTestUtils.REGION_URL_PREF,
|
||||
"http://localhost:0"
|
||||
);
|
||||
await Region._fetchRegion();
|
||||
let result = await Region._fetchRegion();
|
||||
Assert.ok(!result, "Should return no result");
|
||||
await checkTelemetry(Region.TELEMETRY.NO_RESULT);
|
||||
});
|
||||
|
||||
@@ -122,7 +123,8 @@ add_task(async function test_invalid_json() {
|
||||
RegionTestUtils.REGION_URL_PREF,
|
||||
'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);
|
||||
});
|
||||
|
||||
@@ -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 cleanup(srv);
|
||||
});
|
||||
|
||||
@@ -87,6 +87,8 @@ skip-if = ["os == 'android'"]
|
||||
|
||||
["test_ObjectUtils.js"]
|
||||
|
||||
["test_ObjectUtils_strict.js"]
|
||||
|
||||
["test_PermissionsUtils.js"]
|
||||
|
||||
["test_Preferences.js"]
|
||||
|
||||
Reference in New Issue
Block a user