-
Notifications
You must be signed in to change notification settings - Fork 56
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Proposal: Toggleable event listeners #501
Comments
We had discussed the "chattiness" of some APIs a few times already, and so far did not come to a consensus. This specific use case, however, does have a decent workaround: the background context listens only for Overall:
|
That workaround seems to only be relevant if the chatty events are storage changes. I'm more interested in a declarative way of adding listeners so they can be disabled or enabled dynamically. #475 This proposal seems to fix that issue you're referencing. |
AFAICT there's no need to change addListener's signature because it already accepts an object in the second parameter for various options, so it's probably easier to add a new property there e.g. |
I'm having some difficulty understanding this suggestion. Would it allow for enabling/disabling a listener based on the user enabling/disabling a feature?
Only certain events (webRequests.on*, windows.on*, webNavigation.on*) seem to accept a second parameter and they all expect a different type of filter. I think documentation and implementation will be much simpler if the first parameter was used. Proposed shape of add listener. interface AddListenerInit<H> {
callback: H,
group?: string
}
Event.addListener(callback: H | AddListenerInit<H>) |
Yes, of course you can call chrome.foo.onBar.addListener(fn, {wakeUp: true}) at any time to register a listener that wakes the background script, and removeListener to unregister it.
The second parameter already exists on events that allow modifying the listener's behavior, which is exactly what is being discussed, hence extending it to other events seems straightforward and intuitive. Conversely, splitting the behavioral options in two different parameters is counter-intuitive. Another solution might be to use |
I'm still having trouble understanding, which might be a sign that it's not very intuitive. In the background service worker, all events need to be registered synchronously. So the concept of adding the listener at any time isn't clicking with me.
Second parameter is for filters specific to that event. The first parameter would be an init that's applicable to all events. I personally think it's more suitable for the first parameter, but I would be ok with both options. |
Usually it's an indicator of a bias/preconception.
This requirement has been always terribly counter-intuitive due to the implicit magical behavior of having the listener being registered in the first turn of the event loop, it's also regularly misunderstood by developers as it's hard to describe (the documentation even incorrectly stated for 10+ years that the listeners must be declared at the top level of the script). I suggest an explicit method of indicating the intent, thus solving both the suggested case and the current implicit counter-intuitive magical voodoo system. |
Could you give a usage example, similar to my proposal's example? browser.webNavigation.onCommitted.addListener({
group: "someGroupName",
callback: onListener
})
// In options page
// if user enables or disables someFeature, you can enable or disable the intensive listeners.
browser.runtime.disableListenerGroup("someGroupName") |
Oh. I didn't account for your suggestion to control the behavior from another page, so my suggestion was assuming you send a message to the service worker, which will toggle listener registration accordingly, just like we do currently with the only difference of using an explicit To incorporate your suggestion, my idea may be modified to add
|
Let's say it's through a message, will it look like this? browser.onMessage.addListener(msg => {
if (msg.action === 'activateListener') {
browser.webNavigation.onCommitted.addListener(onListener, {wakeUp: true})
}
}) Event listeners need to be added each iteration of the service worker, but we're only adding it when we receive a message. Is the wakeUp parameter a way to indicate the you want to add that listener in perpetuity? If so, maybe If that's the case, it seems like a good proposal, but I think it would be difficult to implement. My suggestion is pretty easy all things considered. All the browser has to do is ensure a listener's group isn't disabled before dispatching to the listener. |
Indeed, wakeUp and late registration either won't work or won't be easy to implement. My revised suggestion ended up being a cosmetic alternative to yours: use |
Currently there is no way to register listeners dynamically in the background. Your workaround should work (but has a race condition). Anyway (no matter what the API looks like), to support dynamic listeners in the background, the current implementation logic must be modified (a lot). At present, the browser doesn't remember what listeners to trigger, it fires events to all listeners that are registered at the first event loop when the service worker wakes up. |
The dynamically registered listeners idea was my wrong initial suggestion, which I finally corrected. The actual idea is that all listeners are registered synchronously with an id specified via Hence, there should be no race conditions because a) the browser won't wake up the background script for a disabled id, b) it won't send the event to an already running script, c) the list of enabled ids will be known at the start of the script as it'll be passed in the internal message that contains the data for the event that woke the script. Of course, it's possible to introduce raciness, as the API to disable an id is asynchronous, but it's not specific to this case. BTW, it means we should add a way to re-enable the id/group e.g. chrome.runtime.enableListenerGroup or chrome.webNavigation.onCommitted.enableListenerId. |
I added an alternative proposal so the interface accepts key for local storage. If specified, the browser will ensure that key is set to true before dispatching to the listener. Update: I ended up removing it because there's no precedence for that kind of thing. |
I don’t think this can be dependent on random storage keys and their format. What storage area should this be read from? Local? Sync? Managed? They should probably have their own grouping/namespace instead: browser.webNavigation.onCommitted.addListener({
group: 'my-events',
callback: onListener
});
browser.webNavigation.onCommitted.toggle('my-events', true) And then you can add your own storage.onChanged listener if you want to link it to a specific group: browser.storage.local.onChanged.addListener(changes => {
if (changes.someFeature) {
browser.webNavigation.onCommitted.toggle('my-events', changes.someFeature.newValue)
}
}) This probably isn't as clean as you hoped though. |
If there's only one, local 100%. Sync is complicated. Session doesn't allow you set to set a default value and requires initialization. You could also have an option for both session and local. interface AddListenerInit<H> {
callback: H,
requiresLocalFlag?: string
requiresSessionFlag?: string
}
Event.addListener(callback: H | AddListenerInit<H>)
I agree, but I think the alternative proposal might be simpler to implement. I will include both. Update: Ended up removing it. |
Options are often in the sync storage, so if I want to make this dependent on an option, it's impossible. Also it's impossible when the options are stored as a single complex object. What you're asking is to overload I like your |
At present, an extension
For dynamically registered events for service worker, I proposal these new methods instead of changing existing methods:
For example:
"FunctionName" is a global function name that declared in service worker, rather than a function object, and doesn't need to be registered in the first event loop when service worker wakes up. This allows the browser to remember both the event and this specific function independently of service worker's lifecycle, not just remember the event like addListener() does. These new methods can be called in both service-worker context and non-service-worker context dynamically, but only trigger events in service worker context, not in other contexts. After calling I'd love to hear from the browser implementation perspective, such as whether it's possible or what problems it will encounter. |
@hanguokai That sounds like an entirely new proposal |
@hanguokai Interesting approach, but there might be a few issues involving bundlers (webpack, vite, etc). You can work around these, but it might stump some people.
|
@fregante @newRoland I am not a browser engineer. So I also raised this issue to the Service Worker community yesterday, asking if they think it can be implemented w3c/ServiceWorker#1698 . If the function can be implemented, it will probably take several years before we can use it. |
Wow, that long? Service worker proposals probably take much longer than a purely web extension proposal, so I want to clarify that my proposal doesn't require changing the service worker spec. Terminology My proposal is to register an event listener with a group name. The group name is like a flag. The Event Dispatcher will check if that group name is enabled before dispatching to that listener. This proposal applies to event.addListener in all contexts (content script, options page, background ,etc). |
The time I'm talking about is for adding a new extension behavior or API like this, especially the proposals coming from developers rather than browser vendors. I also don't want to change Web service worker standard, just asking for advice there.
Thanks for the explanation. So your proposal still needs to add event listeners in the first event loop in service worker, not for dynamically adding event listeners. What is the default state (enable or disable) when addListener() with a group name if developer doesn't set the state before? |
🤔 Haven't really thought about it, but off by default seems like a better choice. On a related note, I'm souring on my naming choices. Group sounds too generic so I'll switch to requiresFlag. browser.webNavigation.onCommitted.addListener({
requiresFlag: "someFlag",
callback: onListener
})
// In options page, enable/disable on demand.
browser.runtime.setListenerFlag("someFlag", true) |
There's no good way to make event listeners optional for the background (service worker or event page).
An extension might have an optional feature that requires listening to chatty events. Always having these event listeners active is not ideal, especially when not all users enable that feature. Listening to chatty events impact performance by waking the background frequently or deprive it from rest. Currently, there's no standard method to make event listeners optional. The usual approach of attaching and detaching them is impractical for the background page.
The current workaround is a hacky process that needs to be done every time the background loads.
Proposal
If requiresFlag was specified, the browser will ensure that flag is true before dispatching to the listener.
The text was updated successfully, but these errors were encountered: