Skip to content

Commit ddbc0f3

Browse files
committed
Address several comments
1 parent 91c3a3e commit ddbc0f3

1 file changed

Lines changed: 80 additions & 19 deletions

File tree

proposals/async-listener-registration.md

Lines changed: 80 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,14 @@ This proposal introduces a mechanism to explicitly delay the finalization of the
3636

3737
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.
3838

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+
3947
## Specification
4048

4149
### Schema
@@ -57,9 +65,9 @@ Extensions which require dynamic or conditionally registered event listeners bas
5765
namespace runtime {
5866
/**
5967
* 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.
6371
*
6472
* This method is only available to the background script context.
6573
*/
@@ -69,18 +77,27 @@ namespace runtime {
6977

7078
### Behavior
7179

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.
7393

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.
8299

83-
#### Example Flow
100+
##### Example Flow
84101

85102
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.
86103

@@ -117,17 +134,53 @@ If the storage promise resolves but `settings.shouldListen` evaluates to `false`
117134
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.
118135
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).
119136
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+
async function setShouldListen(shouldListen) {
155+
await browser.storage.local.set({ shouldListen });
156+
157+
if (shouldListen) {
158+
if (!browser.tabs.onUpdated.hasListener(handleTabUpdate)) {
159+
browser.tabs.onUpdated.addListener(handleTabUpdate);
160+
}
161+
} else {
162+
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+
120175
#### Edge Cases
121176
122177
* **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.
127178
128179
#### Performance Considerations
129180
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.
131184
132185
### New Permissions
133186
@@ -238,4 +291,12 @@ Service workers on the open web handle initialization via the `install` and `act
238291
239292
## Future Work
240293
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:
297+
298+
```javascript
299+
browser.tabs.onUpdated.addListener(handleTemporaryUpdate, { ...filter, persistent: false });
300+
```
301+
302+
This would address temporary listener use cases without changing the default behavior of late registrations in this proposal.

0 commit comments

Comments
 (0)