XPCOMUtils.sys.mjs (14014B)
1 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- 2 * vim: sw=2 ts=2 sts=2 et filetype=javascript 3 * This Source Code Form is subject to the terms of the Mozilla Public 4 * License, v. 2.0. If a copy of the MPL was not distributed with this 5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 6 7 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; 8 9 let global = Cu.getGlobalForObject({}); 10 11 // Some global imports expose additional symbols; for example, 12 // `Cu.importGlobalProperties(["MessageChannel"])` imports `MessageChannel` 13 // and `MessagePort`. This table maps those extra symbols to the main 14 // import name. 15 const EXTRA_GLOBAL_NAME_TO_IMPORT_NAME = { 16 MessagePort: "MessageChannel", 17 }; 18 19 /** 20 * Redefines the given property on the given object with the given 21 * value. This can be used to redefine getter properties which do not 22 * implement setters. 23 */ 24 function redefine(object, prop, value) { 25 Object.defineProperty(object, prop, { 26 configurable: true, 27 enumerable: true, 28 value, 29 writable: true, 30 }); 31 return value; 32 } 33 34 /** 35 * XPCOMUtils contains helpers to make lazily loading scripts, modules, prefs 36 * and XPCOM services more ergonomic for JS consumers. 37 * 38 * @class 39 */ 40 export var XPCOMUtils = { 41 /** 42 * Defines a getter on a specified object for a script. The script will not 43 * be loaded until first use. 44 * 45 * @param {object} aObject 46 * The object to define the lazy getter on. 47 * @param {string|string[]} aNames 48 * The name of the getter to define on aObject for the script. 49 * This can be a string if the script exports only one symbol, 50 * or an array of strings if the script can be first accessed 51 * from several different symbols. 52 * @param {string} aResource 53 * The URL used to obtain the script. 54 */ 55 defineLazyScriptGetter(aObject, aNames, aResource) { 56 if (!Array.isArray(aNames)) { 57 aNames = [aNames]; 58 } 59 for (let name of aNames) { 60 Object.defineProperty(aObject, name, { 61 get() { 62 XPCOMUtils._scriptloader.loadSubScript(aResource, aObject); 63 return aObject[name]; 64 }, 65 set(value) { 66 redefine(aObject, name, value); 67 }, 68 configurable: true, 69 enumerable: true, 70 }); 71 } 72 }, 73 74 /** 75 * Overrides the scriptloader definition for tests to help with globals 76 * tracking. Should only be used for tests. 77 * 78 * @param {object} aObject 79 * The alternative script loader object to use. 80 */ 81 overrideScriptLoaderForTests(aObject) { 82 Cu.crashIfNotInAutomation(); 83 delete this._scriptloader; 84 this._scriptloader = aObject; 85 }, 86 87 /** 88 * Defines a getter property on the given object for each of the given 89 * global names as accepted by Cu.importGlobalProperties. These 90 * properties are imported into the shared system global, and then 91 * copied onto the given object, no matter which global the object 92 * belongs to. 93 * 94 * @param {object} aObject 95 * The object on which to define the properties. 96 * @param {string[]} aNames 97 * The list of global properties to define. 98 */ 99 defineLazyGlobalGetters(aObject, aNames) { 100 for (let name of aNames) { 101 ChromeUtils.defineLazyGetter(aObject, name, () => { 102 if (!(name in global)) { 103 let importName = EXTRA_GLOBAL_NAME_TO_IMPORT_NAME[name] || name; 104 // eslint-disable-next-line mozilla/reject-importGlobalProperties, no-unused-vars 105 Cu.importGlobalProperties([importName]); 106 } 107 return global[name]; 108 }); 109 } 110 }, 111 112 /** 113 * Defines a getter on a specified object for a service. The service will not 114 * be obtained until first use. 115 * 116 * @param {object} aObject 117 * The object to define the lazy getter on. 118 * @param {string} aName 119 * The name of the getter to define on aObject for the service. 120 * @param {string} aContract 121 * The contract used to obtain the service. 122 * @param {nsIID} aInterface 123 * The interface or name of interface to query the service to. 124 */ 125 defineLazyServiceGetter(aObject, aName, aContract, aInterface) { 126 ChromeUtils.defineLazyGetter(aObject, aName, () => { 127 return Cc[aContract].getService(aInterface); 128 }); 129 }, 130 131 /** 132 * @typedef {{[key: string]: [string, nsIID]}} ServicesDetail 133 * Details of the services by name. The first item in the value array is the 134 * contract ID, the second is the nsIID for the interface of the service. 135 */ 136 137 /** 138 * Defines a lazy service getter on a specified object for each 139 * property in the given object. 140 * 141 * @param {object} aObject 142 * The object to define the lazy getter on. 143 * @param {ServicesDetail} aServices 144 * An object with a property for each service to be 145 * imported. 146 */ 147 defineLazyServiceGetters(aObject, aServices) { 148 for (let [name, service] of Object.entries(aServices)) { 149 // Note: This is hot code, and cross-compartment array wrappers 150 // are not JIT-friendly to destructuring or spread operators, so 151 // we need to use indexed access instead. 152 this.defineLazyServiceGetter(aObject, name, service[0], service[1]); 153 } 154 }, 155 156 /** 157 * Defines a getter on a specified object for preference value. The 158 * preference is read the first time that the property is accessed, 159 * and is thereafter kept up-to-date using a preference observer. 160 * 161 * @param {object} aObject 162 * The object to define the lazy getter on. 163 * @param {string} aName 164 * The name of the getter property to define on aObject. 165 * @param {string} aPreference 166 * The name of the preference to read. 167 * @param {any} aDefaultPrefValue 168 * The default value to use, if the preference is not defined. 169 * This is the default value of the pref, before applying aTransform. 170 * @param {Function} aOnUpdate 171 * A function to call upon update. Receives as arguments 172 * `(aPreference, previousValue, newValue)` 173 * @param {Function} aTransform 174 * An optional function to transform the value. If provided, 175 * this function receives the new preference value as an argument 176 * and its return value is used by the getter. 177 */ 178 defineLazyPreferenceGetter( 179 aObject, 180 aName, 181 aPreference, 182 aDefaultPrefValue = null, 183 aOnUpdate = null, 184 aTransform = val => val 185 ) { 186 if (AppConstants.DEBUG && aDefaultPrefValue !== null) { 187 let prefType = Services.prefs.getPrefType(aPreference); 188 if (prefType != Ci.nsIPrefBranch.PREF_INVALID) { 189 // The pref may get defined after the lazy getter is called 190 // at which point the code here won't know the expected type. 191 let prefTypeForDefaultValue = { 192 boolean: Ci.nsIPrefBranch.PREF_BOOL, 193 number: Ci.nsIPrefBranch.PREF_INT, 194 string: Ci.nsIPrefBranch.PREF_STRING, 195 }[typeof aDefaultPrefValue]; 196 if (prefTypeForDefaultValue != prefType) { 197 throw new Error( 198 `Default value does not match preference type (Got ${prefTypeForDefaultValue}, expected ${prefType}) for ${aPreference}` 199 ); 200 } 201 } 202 } 203 204 // Note: We need to keep a reference to this observer alive as long 205 // as aObject is alive. This means that all of our getters need to 206 // explicitly close over the variable that holds the object, and we 207 // cannot define a value in place of a getter after we read the 208 // preference. 209 let observer = { 210 QueryInterface: XPCU_lazyPreferenceObserverQI, 211 212 value: undefined, 213 214 observe(subject, topic, data) { 215 if (data == aPreference) { 216 if (aOnUpdate) { 217 let previous = this.value; 218 219 // Fetch and cache value. 220 this.value = undefined; 221 let latest = lazyGetter(); 222 aOnUpdate(data, previous, latest); 223 } else { 224 // Empty cache, next call to the getter will cause refetch. 225 this.value = undefined; 226 } 227 } 228 }, 229 }; 230 231 let defineGetter = get => { 232 Object.defineProperty(aObject, aName, { 233 configurable: true, 234 enumerable: true, 235 get, 236 }); 237 }; 238 239 function lazyGetter() { 240 if (observer.value === undefined) { 241 let prefValue; 242 switch (Services.prefs.getPrefType(aPreference)) { 243 case Ci.nsIPrefBranch.PREF_STRING: 244 prefValue = Services.prefs.getStringPref(aPreference); 245 break; 246 247 case Ci.nsIPrefBranch.PREF_INT: 248 prefValue = Services.prefs.getIntPref(aPreference); 249 break; 250 251 case Ci.nsIPrefBranch.PREF_BOOL: 252 prefValue = Services.prefs.getBoolPref(aPreference); 253 break; 254 255 case Ci.nsIPrefBranch.PREF_INVALID: 256 prefValue = aDefaultPrefValue; 257 break; 258 259 default: 260 // This should never happen. 261 throw new Error( 262 `Error getting pref ${aPreference}; its value's type is ` + 263 `${Services.prefs.getPrefType(aPreference)}, which I don't ` + 264 `know how to handle.` 265 ); 266 } 267 268 observer.value = aTransform(prefValue); 269 } 270 return observer.value; 271 } 272 273 defineGetter(() => { 274 Services.prefs.addObserver(aPreference, observer, true); 275 276 defineGetter(lazyGetter); 277 return lazyGetter(); 278 }); 279 }, 280 281 /** 282 * Defines properties on the given object which lazily import 283 * an ES module or run another utility getter when accessed. 284 * 285 * Use this version when you need to define getters on the 286 * global `this`, or any other object you can't assign to: 287 * 288 * @example 289 * XPCOMUtils.defineLazy(this, { 290 * AppConstants: "resource://gre/modules/AppConstants.sys.mjs", 291 * verticalTabs: { pref: "sidebar.verticalTabs", default: false }, 292 * MIME: { service: "@mozilla.org/mime;1", iid: Ci.nsInsIMIMEService }, 293 * expensiveThing: () => fetch_or_compute(), 294 * }); 295 * 296 * Additionally, the given object is also returned, which enables 297 * type-friendly composition: 298 * 299 * @example 300 * const existing = { 301 * someProps: new Widget(), 302 * }; 303 * const combined = XPCOMUtils.defineLazy(existing, { 304 * expensiveThing: () => fetch_or_compute(), 305 * }); 306 * 307 * The `combined` variable is the same object reference as `existing`, 308 * but TypeScript also knows about lazy getters defined on it. 309 * 310 * Since you probably don't want aliases, you can use it like this to, 311 * for example, define (static) lazy getters on a class: 312 * 313 * @example 314 * const Widget = XPCOMUtils.defineLazy( 315 * class Widget { 316 * static normalProp = 3; 317 * }, 318 * { 319 * verticalTabs: { pref: "sidebar.verticalTabs", default: false }, 320 * } 321 * ); 322 * 323 * @template {LazyDefinition} const L, T 324 * 325 * @param {T} lazy 326 * The object to define the getters on. 327 * 328 * @param {L} definition 329 * Each key:value property defines type and parameters for getters. 330 * 331 * - "resource://module" string 332 * @see ChromeUtils.defineESModuleGetters 333 * 334 * - () => value 335 * @see ChromeUtils.defineLazyGetter 336 * 337 * - { service: "contract", iid?: nsIID } 338 * @see XPCOMUtils.defineLazyServiceGetter 339 * 340 * - { pref: "name", default?, onUpdate?, transform? } 341 * @see XPCOMUtils.defineLazyPreferenceGetter 342 * 343 * @param {ImportESModuleOptionsDictionary} [options] 344 * When importing ESModules in devtools and worker contexts, 345 * the third parameter is required. 346 */ 347 defineLazy(lazy, definition, options) { 348 let modules = {}; 349 350 for (let [key, val] of Object.entries(definition)) { 351 if (typeof val === "string") { 352 modules[key] = val; 353 } else if (typeof val === "function") { 354 ChromeUtils.defineLazyGetter(lazy, key, val); 355 } else if ("service" in val) { 356 XPCOMUtils.defineLazyServiceGetter(lazy, key, val.service, val.iid); 357 } else if ("pref" in val) { 358 XPCOMUtils.defineLazyPreferenceGetter( 359 lazy, 360 key, 361 val.pref, 362 val.default, 363 val.onUpdate, 364 val.transform 365 ); 366 } else { 367 throw new Error(`Unkown LazyDefinition for ${key}`); 368 } 369 } 370 371 ChromeUtils.defineESModuleGetters(lazy, modules, options); 372 return /** @type {T & DeclaredLazy<L>} */ (lazy); 373 }, 374 375 /** 376 * @see XPCOMUtils.defineLazy 377 * A shorthand for above which always returns a new lazy object. 378 * Use this version if you have a global `lazy` const with all the getters: 379 * 380 * @example 381 * const lazy = XPCOMUtils.declareLazy({ 382 * AppConstants: "resource://gre/modules/AppConstants.sys.mjs", 383 * verticalTabs: { pref: "sidebar.verticalTabs", default: false }, 384 * MIME: { service: "@mozilla.org/mime;1", iid: Ci.nsInsIMIMEService }, 385 * expensiveThing: () => fetch_or_compute(), 386 * }); 387 * 388 * @template {LazyDefinition} const L 389 * @param {L} declaration 390 * @param {ImportESModuleOptionsDictionary} [options] 391 */ 392 declareLazy(declaration, options) { 393 return XPCOMUtils.defineLazy({}, declaration, options); 394 }, 395 396 /** 397 * Defines a non-writable property on an object. 398 * 399 * @param {object} aObj 400 * The object to define the property on. 401 * 402 * @param {string} aName 403 * The name of the non-writable property to define on aObject. 404 * 405 * @param {any} aValue 406 * The value of the non-writable property. 407 */ 408 defineConstant(aObj, aName, aValue) { 409 Object.defineProperty(aObj, aName, { 410 value: aValue, 411 enumerable: true, 412 writable: false, 413 }); 414 }, 415 }; 416 417 ChromeUtils.defineLazyGetter(XPCOMUtils, "_scriptloader", () => { 418 return Services.scriptloader; 419 }); 420 421 var XPCU_lazyPreferenceObserverQI = ChromeUtils.generateQI([ 422 "nsIObserver", 423 "nsISupportsWeakReference", 424 ]);