/* 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/. */ const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { FeatureGate: "resource://featuregates/FeatureGate.sys.mjs", }); /** An individual feature gate that can be re-used for more advanced usage. */ export class FeatureGateImplementation { // Note that the following comment is *not* a jsdoc. Making it a jsdoc would // makes sphinx-js expose it to users. This feature shouldn't be used by // users, and so should not be in the docs. Sphinx-js does not respect the // @private marker on a constructor (https://github.com/erikrose/sphinx-js/issues/71). /* * This constructor should only be used directly in tests. * ``FeatureGate.fromId`` should be used instead for most cases. * * @private * * @param {object} definition Description of the feature gate. * @param {string} definition.id * @param {string} definition.title * @param {string} definition.description * @param {string} definition.descriptionLinks * @param {boolean} definition.restartRequired * @param {string} definition.type * @param {string} definition.preference * @param {string} definition.defaultValue * @param {object} definition.isPublic * @param {object} definition.bugNumbers */ constructor(definition) { this._definition = definition; this._observers = new Set(); } // The below are all getters instead of direct access to make it easy to provide JSDocs. /** * A short string used to refer to this feature in code. * @type string */ get id() { return this._definition.id; } /** * A Fluent string ID that will resolve to some text to identify this feature to users. * @type string */ get title() { return this._definition.title; } /** * A Fluent string ID that will resolve to a longer string to show to users that explains the feature. * @type string */ get description() { return this._definition.description; } get descriptionLinks() { return this._definition.descriptionLinks; } /** * Whether this feature requires a browser restart to take effect after toggling. * @type boolean */ get restartRequired() { return this._definition.restartRequired; } /** * The type of feature. Currently only booleans are supported. This may be * richer than JS types in the future, such as enum values. * @type string */ get type() { return this._definition.type; } /** * The name of the preference that stores the value of this feature. * * This preference should not be read directly, but instead its values should * be accessed via FeatureGate#addObserver or FeatureGate#getValue. This * property is provided for backwards compatibility. * * @type string */ get preference() { return this._definition.preference; } /** * The default value for the feature gate for this update channel. * @type boolean */ get defaultValue() { return this._definition.defaultValue; } /** The default value before any targeting evaluation. */ get defaultValueOriginalValue() { // This will probably be overwritten by the loader, but if not provide a default. return ( this._definition.defaultValueOriginalValue || { default: this._definition.defaultValue, } ); } /** * Check what the default value of this feature gate would be on another * browser with different facts, such as on another platform. * * @param {Map} extraFacts * A `Map` of hypothetical facts to consider, such as {'windows': true} to * check what the value of this feature would be on Windows. */ defaultValueWith(extraFacts) { return lazy.FeatureGate.evaluateTargetedValue( this.defaultValueOriginalValue, extraFacts, { mergeFactsWithDefault: true } ); } /** * If this feature should be exposed to users in an advanced settings panel * for this build of Firefox. * * @type boolean */ get isPublic() { return this._definition.isPublic; } /** The isPublic before any targeting evaluation. */ get isPublicOriginalValue() { // This will probably be overwritten by the loader, but if not provide a default. return ( this._definition.isPublicOriginalValue || { default: this._definition.isPublic, } ); } /** * Check if this feature is available on another browser with different * facts, such as on another platform. * * @param {Map} extraFacts * A `Map` of hypothetical facts to consider, such as {'windows': true} to * check if this feature would be available on Windows. */ isPublicWith(extraFacts) { return lazy.FeatureGate.evaluateTargetedValue( this.isPublicOriginalValue, extraFacts, { mergeFactsWithDefault: true } ); } /** * Bug numbers associated with this feature. * @type Array */ get bugNumbers() { return this._definition.bugNumbers; } /** * 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 * @returns {Promise} A promise for the value associated with this feature. */ // Note that this is async for potential future use of a storage backend besides preferences. async getValue() { return Services.prefs.getBoolPref(this.preference, this.defaultValue); } /** * An alias of `getValue` for boolean typed feature gates. * * @async * @returns {Promise} A promise for the value associated with this feature. * @throws {Error} If the feature is not a boolean. */ // Note that this is async for potential future use of a storage backend besides preferences. async isEnabled() { if (this.type !== "boolean") { throw new Error( `Tried to call isEnabled when type is not boolean (it is ${this.type})` ); } return this.getValue(); } /** * Add an observer for changes to this feature. When the observer is added, * `onChange` will asynchronously be called with the current value of the * preference. If the feature is of type boolean and currently enabled, * `onEnable` will additionally be called. * * @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: boolean)} [observer.onChange] Called when the * feature's state changes to any value. The new value will be passed to the * function. * @returns {Promise} The current value of the feature. */ async addObserver(observer) { if (this._observers.size === 0) { Services.prefs.addObserver(this.preference, this); } this._observers.add(observer); if (this.type === "boolean" && (await this.isEnabled())) { this._callObserverMethod(observer, "onEnable"); } // onDisable should not be called, because features should be assumed // disabled until onEnabled is called for the first time. return this.getValue(); } /** * Remove an observer of changes from this feature * @param observer The observer that was passed to addObserver to remove. */ removeObserver(observer) { this._observers.delete(observer); if (this._observers.size === 0) { Services.prefs.removeObserver(this.preference, this); } } /** * Removes all observers from this instance of the feature gate. */ removeAllObservers() { if (this._observers.size > 0) { this._observers.clear(); Services.prefs.removeObserver(this.preference, this); } } _callObserverMethod(observer, method, ...args) { if (method in observer) { try { observer[method](...args); } catch (err) { console.error(err); } } } /** * Observes changes to the preference storing the enabled state of the * feature. The observer is dynamically added only when observer have been * added. * @private */ async observe(aSubject, aTopic, aData) { if (aTopic === "nsPref:changed" && aData === this.preference) { const value = await this.getValue(); for (const observer of this._observers) { this._callObserverMethod(observer, "onChange", value); if (value) { this._callObserverMethod(observer, "onEnable"); } else { this._callObserverMethod(observer, "onDisable"); } } } else { console.error( new Error(`Unexpected event observed: ${aSubject}, ${aTopic}, ${aData}`) ); } } }