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 `_ 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() } } } }