138 lines
5.4 KiB
Markdown
138 lines
5.4 KiB
Markdown
# Category manager indirection (callModulesFromCategory)
|
|
|
|
Firefox front-end code uses the category manager as a publish/subscribe
|
|
mechanism for dependency injection, so consumers can be notified of interesting
|
|
things happening without having to directly talk to the publisher/actor who
|
|
decides the interesting thing is happening.
|
|
|
|
There are 2 parts to this:
|
|
|
|
1. Consumers registering with the category manager
|
|
2. Publishers/actors invoking consumers via the category manager.
|
|
|
|
## Consumer registration with the category manager.
|
|
|
|
The category manager is used for various purposes within Firefox; it is more or
|
|
less an arbitrary double string-keyed data store.
|
|
|
|
For this particular usecase, the publisher/consumer have to use the same
|
|
primary key (i.e. category name), such as `browser-idle-startup`.
|
|
|
|
The secondary key is a full URL to a `sys.mjs` module. Note that because this
|
|
is a key, **only one consumer per module & category combination is possible**.
|
|
|
|
The "value" part is an `Object.method` notation, where the expectation is that
|
|
`Object` is an exported symbol from the module identified as the secondary key,
|
|
and `method` is some method on that object.
|
|
|
|
At compile-time, registration can happen with an entry in a `.manifest` file
|
|
like [BrowserComponents.manifest](https://searchfox.org/mozilla-central/source/browser/components/BrowserComponents.manifest).
|
|
Note that any manifest successfully processed by the build system would do,
|
|
we don't need to use `BrowserComponents.manifest` specifically. In fact, it
|
|
would be preferable if components used their own manifest files.
|
|
|
|
An example registration looks like:
|
|
|
|
```
|
|
category browser-idle-startup moz-src:///browser/components/tabbrowser/TabUnloader.sys.mjs TabUnloader.init
|
|
```
|
|
|
|
This will ensure that when the `browser-idle-startup` publisher is invoked,
|
|
the `TabUnloader.sys.mjs` module is loaded and the `init` method on the exported
|
|
`TabUnloader` object is invoked.
|
|
|
|
### Runtime registration
|
|
|
|
Runtime registration is less-often used, but can be done using the category
|
|
manager's XPCOM API:
|
|
|
|
```js
|
|
Services.catMan.addCategoryEntry(
|
|
"browser-idle-startup",
|
|
"moz-src:///browser/components/tabbrowser/TabUnloader.sys.mjs",
|
|
"TabUnloader.init"
|
|
)
|
|
```
|
|
|
|
## Publishers/actors invoking consumers
|
|
|
|
Publishers call `BrowserUtils.callModulesFromCategory` with a dictionary of
|
|
options as the first argument. If provided, any other arguments are passed
|
|
straight through to any consumers.
|
|
|
|
```{js:autofunction} BrowserUtils.callModulesFromCategory
|
|
```
|
|
|
|
Example:
|
|
|
|
```js
|
|
BrowserUtils.callModulesFromCategory({
|
|
categoryName: "my-fancy-category-name",
|
|
profilerMarker: "markMyCategories",
|
|
idleDispatch: true,
|
|
someArgument
|
|
});
|
|
```
|
|
|
|
This will pass `someArgument` to each consumer registered for
|
|
`my-fancy-category-name`. Each consumer will be invoked via an idle task, and
|
|
each task will get a profiler marker (labelled `"markMyCategories"`) in the
|
|
[Firefox Profiler](https://profiler.firefox.com/) so it's easy to find in
|
|
performance profiles.
|
|
|
|
You should consider using `idleDispatch: true` if invocation of the consumers
|
|
does not need to happen synchronously.
|
|
|
|
If you need to care about errors produced by consumers, you can specify
|
|
a function for `failureHandler` and handle any exceptions/errors using your own
|
|
logic. Note that it may be invoked asynchronously if the consumers are async.
|
|
|
|
## Caveats
|
|
|
|
Any errors thrown by consumers are automatically caught and reported via the
|
|
[Browser Console](/devtools-user/browser_console/index.rst).
|
|
|
|
Async functions are not awaited before invoking other consumers. Note that
|
|
rejections (exceptions from async code) are still caught and reported to the
|
|
console, and that the async duration of a given consumer will be what
|
|
determines the length of the profiler marker, if the publisher asks for profiler
|
|
markers.
|
|
|
|
## Why not just call consumers directly?
|
|
|
|
There are a number of benefits over direct method calls and module imports.
|
|
|
|
### Reducing direct dependencies between different parts of the code-base
|
|
|
|
Code that looks like this:
|
|
|
|
```
|
|
Foo.thingHappened(arg);
|
|
Bar.thingHappened(arg);
|
|
Baz.thingHappened(arg);
|
|
```
|
|
|
|
is not only repetitive, it also means that the code in question has to directly
|
|
import all the modules that provide `Foo`, `Bar` and `Baz`. It means that if
|
|
those modules change or move or are refactored, the "publisher" code has to
|
|
be updated, with all the added burdens that comes with (potential for merge
|
|
conflicts, more automatically added reviewer groups for trivial changes, easy
|
|
to miss if dependencies are widespread).
|
|
|
|
### Avoiding a bootstrap problem in favour of a "just in time" approach
|
|
|
|
To make sure code is invoked later, when using the observer service, DOM event
|
|
listeners, or other mechanisms, it usually needs to add a listener
|
|
before the event of interest happens. If not managed carefully, this often leads
|
|
to component initialization being front-loaded to make sure not to "miss" it
|
|
later. This in turn makes browser startup more heavyweight rather than it needs
|
|
to be, because we set up listeners for _everything_, potentially loading entire
|
|
JS modules just to do that.
|
|
|
|
### Unified error-handling, performance inspection, and scheduling
|
|
|
|
Using the `BrowserUtils.callModulesFromCategory` API allows specifying error
|
|
handling, performance profiler markers, and scheduling (use of idle tasks) in
|
|
one place. This abstracts away the fact that we never want observers, event
|
|
listeners or other mechanisms like this to break and stop notifying (or worse,
|
|
propagate an exception themselves) when one of the consumers breaks.
|