diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
commit | 2aa4a82499d4becd2284cdb482213d541b8804dd (patch) | |
tree | b80bf8bf13c3766139fbacc530efd0dd9d54394c /toolkit/components/extensions/docs/events.rst | |
parent | Initial commit. (diff) | |
download | firefox-upstream.tar.xz firefox-upstream.zip |
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r-- | toolkit/components/extensions/docs/events.rst | 314 |
1 files changed, 314 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..8fd01976cd --- /dev/null +++ b/toolkit/components/extensions/docs/events.rst @@ -0,0 +1,314 @@ +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, + name: "myapi.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, + name: "myapi.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. + + |