You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: proposals/async-listener-registration.md
+80-19Lines changed: 80 additions & 19 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -36,6 +36,14 @@ This proposal introduces a mechanism to explicitly delay the finalization of the
36
36
37
37
Extensions which require dynamic or conditionally registered event listeners based on asynchronous state, without resorting to unconditionally waking up the background context for events they may not care about.
38
38
39
+
### Non-goals / Out of Scope
40
+
41
+
This proposal is scoped to extension API event listener registration in an extension background context.
42
+
43
+
* It does not change the lifecycle of standard service worker events (such as `install`, `activate`, or `fetch`).
44
+
* It does not affect web platform API events or non-event extension APIs.
45
+
* It does not affect behavior in non-background extension contexts, such as popups, side panels, content scripts, etc.
46
+
39
47
## Specification
40
48
41
49
### Schema
@@ -57,9 +65,9 @@ Extensions which require dynamic or conditionally registered event listeners bas
57
65
namespaceruntime {
58
66
/**
59
67
* Signals to the browser that asynchronous initialization is complete.
60
-
* Transitions the background context out of the initialization phase, replaces
61
-
* the previous run's persisted listeners with the newly registered ones,
62
-
* and flushes the event queue.
68
+
* Transitions the background context out of the listener registration phase,
69
+
* replaces the previous run's persisted listeners with the newly registered
70
+
* ones, and flushes the event queue.
63
71
*
64
72
* This method is only available to the background script context.
65
73
*/
@@ -69,18 +77,27 @@ namespace runtime {
69
77
70
78
### Behavior
71
79
72
-
#### Initialization Lifecycle
80
+
#### Listener Registration Lifecycle
81
+
82
+
When an extension opts into `"async_listener_registration": true`, the browser enters a listener registration phase each time the background context starts.
83
+
84
+
During this phase:
85
+
86
+
1. The browser routes extension API events to the initializing background context if the event matches either:
87
+
* the previous persisted routing state; or
88
+
* any new listeners being registered during the current run.
89
+
90
+
2. Routed events are queued instead of dispatched to JavaScript callbacks until listener registration is completed.
91
+
92
+
3. The extension registers all listeners that should participate in the next persisted routing state.
73
93
74
-
***Manifest opt-in:** Extensions declare `"async_listener_registration": true` nested inside the `"background"` key in their `manifest.json`.
75
-
***Renderer-side queueing:** During the initialization phase, the browser routes incoming events that match the previously registered listeners to the background context's renderer process, which queues them instead of immediately attempting to dispatch them to JavaScript callbacks (which might not be attached yet).
76
-
***Event routing (union of old and new):** While in the initialization phase, the browser will route an event to the renderer queue if it matches *either* the old persisted routing state (from the previous run) or any new listeners currently being registered by the executing script.
77
-
***Listener registration:** Developers perform their potentially asynchronous setup (e.g., reading from storage) and call `addListener()` to attach their desired JavaScript callbacks *before* calling the completion API.
78
-
***State commit & completion API:** The extension must call `browser.runtime.markListenerRegistrationComplete()` once its potentially async setup is complete. At that moment:
79
-
1. The old persisted routing state is **thrown away**.
80
-
2. The new listeners currently registered are committed to the browser as the new persisted routing state for future wake-ups.
81
-
***Event flush:** Once `markListenerRegistrationComplete()` ends the initialization phase, the renderer flushes its queue, dispatching all held events to the newly registered JavaScript callbacks in FIFO order.
94
+
4. The extension calls `browser.runtime.markListenerRegistrationComplete();`. This atomically completes the listener registration phase:
95
+
* The previous persisted routing state is replaced with the current listener set.
96
+
* The background context leaves the listener registration phase.
97
+
* Queued events are flushed in FIFO order and dispatched against the current listener set.
98
+
* Queued events that no longer match any registered listener are discarded.
82
99
83
-
#### Example Flow
100
+
#####Example Flow
84
101
85
102
To illustrate how these lifecycle changes resolve the timing issues of asynchronous setup, consider an extension that conditionally tracks tab updates based on an asynchronous storage read.
86
103
@@ -117,17 +134,53 @@ If the storage promise resolves but `settings.shouldListen` evaluates to `false`
117
134
3. The browser commits the routing state. Because the listener was omitted during this run, the browser overwrites and **clears** the old persisted routing state. The renderer flushes the queued event, which is harmlessly dropped since no JavaScript callback is attached.
118
135
4. Because the routing state is now cleared, future `tabs.onUpdated` events will no longer wake up the background context. This implicitly cleans up the routing state without the developer needing to explicitly call `removeListener()` (see Workaround B) or accept unnecessary wake-ups on future events (see Workaround A).
119
136
137
+
#### Late Registrations
138
+
139
+
Once `markListenerRegistrationComplete()` is called, event listener registration resumes its normal behavior for the running background context. By default, listeners added after completion also update the browser's persisted routing state so that the extension can be woken up for those events in the next run.
140
+
141
+
A listener registered after `markListenerRegistrationComplete()` is a "late listener registration". Late listener registrations are persisted by default. This is intentional: it allows extensions to change runtime configuration after startup. For example, if a feature is enabled after listener registration has already completed, the extension can register the newly needed listener immediately and rely on that listener to wake the background context in the next run.
142
+
143
+
On the next background context startup, however, `markListenerRegistrationComplete()` again performs a full replacement of the persisted routing state. Therefore, a late listener from a previous run remains persisted only if the extension re-registers it before the next successful call to `markListenerRegistrationComplete()`.
144
+
145
+
If a late listener was accidentally persisted and is not re-registered during the next successful listener registration phase, it is removed from the persisted routing state at the next completion point. It may still cause an unnecessary wake-up before then.
146
+
147
+
Listeners intended to be temporary or scoped only to the current background context run must be removed explicitly with `removeListener()`. This proposal does not introduce non-persistent / session-scoped listeners; that is listed as future work.
148
+
149
+
##### Example Flow of a Late Registration
150
+
151
+
After listener registration has completed, an extension might change a setting at runtime:
152
+
153
+
```javascript
154
+
asyncfunctionsetShouldListen(shouldListen) {
155
+
awaitbrowser.storage.local.set({ shouldListen });
156
+
157
+
if (shouldListen) {
158
+
if (!browser.tabs.onUpdated.hasListener(handleTabUpdate)) {
if (browser.tabs.onUpdated.hasListener(handleTabUpdate)) {
163
+
browser.tabs.onUpdated.remove(handleTabUpdate);
164
+
}
165
+
}
166
+
}
167
+
```
168
+
169
+
If `shouldListen` changes from `false` to `true` after `markListenerRegistrationComplete()` has already been called, the `tabs.onUpdated` listener is a late listener registration. It updates the persisted routing state and can wake the extension on a future `tabs.onUpdated` event.
170
+
171
+
On the next startup, the extension is still expected to read `shouldListen` and re-register `handleTabUpdate` before calling `markListenerRegistrationComplete()`. If it does, the listener remains in the persisted routing state. Otherwise, the next successful completion replaces the state and removes the late listener.
172
+
173
+
If `shouldListen` changes from `true` to `false`, the extension should call `removeListener()` so future `tabs.onUpdated` events do not unnecessarily wake the background context.
174
+
120
175
#### Edge Cases
121
176
122
177
* **Initialization failure fallback:** If the initialization phase fails (e.g., the worker crashes or hits a timeout before the completion API is called), the browser will abort the state commit. It will keep the previous persisted routing state to prevent leaving the extension in a broken state.
123
-
* **Late registration:** It is permissible to register new listeners after the `markListenerRegistrationComplete()` API call. If an extension does so, developers must understand the following lifecycle:
124
-
* The browser will persist these "late" listeners across background context terminations to successfully trigger a future wake-up.
125
-
* On the subsequent wake-up, all relevant listeners must be re-registered. Calling `markListenerRegistrationComplete()` entirely replaces the routing state from the previous run.
126
-
* Therefore, any late-registered listeners from a previous run will be removed from the registered listeners during the next run's state commit unless the extension explicitly re-registers them before calling the completion API.
127
178
128
179
#### Performance Considerations
129
180
130
-
Because MV3 background contexts are ephemeral and wake up frequently, developers must be aware that delaying initialization adds latency to event dispatch. If an extension relies on slow I/O before calling `markListenerRegistrationComplete()`, that latency penalty applies to every single event that triggers a cold start. Developers are heavily encouraged to use fast, local I/O for conditional listener registration during routine wake-ups.
181
+
Because MV3 background contexts are ephemeral and may wake up frequently, delaying listener registration can add latency to event dispatch on every cold start. If an extension performs slow I/O before calling `markListenerRegistrationComplete()`, that delay applies to events queued during the listener registration phase. Extensions are encouraged to keep listener registration work short and to prefer fast, local state for routine startup decisions. Where possible, extensions should call `markListenerRegistrationComplete()` as soon as the listener set for the current run has been determined.
182
+
183
+
Delayed dispatch can also increase stale-state and time-of-check to time-of-use (TOCTOU) risks. Handlers for delayed events should revalidate assumptions before acting and should prefer stable identifiers where available.
131
184
132
185
### New Permissions
133
186
@@ -238,4 +291,12 @@ Service workers on the open web handle initialization via the `install` and `act
238
291
239
292
## Future Work
240
293
241
-
N/A
294
+
### Non-persistent or Session-scoped Listeners
295
+
296
+
A future proposal could allow an extension to register a listener that is active only for the current background context run and does not update persisted routing state. For example, this might be represented as an explicit listener option:
0 commit comments