Bug 1896775 - set up a category manager helper in XPCOMUtils to enable easy JS dependency injection, r=mossop
Differential Revision: https://phabricator.services.mozilla.com/D210432
This commit is contained in:
@@ -31,6 +31,69 @@ function redefine(object, prop, value) {
|
||||
return value;
|
||||
}
|
||||
|
||||
let lazy = {};
|
||||
ChromeUtils.defineLazyGetter(lazy, "CatManListenerManager", () => {
|
||||
const CatManListenerManager = {
|
||||
cachedModules: {},
|
||||
cachedListeners: {},
|
||||
// All 3 category manager notifications will have the category name
|
||||
// as the `data` part of the observer notification.
|
||||
observe(_subject, _topic, categoryName) {
|
||||
delete this.cachedListeners[categoryName];
|
||||
},
|
||||
/**
|
||||
* Fetch and parse category manager consumers for a given category name.
|
||||
* Will use cachedListeners for the given category name if they exist.
|
||||
*/
|
||||
getListeners(categoryName) {
|
||||
if (Object.hasOwn(this.cachedListeners, categoryName)) {
|
||||
return this.cachedListeners[categoryName];
|
||||
}
|
||||
let rv = Array.from(
|
||||
Services.catMan.enumerateCategory(categoryName),
|
||||
({ data: module, value }) => {
|
||||
try {
|
||||
let [objName, method] = value.split(".");
|
||||
if (!Object.hasOwn(this.cachedModules, module)) {
|
||||
this.cachedModules[module] = ChromeUtils.importESModule(module);
|
||||
}
|
||||
let fn = async (...args) => {
|
||||
try {
|
||||
// This await doesn't do much as the caller won't await us,
|
||||
// but means we can catch and report any exceptions.
|
||||
await this.cachedModules[module][objName][method](...args);
|
||||
} catch (ex) {
|
||||
console.error(
|
||||
`Error in processing ${categoryName} for ${objName}`
|
||||
);
|
||||
console.error(ex);
|
||||
}
|
||||
};
|
||||
return fn;
|
||||
} catch (ex) {
|
||||
console.error(
|
||||
`Error processing category manifest for ${module}: ${value}`,
|
||||
ex
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
);
|
||||
// Remove any null entries.
|
||||
rv = rv.filter(l => !!l);
|
||||
this.cachedListeners[categoryName] = rv;
|
||||
return rv;
|
||||
},
|
||||
};
|
||||
Services.obs.addObserver(
|
||||
CatManListenerManager,
|
||||
"xpcom-category-entry-removed"
|
||||
);
|
||||
Services.obs.addObserver(CatManListenerManager, "xpcom-category-entry-added");
|
||||
Services.obs.addObserver(CatManListenerManager, "xpcom-category-cleared");
|
||||
return CatManListenerManager;
|
||||
});
|
||||
|
||||
export var XPCOMUtils = {
|
||||
/**
|
||||
* Defines a getter on a specified object that will be created upon first use.
|
||||
@@ -322,6 +385,22 @@ export var XPCOMUtils = {
|
||||
writable: false,
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Invoke all the category manager consumers of a given JS consumer.
|
||||
* Similar to the (C++-only) NS_CreateServicesFromCategory in that it'll
|
||||
* abstract away the actual work of invoking the modules/services.
|
||||
* Different in that it's JS-only and will invoke methods in modules
|
||||
* instead of using XPCOM services.
|
||||
*/
|
||||
callModulesFromCategory(categoryName, ...args) {
|
||||
for (let listener of lazy.CatManListenerManager.getListeners(
|
||||
categoryName
|
||||
)) {
|
||||
// Note that we deliberately do not await anything here.
|
||||
listener(...args);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
ChromeUtils.defineLazyGetter(XPCOMUtils, "_scriptloader", () => {
|
||||
|
||||
5
js/xpconnect/tests/unit/my_catman_1.sys.mjs
Normal file
5
js/xpconnect/tests/unit/my_catman_1.sys.mjs
Normal file
@@ -0,0 +1,5 @@
|
||||
export let Module1 = {
|
||||
test(arg) {
|
||||
Services.obs.notifyObservers(null, "test-modules-from-catman-notification", arg);
|
||||
},
|
||||
};
|
||||
6
js/xpconnect/tests/unit/my_catman_2.sys.mjs
Normal file
6
js/xpconnect/tests/unit/my_catman_2.sys.mjs
Normal file
@@ -0,0 +1,6 @@
|
||||
export let Module2 = {
|
||||
othertest(arg) {
|
||||
Services.obs.notifyObservers(null, "test-modules-from-catman-other-notification", arg);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
const {AppConstants} = ChromeUtils.importESModule("resource://gre/modules/AppConstants.sys.mjs");
|
||||
const {ComponentUtils} = ChromeUtils.importESModule("resource://gre/modules/ComponentUtils.sys.mjs");
|
||||
const {Preferences} = ChromeUtils.importESModule("resource://gre/modules/Preferences.sys.mjs");
|
||||
const {TestUtils} = ChromeUtils.importESModule("resource://testing-common/TestUtils.sys.mjs");
|
||||
const {XPCOMUtils} = ChromeUtils.importESModule("resource://gre/modules/XPCOMUtils.sys.mjs");
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
@@ -239,6 +240,85 @@ add_test(function test_generateSingletonFactory()
|
||||
run_next_test();
|
||||
});
|
||||
|
||||
/**
|
||||
* Verify that category manager calling modules are loaded on-demand,
|
||||
* and that caching doesn't break adding more modules as category entries
|
||||
* at runtime.
|
||||
*/
|
||||
add_test(async function test_callModulesFromCategory() {
|
||||
const MODULE1 = "resource://test/my_catman_1.sys.mjs";
|
||||
const MODULE2 = "resource://test/my_catman_2.sys.mjs";
|
||||
const CATEGORY = "test-modules-from-catman";
|
||||
const OBSTOPIC1 = CATEGORY + "-notification";
|
||||
const OBSTOPIC2 = CATEGORY + "-other-notification";
|
||||
|
||||
// The two modules both fire different observer topics to allow us to ensure
|
||||
// they have been called. This helper just makes it easier to get only
|
||||
// that return value as a result of a promise, as `topicObserved` also
|
||||
// returns the "subject" of the observer notification, which we don't care about.
|
||||
let rvFromModule = topic => TestUtils.topicObserved(topic).then(
|
||||
([_subj, data]) => data
|
||||
);
|
||||
|
||||
// Start off with nothing in a category:
|
||||
Assert.equal(Cu.isESModuleLoaded(MODULE1), false, "First module should not be loaded.");
|
||||
let catEntries = Array.from(Services.catMan.enumerateCategory(CATEGORY));
|
||||
Assert.deepEqual(catEntries, [], "Should be no entries for this category.");
|
||||
|
||||
try {
|
||||
// There's nothing in this category right now so this should be a no-op.
|
||||
XPCOMUtils.callModulesFromCategory(CATEGORY, "Hello");
|
||||
} catch (ex) {
|
||||
Assert.ok(false, `Should not have thrown but received an exception ${ex}`);
|
||||
}
|
||||
|
||||
// Now add an item, check that calling it now works.
|
||||
//
|
||||
// Note that category manager observer notifications are async (they get
|
||||
// dispatched as runnables) and so we have to wait for it to make sure that
|
||||
// XPCOMUtils has had a chance of being told new entries have arrived.
|
||||
let catManUpdated = TestUtils.topicObserved("xpcom-category-entry-added");
|
||||
|
||||
Services.catMan.addCategoryEntry(CATEGORY, MODULE1, `Module1.test`, false, false);
|
||||
catEntries = Array.from(Services.catMan.enumerateCategory(CATEGORY));
|
||||
Assert.equal(catEntries.length, 1);
|
||||
|
||||
// See note above.
|
||||
await catManUpdated;
|
||||
|
||||
Assert.equal(Cu.isESModuleLoaded(MODULE1), false, "First module should still not be loaded.");
|
||||
|
||||
// This entry will cause an observer topic to notify, so ensure that happens.
|
||||
let moduleResult = rvFromModule(OBSTOPIC1);
|
||||
XPCOMUtils.callModulesFromCategory(CATEGORY, "Hello");
|
||||
Assert.equal(Cu.isESModuleLoaded(MODULE1), true, "First module should be loaded sync.");
|
||||
Assert.equal("Hello", await moduleResult, "Should have been called.");
|
||||
|
||||
// Now add another item, check that both are called.
|
||||
catManUpdated = TestUtils.topicObserved("xpcom-category-entry-added");
|
||||
Services.catMan.addCategoryEntry(CATEGORY, MODULE2, `Module2.othertest`, false, false);
|
||||
await catManUpdated;
|
||||
|
||||
moduleResult = Promise.all([rvFromModule(OBSTOPIC1), rvFromModule(OBSTOPIC2)]);
|
||||
|
||||
XPCOMUtils.callModulesFromCategory(CATEGORY, "Hello");
|
||||
Assert.deepEqual(["Hello", "Hello"], await moduleResult, "Both modules should have been called.");
|
||||
|
||||
// Now remove the first module again, check that only the second one notifies.
|
||||
catManUpdated = TestUtils.topicObserved("xpcom-category-entry-removed");
|
||||
Services.catMan.deleteCategoryEntry(CATEGORY, MODULE1, false);
|
||||
await catManUpdated;
|
||||
let ob = () => Assert.ok(false, "I shouldn't be called.");
|
||||
Services.obs.addObserver(ob, OBSTOPIC1);
|
||||
|
||||
moduleResult = rvFromModule(OBSTOPIC2);
|
||||
XPCOMUtils.callModulesFromCategory(CATEGORY, "Hello");
|
||||
Assert.equal("Hello", await moduleResult, "Second module should still be called.");
|
||||
Services.obs.removeObserver(ob, OBSTOPIC1);
|
||||
|
||||
run_next_test();
|
||||
});
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//// Test Runner
|
||||
|
||||
|
||||
@@ -427,6 +427,10 @@ head = "head_watchdog.js"
|
||||
["test_wrapped_js_enumerator.js"]
|
||||
|
||||
["test_xpcomutils.js"]
|
||||
support-files = [
|
||||
"my_catman_1.sys.mjs",
|
||||
"my_catman_2.sys.mjs",
|
||||
]
|
||||
|
||||
["test_xpcwn_instanceof.js"]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user