зеркало из https://github.com/mozilla/gecko-dev.git
187 строки
7.0 KiB
JavaScript
187 строки
7.0 KiB
JavaScript
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||
|
* 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/. */
|
||
|
|
||
|
"use strict";
|
||
|
|
||
|
ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
|
||
|
ChromeUtils.defineModuleGetter(this, "AppConstants", "resource://gre/modules/AppConstants.jsm");
|
||
|
ChromeUtils.defineModuleGetter(this, "FeatureGateImplementation", "resource://featuregates/FeatureGateImplementation.jsm");
|
||
|
|
||
|
XPCOMUtils.defineLazyGlobalGetters(this, ["fetch"]);
|
||
|
|
||
|
var EXPORTED_SYMBOLS = ["FeatureGate"];
|
||
|
|
||
|
XPCOMUtils.defineLazyGetter(this, "gFeatureDefinitionsPromise", async () => {
|
||
|
const url = "resource://featuregates/feature_definitions.json";
|
||
|
return fetchFeatureDefinitions(url);
|
||
|
});
|
||
|
|
||
|
const kTargetFacts = new Map([
|
||
|
["release", AppConstants.MOZ_UPDATE_CHANNEL === "release"],
|
||
|
["beta", AppConstants.MOZ_UPDATE_CHANNEL === "beta"],
|
||
|
["dev-edition", AppConstants.MOZ_UPDATE_CHANNEL === "aurora"],
|
||
|
["nightly", AppConstants.MOZ_UPDATE_CHANNEL === "nightly"],
|
||
|
["win", AppConstants.platform === "win"],
|
||
|
["mac", AppConstants.platform === "macosx"],
|
||
|
["linux", AppConstants.platform === "linux"],
|
||
|
["android", AppConstants.platform === "android"],
|
||
|
]);
|
||
|
|
||
|
async function fetchFeatureDefinitions(url) {
|
||
|
const res = await fetch(url);
|
||
|
let definitionsJson = await res.json();
|
||
|
return new Map(Object.entries(definitionsJson));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Take a map of conditions to values, and return the value who's conditions
|
||
|
* match this browser, or the default value in the map.
|
||
|
*
|
||
|
* @example `evaluateTargetedValue({default: false, nightly: true})` would
|
||
|
* return true on Nightly, and false otherwise.
|
||
|
* @param {Object} targetedValue An object mapping string conditions to values. The
|
||
|
* conditions are comma separated values such as those specified
|
||
|
* in `kTargetFacts` above. A condition "default" is required, as
|
||
|
* the fallback valued.
|
||
|
* @param {Map} targetingFacts A map of target facts to use, such as `kTargetFacts`.
|
||
|
* @returns A value from `targetedValue`.
|
||
|
*/
|
||
|
function evaluateTargetedValue(targetedValue, targetingFacts) {
|
||
|
if (!Object.hasOwnProperty.call(targetedValue, "default")) {
|
||
|
throw new Error(
|
||
|
`Targeted value ${JSON.stringify(targetedValue)} has no default key`
|
||
|
);
|
||
|
}
|
||
|
|
||
|
for (const [key, value] of Object.entries(targetedValue)) {
|
||
|
if (key === "default") {
|
||
|
continue;
|
||
|
}
|
||
|
if (key.split(",").every(part => targetingFacts.get(part))) {
|
||
|
return value;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return targetedValue.default;
|
||
|
}
|
||
|
|
||
|
const kFeatureGateCache = new Map();
|
||
|
|
||
|
/** A high level control for turning features on and off. */
|
||
|
class FeatureGate {
|
||
|
/*
|
||
|
* This is structured as a class with static methods to that sphinx-js can
|
||
|
* easily document it. This constructor is required for sphinx-js to detect
|
||
|
* this class for documentation.
|
||
|
*/
|
||
|
|
||
|
constructor() {}
|
||
|
|
||
|
/**
|
||
|
* Constructs a feature gate object that is defined in ``Features.toml``.
|
||
|
* This is the primary way to create a ``FeatureGate``.
|
||
|
*
|
||
|
* @param {string} id The ID of the feature's definition in `Features.toml`.
|
||
|
* @param {string} testDefinitionsUrl A URL from which definitions can be fetched. Only use this in tests.
|
||
|
* @throws If the ``id`` passed is not defined in ``Features.toml``.
|
||
|
*/
|
||
|
static async fromId(id, testDefinitionsUrl = undefined) {
|
||
|
let featureDefinitions;
|
||
|
if (testDefinitionsUrl) {
|
||
|
featureDefinitions = await fetchFeatureDefinitions(testDefinitionsUrl);
|
||
|
} else {
|
||
|
featureDefinitions = await gFeatureDefinitionsPromise;
|
||
|
}
|
||
|
|
||
|
if (!featureDefinitions.has(id)) {
|
||
|
throw new Error(
|
||
|
`Unknown feature id ${id}. Features must be defined in toolkit/components/featuregates/Features.toml`
|
||
|
);
|
||
|
}
|
||
|
|
||
|
const definition = featureDefinitions.get(id);
|
||
|
const targetValueKeys = ["defaultValue", "isPublic"];
|
||
|
for (const key of targetValueKeys) {
|
||
|
definition[key] = evaluateTargetedValue(definition[key], kTargetFacts);
|
||
|
}
|
||
|
return new FeatureGateImplementation(definition);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Add an observer for a feature gate by ID. If the feature is of type
|
||
|
* boolean and currently enabled, `onEnable` will be called.
|
||
|
*
|
||
|
* The underlying feature gate instance will be shared with all other callers
|
||
|
* of this function, and share an observer.
|
||
|
*
|
||
|
* @param {string} id The ID of the feature's definition in `Features.toml`.
|
||
|
* @param {object} observer Functions to be called when the feature changes.
|
||
|
* All observer functions are optional.
|
||
|
* @param {Function()} [observer.onEnable] Called when the feature becomes enabled.
|
||
|
* @param {Function()} [observer.onDisable] Called when the feature becomes disabled.
|
||
|
* @param {Function(newValue)} [observer.onChange] Called when the
|
||
|
* feature's state changes to any value. The new value will be passed to the
|
||
|
* function.
|
||
|
* @param {string} testDefinitionsUrl A URL from which definitions can be fetched. Only use this in tests.
|
||
|
* @returns {Promise<boolean>} The current value of the feature.
|
||
|
*/
|
||
|
static async addObserver(id, observer, testDefinitionsUrl = undefined) {
|
||
|
if (!kFeatureGateCache.has(id)) {
|
||
|
kFeatureGateCache.set(id, await FeatureGate.fromId(id, testDefinitionsUrl));
|
||
|
}
|
||
|
const feature = kFeatureGateCache.get(id);
|
||
|
return feature.addObserver(observer);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Remove an observer of changes from this feature
|
||
|
* @param {string} id The ID of the feature's definition in `Features.toml`.
|
||
|
* @param observer Then observer that was passed to addObserver to remove.
|
||
|
*/
|
||
|
static async removeObserver(id, observer) {
|
||
|
let feature = kFeatureGateCache.get(id);
|
||
|
if (!feature) {
|
||
|
return;
|
||
|
}
|
||
|
feature.removeObserver(observer);
|
||
|
if (feature._observers.size === 0) {
|
||
|
kFeatureGateCache.delete(id);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get the current value of this feature gate. Implementors should avoid
|
||
|
* storing the result to avoid missing changes to the feature's value.
|
||
|
* Consider using :func:`addObserver` if it is necessary to store the value
|
||
|
* of the feature.
|
||
|
*
|
||
|
* @async
|
||
|
* @param {string} id The ID of the feature's definition in `Features.toml`.
|
||
|
* @returns {Promise<boolean>} A promise for the value associated with this feature.
|
||
|
*/
|
||
|
static async getValue(id, testDefinitionsUrl = undefined) {
|
||
|
let feature = kFeatureGateCache.get(id);
|
||
|
if (!feature) {
|
||
|
feature = await FeatureGate.fromId(id, testDefinitionsUrl);
|
||
|
}
|
||
|
return feature.getValue();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* An alias of `getValue` for boolean typed feature gates.
|
||
|
*
|
||
|
* @async
|
||
|
* @param {string} id The ID of the feature's definition in `Features.toml`.
|
||
|
* @returns {Promise<boolean>} A promise for the value associated with this feature.
|
||
|
* @throws {Error} If the feature is not a boolean.
|
||
|
*/
|
||
|
static async isEnabled(id, testDefinitionsUrl = undefined) {
|
||
|
let feature = kFeatureGateCache.get(id);
|
||
|
if (!feature) {
|
||
|
feature = await FeatureGate.fromId(id, testDefinitionsUrl);
|
||
|
}
|
||
|
return feature.isEnabled();
|
||
|
}
|
||
|
}
|