summaryrefslogtreecommitdiffstats
path: root/toolkit/components/extensions/docs/events.rst
blob: d494155ffc0bca4b8d063499e792487beb27e46c (plain)
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
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
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()
         }
       }
     }
   }