Importance of the Order of Calls to postMessage and addEventListener

Things are strange in the world of JavaScript. I wanted to take a few minutes to document something that is, well, interesting.

In the living version of the specification of HTML from the WHATWG, there is an API known as Cross-document messaging. This allows two documents, from the same or different domains, to send messages to one another. The API allows, for example, two documents embedded alongside one another in iframes to exchange messages to coordinate behavior.

Although this is not how it is defined in the specification, consider that a message logically has a sender and zero or more receivers. In the design of a message-passing framework, there are (at least) two different ways to deliver messages:

  1. deliver the message to existing registered receivers and delete the message or
  2. deliver the message to all registered receivers and store that message for delivery to receivers that register in the future.

Obviously, 1 makes the most sense. To implement delivery method 2 would mean that a single sender transmitting a high rate of messages could cripple the delivery system by forcing it to store every sent message for potential future delivery.

The Cross-document message API implements 1. Therefore, it would make sense that if a receiver r wanted to hear a message sent by sender s, r must be registered before s sends a message. The HTML spec says that s calls postMessage() to send a message and r calls addEventHandler() to become a receiver.

However, that logical ordering is not always required to use the Cross-document messaging API! Don’t believe me? Go to the executable version of the code below and click the sendMessageAndSetHandler button! You’ll find that, even though the listener registers after the message is sent, it still receives the sender’s transmission!

<html>
<head>
<script>
function sendMessage() {
  window.postMessage("I can hear you!");
}
function setMessageHandler() {
  function messageHandler(event) {
    alert("message: " + event.data);
  }
  window.addEventListener("message", messageHandler);
}
function sendMessageAndSetHandler() {
  sendMessage();
  setMessageHandler();
}
</script>
</head>
<body>
<button onclick="sendMessageAndSetHandler();">sendMessageAndSetHandler</button><br>
</body>
</html>

How is this possible?

The listener registration takes place immediately but, as the specification says, postMessage() only queues a task to send the message and does not actually send it. The task added to the queue by the sender to deliver the message is only actually executed when it is at the top of the so-called posted message task source queue and that queue is serviced by the browser. The posted message task source queue is serviced during the execution of the event loop and (modulo my understanding of very intricate JS behavior) the execution of a JavaScript function will never be interrupted to service the event loop.

In other words, between the call to sendMessage() and setMessageHandler() in the example above, the browser does not actually transmit the message. Therefore, by the time the message is actually sent, the receiver will be listening!

Makes perfect sense, right?

Leave a Reply

Your email address will not be published. Required fields are marked *