Open Jamesernator opened 3 years ago
isTrusted
a legacy feature, but yeah, I think there is some value in it for cases like this. I think the correct approach here would be to add a feature that event listeners can filter for this, not to forbid dispatchEvent()
. The latter seems too drastic and quite a departure from the existing model, whereas the former fits in quite naturally.As for new EventTarget()
, perhaps https://blog.domenic.me/the-revealing-constructor-pattern/ would be a way to expose a dispatchEvent()
that can dispatch trusted Event
instances. Although really you want the Event
instance itself to be created or blessed in such a way (so ev.isTrusted
is correct at all times), so this might need some more thought.
863 considered
isTrusted
a legacy feature, but yeah, I think there is some value in it for cases like this. I think the correct approach here would be to add a feature that event listeners can filter for this, not to forbiddispatchEvent()
. The latter seems too drastic and quite a departure from the existing model, whereas the former fits in quite naturally.
Yeah thinking about I agree it would be simpler for it just to be an option, maybe something like eventTarget.addEventListener("some-event", listener, { trustedOnly: true })
.
Something I'm torn on is whether or not event targets should allow configuring the default value of this option for certain events, in particular because the naive usage of abortSignal.addEventListner("abort", memoryLeakingListener)
. I feel like the majority of people would appreciate having the naive version automatically clean up the listener after abort happens, people who want non-trusted events on AbortSignal
could just pass { trustedOnly: false }
explictly.
As for
new EventTarget()
, perhaps https://blog.domenic.me/the-revealing-constructor-pattern/ would be a way to expose adispatchEvent()
that can dispatch trustedEvent
instances. Although really you want theEvent
instance itself to be created or blessed in such a way (soev.isTrusted
is correct at all times), so this might need some more thought.
Yeah, the tricky thing here is that events can be subclassed so even if there was something like:
const target = new EventTarget({
start(controller) {
controller.dispatchTrustedEvent({ name: "some-event" });
},
});
there would no way to create instances of arbitrary classes. Now we could certainly have something like:
class MyEvent extends Event {
constructor() {
super("my-event");
}
myEventMethod() {
// Frobulate the foobar
}
}
const target = new EventTarget({
start(controller) {
const event = new MyEvent();
controller.markTrusted(event);
controller.dispatchEvent(event);
},
});
however this would beg some questions, like what would happen if we dispatched the event on a different event listener? Throw an error? clear the trusted mark? Only allow dispatchEvent on the same controller to be used?
I feel like whoever is creating the events won't really care that .isTrusted
would be initially false but set to true during dispatch simply because in the majority of cases the event is created immediately before dispatch anyway. Even if they are going to something with the event object before dispatching (calling methods or whatever), it can just be assumed that such actions are trusted as whoever called the constructor has full control of the event object anyway (i.e. if a method is called on the event, and the method wants to check .isTrusted
, instead it can consider event.eventPhase === Event.NONE
as trustworthy as well).
The userland part of the idea still clearly needs more thought, so I don't know if it would be worth specifying { trustedOnly }
by itself to allow abortSignal
(and probably some other specs eventually) to use this, and revisit userland trusted events as a follow-on proposal.
Another change that's needed for reasonable userland AbortSignal
implementations is that all event listeners get run, regardless of stopImmediatePropagation()
invocation.
@rniwa @smaug---- @mfreed7 @domenic do you think it's reasonable if we created an event dispatch feature that allows for disabling stopImmediatePropagation()
(and perhaps stopPropagation()
at the same time)?
(Maybe I should split this into a separate issue as this seems like a requirement, whereas filtering for isTrusted
is more of a nice-to-have.)
Why would we disable stopImmediatePropagation or stopPropagation. I don't see any strong reasons for that.
About listeners only for trusted events, Gecko has had wantsTrusted option / 4th param for privileged JS for ages https://searchfox.org/mozilla-central/rev/85b4f7363292b272eb9b606e00de2c37a6be73f0/dom/webidl/EventTarget.webidl#21,26,42
@smaug---- the idea is that you can hand out a signal to someone safely without them being able to stop the signal from doing something. However, if you can prevent further listeners from running you have a problem in this scenario:
giveSignalToSomeone(mysignal);
userlandFetch(..., { signal: mysignal });
giveSignalToSomeone()
could prevent userlandFetch()
from seeing the signal was aborted by using stopImmediatePropagation()
.
(Maybe I should split this into a separate issue as this seems like a requirement
This would make sense, trustedOnly
is useful for other events as well not just abort related events.
I will change the title of this issue to reflect the scope of what I was suggesting.
Also I know there are discussions in AbortSignal.any
about weak listeners, as mentioned in one of my previous comments, if trustedOnly
were specified one could make such event listeners weak on the associated AbortController
(s) (as one can't make isTrusted: true
events without access to the controller).
giveSignalToSomeone()
could preventuserlandFetch()
from seeing the signal was aborted by usingstopImmediatePropagation()
.
...if using event, and that is the whole point of stop*propagation. The same applies to all the event targets. And if one wants to follow signals, any() should help here, since it can be used to basically clone the signal object.
Part of @smaug----'s comment above was on-topic, repeating that here:
About listeners only for trusted events, Gecko has had wantsTrusted option / 4th param for privileged JS for ages https://searchfox.org/mozilla-central/rev/85b4f7363292b272eb9b606e00de2c37a6be73f0/dom/webidl/EventTarget.webidl#21,26,42
See my second comment for modified suggestion.
As a note, this issue is primarily targeted atAbortSignal
's"abort"
event, however the infrastructure could be useful to many event types where listeners after a certain point are probably a mistake.Anyway, currently with
AbortSignal
we currently have a problem whereAbortSignal
s may live a long time (possibly very long times if they're forwarded from program level). This means if we pass anAbortSignal
around a program, it is very easy to leak memory accidentally by using.addEventListener("abort", () => doSomethingOnAbort())
. Similarly if an event handler is subscribed late, it will never be collected (due to the existence of.dispatchEvent
)A related issue is also due to the existence of
.dispatchEvent
, and that's that even if we add a"abort"
handler, if we don't write awkward boilerplate to check if the event.isTrusted
, then there is no guarantee about consistency with.aborted
or with behaviour of web APIs, i.e.:I'd like to propose a solution that solves both of these issues at once, and that is the idea I'm referring to as "robust events". The basic idea of "robust events" is essentially that certain events are marked so that
.dispatchEvent
is not allowed.Essentially the idea here is that some APIs (in particular
AbortSignal
"abort"
) would declare their events as robust, what this entails is that only events which are trusted can be dispatched on the event target, further the API can declare when a certain type of event may no longer be fired and as such mark all listeners as garbage collectable.With this idea effectively we get this:
Note that this might be useful outside of
AbortSignal
, there's actually a number of events where only real (.isTrusted
) events are generally of interest. For exampleaudioNode.onended
events,animation.onfinished
. It's also worth noting that this isn't just useful for "one-time" events, but rather any events which never occur again after a certain point (i.e. maybe websocket messages, or similar).Some places where this idea is not a good idea would be all DOM events, DOM events are often dispatched explicitly in order to transform events from one kind to another so if you have a handler you probably do want fake events in the majority of cases.
Some open questions remain with this idea though:
"abort"
to disallow.dispatchEvent
web-compatible?.addEventListener
, i.e..addEventListener("abort", callback, { robust: true })
, whenrobust: true
is set then only trusted events are considered, and robust callbacks can be collected.dispatchEvent
would never throw, but in general whether or not.addEventListener
observes those events would be based on some set default, i.e. forAbortSignal
we'd declare that"abort"
handlers are robust by default, so they would both ignore.dispatchEvent
events, and be released for garbage collection on the real eventShould we allow userland
EventTarget
instances the ability to have robust events?If so we'd do something like
~