summaryrefslogtreecommitdiffstats
path: root/toolkit/components/extensions/docs/events.rst
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/extensions/docs/events.rst')
-rw-r--r--toolkit/components/extensions/docs/events.rst609
1 files changed, 609 insertions, 0 deletions
diff --git a/toolkit/components/extensions/docs/events.rst b/toolkit/components/extensions/docs/events.rst
new file mode 100644
index 0000000000..d494155ffc
--- /dev/null
+++ b/toolkit/components/extensions/docs/events.rst
@@ -0,0 +1,609 @@
+Implementing an event
+=====================
+Like a function, an event requires a definition in the schema and
+an implementation in Javascript inside an instance of ExtensionAPI.
+
+Declaring an event in the API schema
+------------------------------------
+The definition for a simple event looks like this:
+
+.. code-block:: json
+
+ [
+ {
+ "namespace": "myapi",
+ "events": [
+ {
+ "name": "onSomething",
+ "type": "function",
+ "description": "Description of the event",
+ "parameters": [
+ {
+ "name": "param1",
+ "description": "Description of the first callback parameter",
+ "type": "number"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+
+This fragment defines an event that is used from an extension with
+code such as:
+
+.. code-block:: js
+
+ browser.myapi.onSomething.addListener(param1 => {
+ console.log(`Something happened: ${param1}`);
+ });
+
+Note that the schema syntax looks similar to that for a function,
+but for an event, the ``parameters`` property specifies the arguments
+that will be passed to a listener.
+
+Implementing an event
+---------------------
+Just like with functions, defining an event in the schema causes
+wrappers to be automatically created and exposed to an extensions'
+appropriate Javascript contexts.
+An event appears to an extension as an object with three standard
+function properties: ``addListener()``, ``removeListener()``,
+and ``hasListener()``.
+Also like functions, if an API defines an event but does not implement
+it in a child process, the wrapper in the child process effectively
+proxies these calls to the implementation in the main process.
+
+A helper class called
+`EventManager <reference.html#eventmanager-class>`_ makes implementing
+events relatively simple. A simple event implementation looks like:
+
+.. code-block:: js
+
+ this.myapi = class extends ExtensionAPI {
+ getAPI(context) {
+ return {
+ myapi: {
+ onSomething: new EventManager({
+ context,
+ name: "myapi.onSomething",
+ register: fire => {
+ const callback = value => {
+ fire.async(value);
+ };
+ RegisterSomeInternalCallback(callback);
+ return () => {
+ UnregisterInternalCallback(callback);
+ };
+ }
+ }).api(),
+ }
+ }
+ }
+ }
+
+The ``EventManager`` class is usually just used directly as in this example.
+The first argument to the constructor is an ``ExtensionContext`` instance,
+typically just the object passed to the API's ``getAPI()`` function.
+The second argument is a name, it is used only for debugging.
+The third argument is the important piece, it is a function that is called
+the first time a listener is added for this event.
+This function is passed an object (``fire`` in the example) that is used to
+invoke the extension's listener whenever the event occurs. The ``fire``
+object has several different methods for invoking listeners, but for
+events implemented in the main process, the only valid method is
+``async()`` which executes the listener asynchronously.
+
+The event setup function (the function passed to the ``EventManager``
+constructor) must return a cleanup function,
+which will be called when the listener is removed either explicitly
+by the extension by calling ``removeListener()`` or implicitly when
+the extension Javascript context from which the listener was added is destroyed.
+
+In this example, ``RegisterSomeInternalCallback()`` and
+``UnregisterInternalCallback()`` represent methods for listening for
+some internal browser event from chrome privileged code. This is
+typically something like adding an observer using ``Services.obs`` or
+attaching a listener to an ``EventEmitter``.
+
+After constructing an instance of ``EventManager``, its ``api()`` method
+returns an object with with ``addListener()``, ``removeListener()``, and
+``hasListener()`` methods. This is the standard extension event interface,
+this object is suitable for returning from the extension's
+``getAPI()`` method as in the example above.
+
+Handling extra arguments to addListener()
+-----------------------------------------
+The standard ``addListener()`` method for events may accept optional
+addition parameters to allow extra information to be passed when registering
+an event listener. One common application of this parameter is for filtering,
+so that extensions that only care about a small subset of the instances of
+some event can avoid the overhead of receiving the ones they don't care about.
+
+Extra parameters to ``addListener()`` are defined in the schema with the
+the ``extraParameters`` property. For example:
+
+.. code-block:: json
+
+ [
+ {
+ "namespace": "myapi",
+ "events": [
+ {
+ "name": "onSomething",
+ "type": "function",
+ "description": "Description of the event",
+ "parameters": [
+ {
+ "name": "param1",
+ "description": "Description of the first callback parameter",
+ "type": "number"
+ }
+ ],
+ "extraParameters": [
+ {
+ "name": "minValue",
+ "description": "Only call the listener for values of param1 at least as large as this value.",
+ "type": "number"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+
+Extra parameters defined in this way are passed to the event setup
+function (the last parameter to the ``EventManager`` constructor.
+For example, extending our example above:
+
+.. code-block:: js
+
+ this.myapi = class extends ExtensionAPI {
+ getAPI(context) {
+ return {
+ myapi: {
+ onSomething: new EventManager({
+ context,
+ module: "myapi",
+ event: "onSomething",
+ register: (fire, minValue) => {
+ const callback = value => {
+ if (value >= minValue) {
+ fire.async(value);
+ }
+ };
+ RegisterSomeInternalCallback(callback);
+ return () => {
+ UnregisterInternalCallback(callback);
+ };
+ }
+ }).api()
+ }
+ }
+ }
+ }
+
+Handling listener return values
+-------------------------------
+Some event APIs allow extensions to affect event handling in some way
+by returning values from event listeners that are processed by the API.
+This can be defined in the schema with the ``returns`` property:
+
+.. code-block:: json
+
+ [
+ {
+ "namespace": "myapi",
+ "events": [
+ {
+ "name": "onSomething",
+ "type": "function",
+ "description": "Description of the event",
+ "parameters": [
+ {
+ "name": "param1",
+ "description": "Description of the first callback parameter",
+ "type": "number"
+ }
+ ],
+ "returns": {
+ "type": "string",
+ "description": "Description of how the listener return value is processed."
+ }
+ }
+ ]
+ }
+ ]
+
+And the implementation of the event uses the return value from ``fire.async()``
+which is a Promise that resolves to the listener's return value:
+
+.. code-block:: js
+
+ this.myapi = class extends ExtensionAPI {
+ getAPI(context) {
+ return {
+ myapi: {
+ onSomething: new EventManager({
+ context,
+ module: "myapi",
+ event: "onSomething",
+ register: fire => {
+ const callback = async (value) => {
+ let rv = await fire.async(value);
+ log(`The onSomething listener returned the string ${rv}`);
+ };
+ RegisterSomeInternalCallback(callback);
+ return () => {
+ UnregisterInternalCallback(callback);
+ };
+ }
+ }).api()
+ }
+ }
+ }
+ }
+
+Note that the schema ``returns`` definition is optional and serves only
+for documentation. That is, ``fire.async()`` always returns a Promise
+that resolves to the listener return value, the implementation of an
+event can just ignore this Promise if it doesn't care about the return value.
+
+Implementing an event in the child process
+------------------------------------------
+The reasons for implementing events in the child process are similar to
+the reasons for implementing functions in the child process:
+
+- Listeners for the event return a value that the API implementation must
+ act on synchronously.
+
+- Either ``addListener()`` or the listener function has one or more
+ parameters of a type that cannot be sent between processes.
+
+- The implementation of the event interacts with code that is only
+ accessible from a child process.
+
+- The event can be implemented substantially more efficiently in a
+ child process.
+
+The process for implementing an event in the child process is the same
+as for functions -- simply implement the event in an ExtensionAPI subclass
+that is loaded in a child process. And just as a function in a child
+process can call a function in the main process with
+`callParentAsyncFunction()`, events in a child process may subscribe to
+events implemented in the main process with a similar `getParentEvent()`.
+For example, the automatically generated event proxy in a child process
+could be written explicitly as:
+
+.. code-block:: js
+
+ this.myapi = class extends ExtensionAPI {
+ getAPI(context) {
+ return {
+ myapi: {
+ onSomething: new EventManager(
+ context,
+ name: "myapi.onSomething",
+ register: fire => {
+ const listener = (value) => {
+ fire.async(value);
+ };
+
+ let parentEvent = context.childManager.getParentEvent("myapi.onSomething");
+ parent.addListener(listener);
+ return () => {
+ parent.removeListener(listener);
+ };
+ }
+ }).api()
+ }
+ }
+ }
+ }
+
+Events implemented in a child process have some additional methods available
+to dispatch listeners:
+
+- ``fire.sync()`` This runs the listener synchronously and returns the
+ value returned by the listener
+
+- ``fire.raw()`` This runs the listener synchronously without cloning
+ the listener arguments into the extension's Javascript compartment.
+ This is used as a performance optimization, it should not be used
+ unless you have a detailed understanding of Javascript compartments
+ and cross-compartment wrappers.
+
+Event Listeners Persistence
+---------------------------
+
+Event listeners are persisted in some circumstances. Persisted event listeners can either
+block startup, and/or cause an Event Page or Background Service Worker to be started.
+
+The event listener must be registered synchronously in the top level scope
+of the background. Event listeners registered later, or asynchronously, are
+not persisted.
+
+Currently only WebRequestBlocking and Proxy events are able to block
+at startup, causing an addon to start earlier in Firefox startup. Whether
+a module can block startup is defined by a ``startupBlocking`` flag in
+the module definition files (``ext-toolkit.json`` or ``ext-browser.json``).
+As well, these are the only events persisted for persistent background scripts.
+
+Events implemented only in a child process, without a parent process counterpart,
+cannot be persisted.
+
+Persisted and Primed Event Listeners
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+In terms of terminology:
+
+- **Persisted Event Listener** is the set of data (in particular API module, API event name
+ and the parameters passed along with addListener call if any) related to an event listener
+ that has been registered by an Event Page (or Background Service Worker) in a previous run
+ and being stored in the StartupCache data
+
+- **Primed Event Listener** is a "placeholder" event listener created, from the **Persisted Event Listener**
+ data found in the StartupCache, while the Event Page (or Background Service Worker) is not running
+ (either not started yet or suspended after the idle timeout was hit)
+
+ExtensionAPIPersistent and PERSISTENT_EVENTS
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Most of the WebExtensions APIs promise some API events, and it is likely that most of those events are also
+expected to be waking up the Event Page (or Background Service Worker) when emitted while the background
+extension context has not been started yet (or it was suspended after the idle timeout was hit).
+
+As part of implementing a WebExtensions API that is meant to persist all or some of its API event
+listeners:
+
+- the WebExtensions API namespace class should extend ``ExtensionAPIPersistent`` (instead of extending
+ the ``ExtensionAPI`` class)
+
+- the WebExtensions API namespace should have a ``PERSISTENT_EVENTS`` property, which is expected to be
+ set to an object defining methods. Each method should be named after the related API event name, which
+ are going to be called internally:
+
+ - while the extension Event Page (or Background Service Worker) isn't running (either never started yet
+ or suspended after the idle timeout). These methods are called by the WebExtensions internals to
+ create placeholder API event listeners in the parent process for each of the API event listeners
+ persisted for that extension. These placeholder listeners are internally referred to as
+ ``primed listeners``).
+
+ - while the extension Event Page (or Background Service Worker) is running (as well as for any other
+ extension context types they may have been created for the extension). These methods are called by the
+ WebExtensions internals to create the parent process callback that will be responsible for
+ forwarding the API events to the extension callbacks in the child processes.
+
+- in the ``getAPI`` method. For all the API namespace properties that represent API events returned by this method,
+ the ``EventManager`` instances created for each of the API events that is expected to persist its listeners
+ should include following options:
+
+ - ``module``, to be set to the API module name as listed in ``"ext-toolkit.json"`` / ``"ext-browser.json"``
+ / ``"ext-android.json"`` (which, in most cases, is the same as the API namespace name string).
+ - ``event``, to be set to the API event name string.
+ - ``extensionApi``, to be set to the ``ExtensionAPIPersistent`` class instance.
+
+Taking a look to some of the patches landed to introduce API event listener persistency on some of the existing
+API as part of introducing support for the Event Page may also be useful:
+
+- Bug-1748546_ ported the browserAction and pageAction API namespace implementations to
+ ``ExtensionAPIPersistent`` and, in particular, the changes applied to:
+
+ - ext-browserAction.js: https://hg.mozilla.org/integration/autoland/rev/08a3eaa8bce7
+ - ext-pageAction.js: https://hg.mozilla.org/integration/autoland/rev/ed616e2e0abb
+
+.. _Bug-1748546: https://bugzilla.mozilla.org/show_bug.cgi?id=1748546
+
+Follows an example of what has been described previously in a code snippet form:
+
+.. code-block:: js
+
+ this.myApiName = class extends ExtensionAPIPersistent {
+ PERSISTENT_EVENTS = {
+ // @param {object} options
+ // @param {object} options.fire
+ // @param {function} options.fire.async
+ // @param {function} options.fire.sync
+ // @param {function} options.fire.raw
+ // For primed listeners `fire.async`/`fire.sync`/`fire.raw` will
+ // collect the pending events to be send to the background context
+ // and implicitly wake up the background context (Event Page or
+ // Background Service Worker), or forward the event right away if
+ // the background context is running.
+ // @param {function} [options.fire.wakeup = undefined]
+ // For primed listeners, the `fire` object also provide a `wakeup` method
+ // which can be used by the primed listener to explicitly `wakeup` the
+ // background context (Event Page or Background Service Worker) and wait for
+ // it to be running (by awaiting on the Promise returned by wakeup to be
+ // resolved).
+ // @param {ProxyContextParent} [options.context=undefined]
+ // This property is expected to be undefined for primed listeners (which
+ // are created while the background extension context does not exist) and
+ // to be set to a ProxyContextParent instance (the same got by the getAPI
+ // method) when the method is called for a listener registered by a
+ // running extension context.
+ //
+ // @param {object} [apiEventsParams=undefined]
+ // The additional addListener parameter if any (some API events are allowing
+ // the extensions to pass some parameters along with the extension callback).
+ onMyEventName({ context, fire }, apiEventParams = undefined) {
+ const listener = (...) {
+ // Wake up the EventPage (or Background ServiceWorker).
+ if (fire.wakeup) {
+ await fire.wakeup();
+ }
+
+ fire.async(...);
+ }
+
+ // Subscribe a listener to an internal observer or event which will be notified
+ // when we need to call fire to either send the event to an extension context
+ // already running or wake up a suspended event page and accumulate the events
+ // to be fired once the extension context is running again and a callback registered
+ // back (which will be used to convert the primed listener created while
+ // the non persistent background extension context was not running yet)
+ ...
+ return {
+ unregister() {
+ // Unsubscribe a listener from an internal observer or event.
+ ...
+ }
+ convert(fireToExtensionCallback) {
+ // Convert gets called once the primed API event listener,
+ // created while the extension background context has been
+ // suspended, is being converted to a parent process API
+ // event listener callback that is responsible for forwarding the
+ // events to the child processes.
+ //
+ // The `fireToExtensionCallback` parameter is going to be the
+ // one that will emit the event to the extension callback (while
+ // the one got from the API event registrar method may be the one
+ // that is collecting the events to emit up until the background
+ // context got started up again).
+ fire = fireToExtensionCallback;
+ },
+ };
+ },
+ ...
+ };
+
+ getAPI(context) {
+ ...
+ return {
+ myAPIName: {
+ ...
+ onMyEventName: new EventManager({
+ context,
+ // NOTE: module is expected to be the API module name as listed in
+ // ext-toolkit.json / ext-browser.json / ext-android.json.
+ module: "myAPIName",
+ event: "onMyEventNAme",
+ extensionApi: this,
+ }),
+ },
+ };
+ }
+ };
+
+Testing Persisted API Event Listeners
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+- ``extension.terminateBackground()`` / ``extension.terminateBackground({ disableResetIdleForTest: true})``:
+
+ - The wrapper object returned by ``ExtensionTestUtils.loadExtension`` provides
+ a ``terminateBackground`` method which can be used to simulate an idle timeout,
+ by explicitly triggering the same idle timeout suspend logic handling the idle timeout.
+ - This method also accept an optional parameter, if set to ``{ disableResetIdleForTest: true}``
+ will forcefully suspend the background extension context and ignore all the
+ conditions that would reset the idle timeout due to some work still pending
+ (e.g. a ``NativeMessaging``'s ``Port`` still open, a ``StreamFilter`` instance
+ still active or a ``Promise`` from an API event listener call not yet resolved)
+
+- ``ExtensionTestUtils.testAssertions.assertPersistentListeners``:
+
+ - This test assertion helper can be used to more easily assert what should
+ be the persisted state of a given API event (e.g. assert it to not be
+ persisted, or to be persisted and/or primed)
+
+.. code-block:: js
+
+ assertPersistentListeners(extension, "browserAction", "onClicked", {
+ primed: false,
+ });
+ await extension.terminateBackground();
+ assertPersistentListeners(extension, "browserAction", "onClicked", {
+ primed: true,
+ });
+
+- ``extensions.background.idle.timeout`` preference determines how long to wait
+ (between API events being notified to the extension event page) before considering
+ the Event Page in the idle state and suspend it, in some xpcshell test this pref
+ may be set to 0 to reduce the amount of time the test will have to wait for the
+ Event Page to be suspended automatically
+
+- ``extension.eventPage.enabled`` pref is responsible for enabling/disabling
+ Event Page support for manifest_version 2 extension, technically it is
+ now set to ``true`` on all channels, but it would still be worth flipping it
+ to ``true`` explicitly in tests that are meant to cover Event Page behaviors
+ for manifest_version 2 test extension until the pref is completely removed
+ (mainly to make sure that if the pref would need to be flipped to false
+ for any reason, the tests will still be passing)
+
+Persisted Event listeners internals
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+The ``ExtensionAPIPersistent`` class provides a way to quickly introduce API event
+listener persistency to a new WebExtensions API, and reduce the number of code
+duplication, the following section provide some more details about what the
+abstractions are doing internally in practice.
+
+WebExtensions APIs classes that extend the ``ExtensionAPIPersistent`` base class
+are still able to support non persisted listeners along with persisted ones
+(e.g. events that are persisting the listeners registered from an event page are
+already not persisting listeners registered from other extension contexts)
+and can mix persisted and non-persisted events.
+
+As an example in ``toolkit/components/extensions/parent/ext-runtime.js``` the two
+events ``onSuspend`` and ``onSuspendCanceled`` are expected to be never persisted
+nor primed (even for an event page) and so their ``EventManager`` instances
+receive the following options:
+
+- a ``register`` callback (instead of the one part of ``PERSISTED_EVENTS``)
+- a ``name`` string property (instead of the two separate ``module`` and ``event``
+ string properties that are used for ``EventManager`` instances from persisted
+ ones
+- no ``extensionApi`` property (because that is only needed for events that are
+ expected to persist event page listeners)
+
+In practice ``ExtensionAPIPersistent`` extends the ``ExtensionAPI`` class to provide
+a generic ``primeListeners`` method, which is the method responsible for priming
+a persisted listener when the event page has been suspended or not started yet.
+
+The ``primeListener`` method is expected to return an object with an ``unregister``
+and ``convert`` method, while the ``register`` callback passed to the ``EventManager``
+constructor is expected to return the ``unregister`` method.
+
+.. code-block:: js
+
+ function somethingListener(fire, minValue) => {
+ const callback = value => {
+ if (value >= minValue) {
+ fire.async(value);
+ }
+ };
+ RegisterSomeInternalCallback(callback);
+ return {
+ unregister() {
+ UnregisterInternalCallback(callback);
+ },
+ convert(_fire, context) {
+ fire = _fire;
+ }
+ };
+ }
+
+ this.myapi = class extends ExtensionAPI {
+ primeListener(extension, event, fire, params, isInStartup) {
+ if (event == "onSomething") {
+ // Note that we return the object with unregister and convert here.
+ return somethingListener(fire, ...params);
+ }
+ // If an event other than onSomething was requested, we are not returning
+ // anything for it, thus it would not be persistable.
+ }
+ getAPI(context) {
+ return {
+ myapi: {
+ onSomething: new EventManager({
+ context,
+ module: "myapi",
+ event: "onSomething",
+ register: (fire, minValue) => {
+ // Note that we return unregister here.
+ return somethingListener(fire, minValue).unregister;
+ }
+ }).api()
+ }
+ }
+ }
+ }