Make popup blocking continue working when service workers are suspended
Background
One of the major challenges introduced by MV3 is that service workers (unlike MV2 background pages) may be suspended and restarted at any time. Our existing codebase has all been written assuming we have a long living background page, and so we can do things like hold information that's relevant between events in local variables, or register events asynchronously.
In an MV3 context, our assumptions about how our code will be run are broken. Our extension will break in all sorts of weird ways. We need to identify what those ways are and fix them.
To simplify the process of keeping our code working, making these changes has been broken down by functional area. In this issue, the popup blocking module should be made to continue working even if the MV3 service worker is suspended.
In https://gitlab.com/eyeo/adblockplus/abc/webext-sdk/-/merge_requests/404, we added a broad method of testing our normal functionality when the service worker is suspended frequently. This is currently skipped for the popup blocking tests, so as part of this issue those tests should be enabled and they should pass.
Use case
Popup blocking continues to work in MV3, even though in MV3 the browser may suspend the service worker at any time.
Acceptance Criteria
- Popup blocking continues to work as expected when the service worker is suspended. Popups are reliably blocked.
- All event listeners that can wake up the service worker must be attached in the first turn of the event loop.
- Events that we emit must be able to have listeners attached in the first turn of the event loop.
- Functionality that currently requires state shared between events must keep working, for example by using a storage API for the state.
- Code is adequately tested to ensure that this is the case, and to assist in ensuring that future changes do not introduce regressions.
Proposed Implementation
- Enable the existing fuzz tests for popup blocking
- When these are enabled, they mostly fail so this is a good starting point.
- First check if it's possible to implement popup blocking using DNR
- Popup blocking listens to a few events:
- browser.webNavigation.onCreatedNavigationTarget is used to identify popups which may need to be blocked. Popups don't always have their URL at this point.
- browser.webRequest.onBeforeRequest and browser.webNavigation.onBeforeNavigate are used to identify when the URL of a popup is changed, and so block it.
- browser.webNavigation.onCompleted and browser.tabs.onRemoved are used to clear up our in memory state when a popup has finished definitely doesn't need to be blocked.
- All of these events need to be registered in the first turn of the event loop. My proposal here is to register them in
popup-blocker.js
'sstart
function, and update the wayinitializer.start
initializes async modules to call them all in the first turn of the event loop, and then await all of their promises at the end. This might need to specially handle modules that depend on each other, like core depending on io.
- These events share data using a Map called
loadingPopups
. That will need to usebrowser.storage.session
in MV3 browsers. Sincebrowser.storage.session
isn't available in MV2, this will be still be a Map in MV2.- Annoyingly this will make all of our event handlers async since it will make all uses of
loadingPopups
async. I don't that will be a problem in this case.
- Annoyingly this will make all of our event handlers async since it will make all uses of
- The events here generally use the filter engine to check for filters, and so will need to ensure the filter engine is initialized before use.
- This might be a problem because it causes a circular dependency between
initializer
andpopup-blocker
, so the way that we ensure the filter engine is initialized might need to be rethought in this case.
- This might be a problem because it causes a circular dependency between
- It looks like there are several MV2 vs MV3 splits here, so the proposed solution is to fork the popup blocker into an MV2 version (unchanged) and an MV3 version (with all the changes listed above). When the initializer is starting the system, it chooses the appropriate one to start.
Integrator Notes
This introduces the use of browser.storage.session
for MV3 extensions. This means that in MV3, the minimum supported version of Chrome is 102 for MV3 extensions.