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).
12 * This constructor should only be used directly in tests.
13 * ``FeatureGate.fromId`` should be used instead for most cases.
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
29 constructor(definition) {
30 this._definition = definition;
31 this._observers = new Set();
34 // The below are all getters instead of direct access to make it easy to provide JSDocs.
37 * A short string used to refer to this feature in code.
41 return this._definition.id;
45 * A Fluent string ID that will resolve to some text to identify this feature's group to users.
49 return this._definition.group;
53 * A Fluent string ID that will resolve to some text to identify this feature to users.
57 return this._definition.title;
61 * A Fluent string ID that will resolve to a longer string to show to users that explains the feature.
65 return this._definition.description;
68 get descriptionLinks() {
69 return this._definition.descriptionLinks;
73 * Whether this feature requires a browser restart to take effect after toggling.
76 get restartRequired() {
77 return this._definition.restartRequired;
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.
86 return this._definition.type;
90 * The name of the preference that stores the value of this feature.
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.
99 return this._definition.preference;
103 * The default value for the feature gate for this update channel.
107 return this._definition.defaultValue;
111 * If this feature should be exposed to users in an advanced settings panel
112 * for this build of Firefox.
117 return this._definition.isPublic;
121 * Bug numbers associated with this feature.
122 * @type Array<number>
125 return this._definition.bugNumbers;
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
135 * @returns {Promise<boolean>} A promise for the value associated with this feature.
137 // Note that this is async for potential future use of a storage backend besides preferences.
139 return Services.prefs.getBoolPref(this.preference, this.defaultValue);
143 * An alias of `getValue` for boolean typed feature gates.
146 * @returns {Promise<boolean>} A promise for the value associated with this feature.
147 * @throws {Error} If the feature is not a boolean.
149 // Note that this is async for potential future use of a storage backend besides preferences.
151 if (this.type !== "boolean") {
153 `Tried to call isEnabled when type is not boolean (it is ${this.type})`
156 return this.getValue();
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.
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
172 * @returns {Promise<boolean>} The current value of the feature.
174 async addObserver(observer) {
175 if (this._observers.size === 0) {
176 Services.prefs.addObserver(this.preference, this);
179 this._observers.add(observer);
181 if (this.type === "boolean" && (await this.isEnabled())) {
182 this._callObserverMethod(observer, "onEnable");
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();
191 * Remove an observer of changes from this feature
192 * @param observer The observer that was passed to addObserver to remove.
194 removeObserver(observer) {
195 this._observers.delete(observer);
196 if (this._observers.size === 0) {
197 Services.prefs.removeObserver(this.preference, this);
202 * Removes all observers from this instance of the feature gate.
204 removeAllObservers() {
205 if (this._observers.size > 0) {
206 this._observers.clear();
207 Services.prefs.removeObserver(this.preference, this);
211 _callObserverMethod(observer, method, ...args) {
212 if (method in observer) {
214 observer[method](...args);
222 * Observes changes to the preference storing the enabled state of the
223 * feature. The observer is dynamically added only when observer have been
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);
234 this._callObserverMethod(observer, "onEnable");
236 this._callObserverMethod(observer, "onDisable");
241 new Error(`Unexpected event observed: ${aSubject}, ${aTopic}, ${aData}`)