-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathAddonSettings.js
More file actions
343 lines (291 loc) · 9.66 KB
/
AddonSettings.js
File metadata and controls
343 lines (291 loc) · 9.66 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
/**
* Caching wrapper for AddonSettings API that does save and load settings.
*
* @public
* @module AddonSettings
* @requires data/DefaultSettings
* @requires ../lodash/isObject
*/
// lodash
import isPlainObject from "../lodash/isPlainObject.js";
import { DEFAULT_SETTINGS } from "../data/DefaultSettings.js";
let enableCaching = true;
let gettingManagedOption;
let gettingSyncOption;
let managedOptions = null;
let syncOptions = null;
/**
* Creates a (shallow) copy of the object or array.
*
* Also returns the value, as it is, if it needs no unfreezing.
*
* @private
* @param {*} value the value
* @returns {*} the value, now freely modifyable
*/
function unfreezeObject(value) {
// always shallow-copy
if (Array.isArray(value)) {
value = value.slice();
} else if ((isPlainObject(value))) {
value = Object.assign({}, value);
} else {
// return primitive value or similar ones
return value;
}
// but warn if object was not even frozen, so deev can fix the default settings
if (!Object.isFrozen(value)) {
console.warn("The following defined default value of type", typeof value, "is not frozen. It is recommend that all default options are frozen.", value);
}
return value;
}
/**
* Get the default value for a seting.
*
* Returns undefined, if option cannot be found.
*
* @public
* @param {string|null} option name of the option
* @returns {Object|undefined}
* @throws {Error} if option is not available
*/
export function getDefaultValue(option = null) {
const clonedDefaultSettings = DEFAULT_SETTINGS;
// if undefined return the object
if (option in clonedDefaultSettings) {
return unfreezeObject(clonedDefaultSettings[option]);
}
// return all default values
if (!option) {
Object.values(unfreezeObject(clonedDefaultSettings)).map(unfreezeObject);
return clonedDefaultSettings;
}
// end result fails
console.error(`Default value for option "${option}" missing. No default value defined.`);
throw new Error(`Default value for option "${option}" missing. No default value defined.`);
}
/**
* (Re)requests the managed and sync options.
*
* Note: It is usually a good idea to clearCache() before!
*
* @private
* @param {null|string|string[]} [requestOption=null] the option value to request
* @returns {void}
* @throws {Error}
*/
function reloadOptions(requestOption = null) {
gettingManagedOption = browser.storage.managed.get(requestOption).then((options) => {
managedOptions = options;
}).catch((error) => {
// rethrow error if it is not just due to missing storage manifest
if (error.message === "Managed storage manifest not found") {
/* only log warning as that is expected when no manifest file is found */
console.warn("could not get managed options", error);
// This error is now handled.
return;
}
throw error;
});
gettingSyncOption = browser.storage.sync.get(requestOption).then((options) => {
if (syncOptions === null) {
syncOptions = options;
} else {
// In case set() is called before this Promise here resolves, we need to keep the old values, but prefer the newly set ones.
Object.assign(options, syncOptions);
}
}).catch((error) => {
console.error("could not get sync options", error);
// re-throw, so Promise is not marked as handled
throw error;
});
}
/**
* Makes sure, that the synced o0ptions are available.
*
* @private
* @returns {Promise}
* @throws {Error}
*/
function requireSyncedOptions() {
return gettingSyncOption.catch(() => {
// fatal error (already logged), as we now require synced options
throw new Error("synced options not available");
});
}
/**
* Get all available options.
*
* It assumes gettingManagedOption is not pending anymore.
*
* @private
* @returns {Object}
*/
async function getAllOptions() {
const result = {};
// also need to wait for synced options
await requireSyncedOptions();
// if all values should be returned, also include all default ones in addition to fetched ones
Object.assign(result, getDefaultValue());
if (syncOptions !== null) {
Object.assign(result, syncOptions);
}
if (managedOptions !== null) {
Object.assign(result, managedOptions);
}
return result;
}
/**
* Clears the stored/cached values.
*
* Usually you should not call this, but just reload the data with
* {@see loadOptions} in case you need this. Otherwise, this leaves the
* module in an uninitalized/unexpected state.
*
* @public
* @returns {void}
* @deprecated Do use {@see setCaching} or {@see loadOptions} instead.
*/
export function clearCache() {
managedOptions = null;
syncOptions = null;
}
/**
* Caching is enabled by default. Call this with "false" to disable.
*
* @public
* @param {boolean} enableCachingNew
* @returns {void}
*/
export function setCaching(enableCachingNew) {
if (enableCachingNew !== true && enableCachingNew !== false) {
throw new TypeError(`First parameter must be a boolean parameter. "${enableCachingNew}" given.`);
}
// clear or load cache if this setting is toggled often
if (enableCachingNew) {
loadOptions();
} else {
clearCache();
}
enableCaching = enableCachingNew;
}
/**
* Returns the add-on setting to use in add-on.
*
* If only a single option is requested (option=string) the result of the
* promise will be that return value.
* Otherwise, you can pass no parmeter or "null" and it will return all
* saved config values.
*
* @public
* @param {string|null} [option=null] name of the option
* @returns {Promise} resulting in single value or object of values or undefined
* @throws {Error} if option is not available or other (internal) error happened
*/
export async function get(option = null) {
let result = undefined;
if (enableCaching === false) {
clearCache();
reloadOptions(option);
}
// verify managed options are loaded (or are not available)
await gettingManagedOption.catch(() => {
// ignore errors, as fallback to other storages is allowed
// (although "no manifest" error is already handled)
});
// return all options
if (option === null) {
return getAllOptions();
}
// first try to get managed option
if (managedOptions !== null && (option in managedOptions)) {
result = managedOptions[option];
console.info(`Managed setting got for "${option}".`, result);
// merge with default options, if this is an object, so default
// values are also provided
if (isPlainObject(result)) {
result = Object.assign({}, getDefaultValue(option), result);
}
return result;
} else {
await requireSyncedOptions();
if (syncOptions !== null && (option in syncOptions)) {
result = syncOptions[option];
console.info(`Synced setting got for "${option}".`, result);
// merge with default options, if this is an object, so default
// values are also provided
if (isPlainObject(result)) {
result = Object.assign({}, getDefaultValue(option), result);
}
return result;
}
}
// get default value as a last fallback
result = getDefaultValue(option);
// last fallback: default value
console.warn(`Could not get option "${option}". Using default.`, result);
return result;
}
/**
* Sets the settings.
*
* Note you can also pass a key -> value object to set here, as with the usual
* "set" storage API, or use the simplified two-parmeters version.
*
* @see {@link https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/storage/StorageArea/set}
* @public
* @param {Object|string} option keys/values to set or single value
* @param {Object} [value] if only a single value is to be set
* @returns {Promise}
* @throws {TypeError}
*/
export function set(option, value) {
// put params into object if needed
if (!isPlainObject(option)) {
if (value === undefined) {
return Promise.reject(new TypeError("Second argument 'value' has not been passed."));
}
option = {
[option]: value
};
}
return browser.storage.sync.set(option).then(() => {
// syncOptions is only null, if loadOptions() -> gettingSyncOption has not yet been resolved
// As such, we still have to save it already, as we do not want to introduce latency.
if (syncOptions === null) {
syncOptions = {};
}
// add to cache
Object.assign(syncOptions, option);
}).catch((error) => {
console.error("Could not save option:", option, error);
// re-throw error to make user aware something failed
throw error;
});
}
/**
* Fetches all options, so they can be used later.
*
* This is basically the init method!
* It returns a promise, but that does not have to be used, because the module is
* built in a way, so that the actual getting of the options is waiting for
* the promise.
*
* @public
* @returns {Promise}
*/
export function loadOptions() {
if (browser.storage === undefined) {
throw new Error("Storage API is not available.");
}
// clear storage first
clearCache();
// just fetch everything
reloadOptions(null);
// if the settings have been received anywhere, they could be loaded
return gettingManagedOption.catch(() => gettingSyncOption);
}
// automatically fetch options
loadOptions().then(() => {
console.info("AddonSettings module loaded.");
});