Bug 1900094 - Add telemetry for impressions missing due to domain-to-categories map...
[gecko.git] / toolkit / components / featuregates / FeatureGateImplementation.sys.mjs
blob247b03323c2afe4ce29e5575281d6e0288304c70
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2  * License, v. 2.0. If a copy of the MPL was not distributed with this
3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 /** An individual feature gate that can be re-used for more advanced usage. */
6 export class FeatureGateImplementation {
7   // Note that the following comment is *not* a jsdoc. Making it a jsdoc would
8   // makes sphinx-js expose it to users. This feature shouldn't be used by
9   // users, and so should not be in the docs. Sphinx-js does not respect the
10   // @private marker on a constructor (https://github.com/erikrose/sphinx-js/issues/71).
11   /*
12    * This constructor should only be used directly in tests.
13    * ``FeatureGate.fromId`` should be used instead for most cases.
14    *
15    * @private
16    *
17    * @param {object} definition Description of the feature gate.
18    * @param {string} definition.id
19    * @param {string} definition.title
20    * @param {string} definition.description
21    * @param {string} definition.descriptionLinks
22    * @param {boolean} definition.restartRequired
23    * @param {string} definition.type
24    * @param {string} definition.preference
25    * @param {string} definition.defaultValue
26    * @param {object} definition.isPublic
27    * @param {object} definition.bugNumbers
28    */
29   constructor(definition) {
30     this._definition = definition;
31     this._observers = new Set();
32   }
34   // The below are all getters instead of direct access to make it easy to provide JSDocs.
36   /**
37    * A short string used to refer to this feature in code.
38    * @type string
39    */
40   get id() {
41     return this._definition.id;
42   }
44   /**
45    * A Fluent string ID that will resolve to some text to identify this feature's group to users.
46    * @type string
47    */
48   get group() {
49     return this._definition.group;
50   }
52   /**
53    * A Fluent string ID that will resolve to some text to identify this feature to users.
54    * @type string
55    */
56   get title() {
57     return this._definition.title;
58   }
60   /**
61    * A Fluent string ID that will resolve to a longer string to show to users that explains the feature.
62    * @type string
63    */
64   get description() {
65     return this._definition.description;
66   }
68   get descriptionLinks() {
69     return this._definition.descriptionLinks;
70   }
72   /**
73    * Whether this feature requires a browser restart to take effect after toggling.
74    * @type boolean
75    */
76   get restartRequired() {
77     return this._definition.restartRequired;
78   }
80   /**
81    * The type of feature. Currently only booleans are supported. This may be
82    * richer than JS types in the future, such as enum values.
83    * @type string
84    */
85   get type() {
86     return this._definition.type;
87   }
89   /**
90    * The name of the preference that stores the value of this feature.
91    *
92    * This preference should not be read directly, but instead its values should
93    * be accessed via FeatureGate#addObserver or FeatureGate#getValue. This
94    * property is provided for backwards compatibility.
95    *
96    * @type string
97    */
98   get preference() {
99     return this._definition.preference;
100   }
102   /**
103    * The default value for the feature gate for this update channel.
104    * @type boolean
105    */
106   get defaultValue() {
107     return this._definition.defaultValue;
108   }
110   /**
111    * If this feature should be exposed to users in an advanced settings panel
112    * for this build of Firefox.
113    *
114    * @type boolean
115    */
116   get isPublic() {
117     return this._definition.isPublic;
118   }
120   /**
121    * Bug numbers associated with this feature.
122    * @type Array<number>
123    */
124   get bugNumbers() {
125     return this._definition.bugNumbers;
126   }
128   /**
129    * Get the current value of this feature gate. Implementors should avoid
130    * storing the result to avoid missing changes to the feature's value.
131    * Consider using :func:`addObserver` if it is necessary to store the value
132    * of the feature.
133    *
134    * @async
135    * @returns {Promise<boolean>} A promise for the value associated with this feature.
136    */
137   // Note that this is async for potential future use of a storage backend besides preferences.
138   async getValue() {
139     return Services.prefs.getBoolPref(this.preference, this.defaultValue);
140   }
142   /**
143    * An alias of `getValue` for boolean typed feature gates.
144    *
145    * @async
146    * @returns {Promise<boolean>} A promise for the value associated with this feature.
147    * @throws {Error} If the feature is not a boolean.
148    */
149   // Note that this is async for potential future use of a storage backend besides preferences.
150   async isEnabled() {
151     if (this.type !== "boolean") {
152       throw new Error(
153         `Tried to call isEnabled when type is not boolean (it is ${this.type})`
154       );
155     }
156     return this.getValue();
157   }
159   /**
160    * Add an observer for changes to this feature. When the observer is added,
161    * `onChange` will asynchronously be called with the current value of the
162    * preference. If the feature is of type boolean and currently enabled,
163    * `onEnable` will additionally be called.
164    *
165    * @param {object} observer Functions to be called when the feature changes.
166    *        All observer functions are optional.
167    * @param {Function()} [observer.onEnable] Called when the feature becomes enabled.
168    * @param {Function()} [observer.onDisable] Called when the feature becomes disabled.
169    * @param {Function(newValue: boolean)} [observer.onChange] Called when the
170    *        feature's state changes to any value. The new value will be passed to the
171    *        function.
172    * @returns {Promise<boolean>} The current value of the feature.
173    */
174   async addObserver(observer) {
175     if (this._observers.size === 0) {
176       Services.prefs.addObserver(this.preference, this);
177     }
179     this._observers.add(observer);
181     if (this.type === "boolean" && (await this.isEnabled())) {
182       this._callObserverMethod(observer, "onEnable");
183     }
184     // onDisable should not be called, because features should be assumed
185     // disabled until onEnabled is called for the first time.
187     return this.getValue();
188   }
190   /**
191    * Remove an observer of changes from this feature
192    * @param observer The observer that was passed to addObserver to remove.
193    */
194   removeObserver(observer) {
195     this._observers.delete(observer);
196     if (this._observers.size === 0) {
197       Services.prefs.removeObserver(this.preference, this);
198     }
199   }
201   /**
202    * Removes all observers from this instance of the feature gate.
203    */
204   removeAllObservers() {
205     if (this._observers.size > 0) {
206       this._observers.clear();
207       Services.prefs.removeObserver(this.preference, this);
208     }
209   }
211   _callObserverMethod(observer, method, ...args) {
212     if (method in observer) {
213       try {
214         observer[method](...args);
215       } catch (err) {
216         console.error(err);
217       }
218     }
219   }
221   /**
222    * Observes changes to the preference storing the enabled state of the
223    * feature. The observer is dynamically added only when observer have been
224    * added.
225    * @private
226    */
227   async observe(aSubject, aTopic, aData) {
228     if (aTopic === "nsPref:changed" && aData === this.preference) {
229       const value = await this.getValue();
230       for (const observer of this._observers) {
231         this._callObserverMethod(observer, "onChange", value);
233         if (value) {
234           this._callObserverMethod(observer, "onEnable");
235         } else {
236           this._callObserverMethod(observer, "onDisable");
237         }
238       }
239     } else {
240       console.error(
241         new Error(`Unexpected event observed: ${aSubject}, ${aTopic}, ${aData}`)
242       );
243     }
244   }