Author: Fergal Daly Status: Draft
This is a proposal to add a close
event
which fires when a MessagePort
's entangled port is closed
or becomes unreferenced
(e.g. it was owned by a process that has crashed).
There is no timely and reliable signal for when
a MessagePort
has become unusable.
This makes it difficult to free resources
associated with ports.
The best that can be achieved is
timeliness in many cases
by reacting to lifecycle state changes
and reliability in the remaining cases
by depending on GC via WeakRef
s.
By using all of these, a comprehensive solution is actually possible but it is unergonomic and it does not seem to be well known.
This issue has a long discussion of the problem and potential solutions.
The MessageChannel
API provides access to a pair of "entangled" ports.
MessagePort
s.
A MessagePort
can send/receive messages with its entangled port.
They can be passed to other frames,
including cross-origin frames.
Either of both ports in an entangled pair
can end up owned by documents that have no knowledge of each-other.
It may be that one holder of a MessagePort
is holding resources
that are associated with it.
These resources should be freed
when the port is no longer useful.
There is no explicit signal available
as to when that has happened.
The spec includes this advice
Broadcasting to many ports is in principle relatively simple: keep an array of
MessagePort
objects to send messages to, and iterate through the array to send a message. However, this has one rather unfortunate effect: it prevents the ports from being garbage collected, even if the other side has gone away. To avoid this problem, implement a simple protocol whereby the other side acknowledges it still exists. If it doesn't do so after a certain amount of time, assume it's gone, close the `MessagePort`` object, and let it be garbage collected.
Waiting for some time and assuming that the other side is gone may be OK for some uses but there are other uses where a port may have a long lifetime or a lifetime/usage that is dependent on user actions. Freeing resources prematurely would be a problem there. So a timeout based approach does not work for the general case.
If the MessagePort
is held only in a WeakRef
,
when the other side goes away,
drops the reference,
calls close()
or otherwise causes the connection between the ports to end,
the local MessagePort
object becomes eligible for garbage collection (GC).
So, periodically checking the WeakRef
and
freeing resources only if it deref()
s to null
will avoid resource issues
without ever prematurely freeing resources.
Relying entirely on GC
may result in an arbitrarily long delay.
Adding some communication of lifecycle state can help.
Having the other side communicate its lifecycle state,
e.g. by sending a message when the document is being destroyed
or navigated away from,
would allow earlier cleanup.
This could be done in a pagehide
event handler
when the event's persisted
field is false
.
The cases where the other side
- crashes
- enters BFCache and is destroyed without being restored
- fails to communicate are covered by GC.
In the issue there are 2 main concerns expressed about any solutions.
By passing a MessagePort
to a cross-origin document
and listening to the close
event,
it is possible to find out when that document has been destroyed.
This can be done with without the cooperation of the document in question.
This information is already available
through the use of WeakRef
s.
The concern here can only be about the timing
of this information.
There would not be any new capability granted
by allowing delivering this event eventually.
Right now a page that wanted this information
as close as possible to the destruction event
could poll the WeakRef
frequently
while allocating engaging in activity
that triggers GC.
It's unclear that there would be any now capability granted by delivering this event promptly if the observer is willing to behave in a way that triggers GC.
Question: Is there a specific problem or attack that is enabled by delivering the event in a timely manner?
Since this event would need to fire in reaction to a port becoming GCed, the timing of the event reveals the occurrence of GC.
Question: Why is this a concern?
Polling WeakRef
s already seems to give this ability.
This would potentially give it cross-origin
but that would only be the case
if the port was closed by becoming unreferenced.
The listener cannot know whether the port was
explicitly closed,
last reference was dropped
or if the owning document has been destroyed.
Is it simply that exposing GC
is something to be avoided when possible?
Add a close
event that fires
when an entangled MessagePort
is closed.
Given a pair of entangled ports, portA
and portB
,
if portA
is closed,
the event is fired on portB
.
This can be done in such a way that it exposes no information that isn't already exposed but provides much better ergonomics and gives us an opportunity to explicitly choose better timing.
There are several different scenarios to cover. In each case, the fact that the port has been closed will inevitably become known to the holder of the entangled port if they implement the strategies above. So all that is really to be decided is the timing of the event firing.
Implementation-wise it seems simplest
to fire the close
event on a port
when that port stops being "entangled" with the other port
(as specified here)
The implications are discussed for each case.
Firing the close
event as soon as possible
after the entangled port's close()
method is called seems appropriate.
This is an explicit action taken by holder of the object
and it reveals nothing about GC.
In this case, from an implementation point of view, it is possible to fire the event on entangled port immediately (subject to inter-process delays).
If the document is destroyed due to something other than a crash,
e.g. navigation or iframe deletion,
this may result in the MessagePort
object becoming unreferenced
and then GCed some time in the future (see below)
or if this was the only document
in the JS execution context it may result in prompt GC.
See the next section.
At the point that the port is GCed,
we can fire the close
event on the entangled port.
The choices above do not always avoid the concerns listed above however it does not seem to be possible to avoid them fully. In particular, it seems impossible to avoid exposing the timing of GC or navigation in some cases. Given that it is already effectively exposed, this accepts that and provides an ergonomic solution.
If we only send the event
on an explicit close()
or when the document owning the object is destroyed
then we avoid exposing GC timing.
If we do this without also changing the entanglement timing
then WeakRef
can still be used to observe GC timing.
Changing when entanglement ends is doable
but currently it ends as soon as possible in all cases.
This would add some complication.
It would also indefinitely extend to the entangled lifetime of some MessagePorts
(those that currently end via GC).
To avoid exposing the information
about navigations occurring in cross-origin frames,
the close
event would not be sent
unless the owning document took some explicit action
after receiving the MessagePort
via postMessage
.
Suggested here
Given that WeakRef
s inevitably expose
the disentanglement of MessagePort
s already,
it does not seem useful to try to hide that.