summaryrefslogtreecommitdiffstats
path: root/toolkit/components/featuregates/FeatureGateImplementation.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/featuregates/FeatureGateImplementation.sys.mjs')
-rw-r--r--toolkit/components/featuregates/FeatureGateImplementation.sys.mjs294
1 files changed, 294 insertions, 0 deletions
diff --git a/toolkit/components/featuregates/FeatureGateImplementation.sys.mjs b/toolkit/components/featuregates/FeatureGateImplementation.sys.mjs
new file mode 100644
index 0000000000..e54ac139cb
--- /dev/null
+++ b/toolkit/components/featuregates/FeatureGateImplementation.sys.mjs
@@ -0,0 +1,294 @@
+/* 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<number>
+ */
+ 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<boolean>} 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<boolean>} 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<boolean>} 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}`)
+ );
+ }
+ }
+}