1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
|
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 Persistence
-----------------
Events are persisted in some circumstances. Persisted events can either
block startup, and/or cause an event page or 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.
To make a persistent listener, the ExtensionAPI class in the module must also
provide a ``primeListeners`` method. The ``module`` and ``event`` params are
required for the ``EventManager`` constructor.
This requires structuring the listener registration code in a way that it can
be used by both the ``primeListener`` call and in the constructor for ``EventManager``.
``primeListener`` must 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()
}
}
}
}
|