Source code
Revision control
Copy as Markdown
Other Tools
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
* vim: sw=2 ts=2 sts=2 et filetype=javascript
* 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
import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
let global = Cu.getGlobalForObject({});
// Some global imports expose additional symbols; for example,
// `Cu.importGlobalProperties(["MessageChannel"])` imports `MessageChannel`
// and `MessagePort`. This table maps those extra symbols to the main
// import name.
const EXTRA_GLOBAL_NAME_TO_IMPORT_NAME = {
MessagePort: "MessageChannel",
};
/**
* Redefines the given property on the given object with the given
* value. This can be used to redefine getter properties which do not
* implement setters.
*/
function redefine(object, prop, value) {
Object.defineProperty(object, prop, {
configurable: true,
enumerable: true,
value,
writable: true,
});
return value;
}
/**
* XPCOMUtils contains helpers to make lazily loading scripts, modules, prefs
* and XPCOM services more ergonomic for JS consumers.
*
* @class
*/
export var XPCOMUtils = {
/**
* Defines a getter on a specified object for a script. The script will not
* be loaded until first use.
*
* @param {object} aObject
* The object to define the lazy getter on.
* @param {string|string[]} aNames
* The name of the getter to define on aObject for the script.
* This can be a string if the script exports only one symbol,
* or an array of strings if the script can be first accessed
* from several different symbols.
* @param {string} aResource
* The URL used to obtain the script.
*/
defineLazyScriptGetter(aObject, aNames, aResource) {
if (!Array.isArray(aNames)) {
aNames = [aNames];
}
for (let name of aNames) {
Object.defineProperty(aObject, name, {
get() {
XPCOMUtils._scriptloader.loadSubScript(aResource, aObject);
return aObject[name];
},
set(value) {
redefine(aObject, name, value);
},
configurable: true,
enumerable: true,
});
}
},
/**
* Overrides the scriptloader definition for tests to help with globals
* tracking. Should only be used for tests.
*
* @param {object} aObject
* The alternative script loader object to use.
*/
overrideScriptLoaderForTests(aObject) {
Cu.crashIfNotInAutomation();
delete this._scriptloader;
this._scriptloader = aObject;
},
/**
* Defines a getter property on the given object for each of the given
* global names as accepted by Cu.importGlobalProperties. These
* properties are imported into the shared system global, and then
* copied onto the given object, no matter which global the object
* belongs to.
*
* @param {object} aObject
* The object on which to define the properties.
* @param {string[]} aNames
* The list of global properties to define.
*/
defineLazyGlobalGetters(aObject, aNames) {
for (let name of aNames) {
ChromeUtils.defineLazyGetter(aObject, name, () => {
if (!(name in global)) {
let importName = EXTRA_GLOBAL_NAME_TO_IMPORT_NAME[name] || name;
// eslint-disable-next-line mozilla/reject-importGlobalProperties, no-unused-vars
Cu.importGlobalProperties([importName]);
}
return global[name];
});
}
},
/**
* Defines a getter on a specified object for a service. The service will not
* be obtained until first use.
*
* @param {object} aObject
* The object to define the lazy getter on.
* @param {string} aName
* The name of the getter to define on aObject for the service.
* @param {string} aContract
* The contract used to obtain the service.
* @param {nsID|string} aInterface
* The interface or name of interface to query the service to.
*/
defineLazyServiceGetter(aObject, aName, aContract, aInterface) {
ChromeUtils.defineLazyGetter(aObject, aName, () => {
if (aInterface) {
if (typeof aInterface === "string") {
aInterface = Ci[aInterface];
}
return Cc[aContract].getService(aInterface);
}
return Cc[aContract].getService().wrappedJSObject;
});
},
/**
* Defines a lazy service getter on a specified object for each
* property in the given object.
*
* @param {object} aObject
* The object to define the lazy getter on.
* @param {object} aServices
* An object with a property for each service to be
* imported, where the property name is the name of the
* symbol to define, and the value is a 1 or 2 element array
* containing the contract ID and, optionally, the interface
* name of the service, as passed to defineLazyServiceGetter.
*/
defineLazyServiceGetters(aObject, aServices) {
for (let [name, service] of Object.entries(aServices)) {
// Note: This is hot code, and cross-compartment array wrappers
// are not JIT-friendly to destructuring or spread operators, so
// we need to use indexed access instead.
this.defineLazyServiceGetter(
aObject,
name,
service[0],
service[1] || null
);
}
},
/**
* Defines a getter on a specified object for preference value. The
* preference is read the first time that the property is accessed,
* and is thereafter kept up-to-date using a preference observer.
*
* @param {object} aObject
* The object to define the lazy getter on.
* @param {string} aName
* The name of the getter property to define on aObject.
* @param {string} aPreference
* The name of the preference to read.
* @param {any} aDefaultPrefValue
* The default value to use, if the preference is not defined.
* This is the default value of the pref, before applying aTransform.
* @param {Function} aOnUpdate
* A function to call upon update. Receives as arguments
* `(aPreference, previousValue, newValue)`
* @param {Function} aTransform
* An optional function to transform the value. If provided,
* this function receives the new preference value as an argument
* and its return value is used by the getter.
*/
defineLazyPreferenceGetter(
aObject,
aName,
aPreference,
aDefaultPrefValue = null,
aOnUpdate = null,
aTransform = val => val
) {
if (AppConstants.DEBUG && aDefaultPrefValue !== null) {
let prefType = Services.prefs.getPrefType(aPreference);
if (prefType != Ci.nsIPrefBranch.PREF_INVALID) {
// The pref may get defined after the lazy getter is called
// at which point the code here won't know the expected type.
let prefTypeForDefaultValue = {
boolean: Ci.nsIPrefBranch.PREF_BOOL,
number: Ci.nsIPrefBranch.PREF_INT,
string: Ci.nsIPrefBranch.PREF_STRING,
}[typeof aDefaultPrefValue];
if (prefTypeForDefaultValue != prefType) {
throw new Error(
`Default value does not match preference type (Got ${prefTypeForDefaultValue}, expected ${prefType}) for ${aPreference}`
);
}
}
}
// Note: We need to keep a reference to this observer alive as long
// as aObject is alive. This means that all of our getters need to
// explicitly close over the variable that holds the object, and we
// cannot define a value in place of a getter after we read the
// preference.
let observer = {
QueryInterface: XPCU_lazyPreferenceObserverQI,
value: undefined,
observe(subject, topic, data) {
if (data == aPreference) {
if (aOnUpdate) {
let previous = this.value;
// Fetch and cache value.
this.value = undefined;
let latest = lazyGetter();
aOnUpdate(data, previous, latest);
} else {
// Empty cache, next call to the getter will cause refetch.
this.value = undefined;
}
}
},
};
let defineGetter = get => {
Object.defineProperty(aObject, aName, {
configurable: true,
enumerable: true,
get,
});
};
function lazyGetter() {
if (observer.value === undefined) {
let prefValue;
switch (Services.prefs.getPrefType(aPreference)) {
case Ci.nsIPrefBranch.PREF_STRING:
prefValue = Services.prefs.getStringPref(aPreference);
break;
case Ci.nsIPrefBranch.PREF_INT:
prefValue = Services.prefs.getIntPref(aPreference);
break;
case Ci.nsIPrefBranch.PREF_BOOL:
prefValue = Services.prefs.getBoolPref(aPreference);
break;
case Ci.nsIPrefBranch.PREF_INVALID:
prefValue = aDefaultPrefValue;
break;
default:
// This should never happen.
throw new Error(
`Error getting pref ${aPreference}; its value's type is ` +
`${Services.prefs.getPrefType(aPreference)}, which I don't ` +
`know how to handle.`
);
}
observer.value = aTransform(prefValue);
}
return observer.value;
}
defineGetter(() => {
Services.prefs.addObserver(aPreference, observer, true);
defineGetter(lazyGetter);
return lazyGetter();
});
},
/**
* Defines properties on the given object which lazily import
* an ES module or run another utility getter when accessed.
*
* Use this version when you need to define getters on the
* global `this`, or any other object you can't assign to:
*
* @example
* XPCOMUtils.defineLazy(this, {
* AppConstants: "resource://gre/modules/AppConstants.sys.mjs",
* verticalTabs: { pref: "sidebar.verticalTabs", default: false },
* MIME: { service: "@mozilla.org/mime;1", iid: Ci.nsInsIMIMEService },
* expensiveThing: () => fetch_or_compute(),
* });
*
* Additionally, the given object is also returned, which enables
* type-friendly composition:
*
* @example
* const existing = {
* someProps: new Widget(),
* };
* const combined = XPCOMUtils.defineLazy(existing, {
* expensiveThing: () => fetch_or_compute(),
* });
*
* The `combined` variable is the same object reference as `existing`,
* but TypeScript also knows about lazy getters defined on it.
*
* Since you probably don't want aliases, you can use it like this to,
* for example, define (static) lazy getters on a class:
*
* @example
* const Widget = XPCOMUtils.defineLazy(
* class Widget {
* static normalProp = 3;
* },
* {
* verticalTabs: { pref: "sidebar.verticalTabs", default: false },
* }
* );
*
* @template {LazyDefinition} const L, T
*
* @param {T} lazy
* The object to define the getters on.
*
* @param {L} definition
* Each key:value property defines type and parameters for getters.
*
* @see ChromeUtils.defineESModuleGetters
*
* - () => value
* @see ChromeUtils.defineLazyGetter
*
* - { service: "contract", iid?: nsIID }
* @see XPCOMUtils.defineLazyServiceGetter
*
* - { pref: "name", default?, onUpdate?, transform? }
* @see XPCOMUtils.defineLazyPreferenceGetter
*
* @param {ImportESModuleOptionsDictionary} [options]
* When importing ESModules in devtools and worker contexts,
* the third parameter is required.
*/
defineLazy(lazy, definition, options) {
let modules = {};
for (let [key, val] of Object.entries(definition)) {
if (typeof val === "string") {
modules[key] = val;
} else if (typeof val === "function") {
ChromeUtils.defineLazyGetter(lazy, key, val);
} else if ("service" in val) {
XPCOMUtils.defineLazyServiceGetter(lazy, key, val.service, val.iid);
} else if ("pref" in val) {
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
key,
val.pref,
val.default,
val.onUpdate,
val.transform
);
} else {
throw new Error(`Unkown LazyDefinition for ${key}`);
}
}
ChromeUtils.defineESModuleGetters(lazy, modules, options);
return /** @type {T & DeclaredLazy<L>} */ (lazy);
},
/**
* @see XPCOMUtils.defineLazy
* A shorthand for above which always returns a new lazy object.
* Use this version if you have a global `lazy` const with all the getters:
*
* @example
* const lazy = XPCOMUtils.declareLazy({
* AppConstants: "resource://gre/modules/AppConstants.sys.mjs",
* verticalTabs: { pref: "sidebar.verticalTabs", default: false },
* MIME: { service: "@mozilla.org/mime;1", iid: Ci.nsInsIMIMEService },
* expensiveThing: () => fetch_or_compute(),
* });
*
* @template {LazyDefinition} const L
* @param {L} declaration
* @param {ImportESModuleOptionsDictionary} [options]
*/
declareLazy(declaration, options) {
return XPCOMUtils.defineLazy({}, declaration, options);
},
/**
* Defines a non-writable property on an object.
*
* @param {object} aObj
* The object to define the property on.
*
* @param {string} aName
* The name of the non-writable property to define on aObject.
*
* @param {any} aValue
* The value of the non-writable property.
*/
defineConstant(aObj, aName, aValue) {
Object.defineProperty(aObj, aName, {
value: aValue,
enumerable: true,
writable: false,
});
},
};
ChromeUtils.defineLazyGetter(XPCOMUtils, "_scriptloader", () => {
return Services.scriptloader;
});
var XPCU_lazyPreferenceObserverQI = ChromeUtils.generateQI([
"nsIObserver",
"nsISupportsWeakReference",
]);