javascript websockets - control initial connection/when does onOpen get bound
TL;DR - The standard states that the connection can be opened "while the [JS] event loop is running" (e.g. by the browser's C++ code), but that firing the open
event must be queued to the JS event loop, meaning any onOpen
callback registered in the same execution block as new WebSocket(...)
is guaranteed to be executed, even if the connection gets opened while the current execution block is still executing.
According to The WebSocket Interface specification in the HTML Standard (emphasis mine):
The
WebSocket(url, protocols)
constructor, when invoked, must run these steps:
- Let
urlRecord
be the result of applying the URL parser tourl
.- If
urlRecord
is failure, then throw a "SyntaxError
"DOMException
.- If
urlRecord
's scheme is not "ws
" or "wss
", then throw a "SyntaxError
"DOMException
.- If
urlRecord
's fragment is non-null, then throw a "SyntaxError
"DOMException
.- If
protocols
is a string, setprotocols
to a sequence consisting of just that string.- If any of the values in
protocols
occur more than once or otherwise fail to match the requirements for elements that comprise the value of Sec-WebSocket-Protocol fields as defined by The WebSocket protocol, then throw a "SyntaxError
"DOMException
.Run this step in parallel:
- Establish a WebSocket connection given urlRecord, protocols, and the entry settings object. [FETCH]
NOTE If the establish a WebSocket connection algorithm fails, it triggers the fail the WebSocket connection algorithm, which then invokes the close the WebSocket connection algorithm, which then establishes that the WebSocket connection is closed, which fires the close event as described below.
- Return a new WebSocket object whose url is urlRecord.
Note the establishment of the connection is run 'in parallel', and the specification further states that "...in parallel means those steps are to be run, one after another, at the same time as other logic in the standard (e.g., at the same time as the event loop). This standard does not define the precise mechanism by which this is achieved, be it time-sharing cooperative multitasking, fibers, threads, processes, using different hyperthreads, cores, CPUs, machines, etc."
Meaning that the connection can theoretically be opened before onOpen
registration, even if onOpen(...)
is the next statement after the constructor call.
However... the standard goes on to state under Feedback from the protocol:
When the WebSocket connection is established, the user agent must queue a task to run these steps:
- Change the
readyState
attribute's value toOPEN
(1).- Change the
extensions
attribute's value to the extensions in use, if it is not thenull
value. [WSP]- Change the
protocol
attribute's value to the subprotocol in use, if it is not thenull
value. [WSP]- Fire an event named
open
at theWebSocket
object.NOTE Since the algorithm above is queued as a task, there is no race condition between the WebSocket connection being established and the script setting up an event listener for the open event.
So in a browser or or library that adheres to the HTML Standard, a callback registered to WebSocket.onOpen(...)
is guaranteed to execute, if it is registered before the end of the execution block in which the constructor is called, and before any subsequent statement in the same block that releases the event loop (e.g. await
).
JavaScript is single threaded which means the network connection can't be established until the current scope of execution completes and the network execution gets a chance to run. The scope of execution could be the current function (the connect
function in the example below). So, you could miss the onopen
event if you bind to it very late on using a setTimeout e.g. in this example you can miss the event:
View: http://jsbin.com/ulihup/edit#javascript,html,live
Code:
var ws = null;
function connect() {
ws = new WebSocket('ws://ws.pusherapp.com:80/app/a42751cdeb5eb77a6889?client=js&version=1.10');
setTimeout(bindEvents, 1000);
setReadyState();
}
function bindEvents() {
ws.onopen = function() {
log('onopen called');
setReadyState();
};
}
function setReadyState() {
log('ws.readyState: ' + ws.readyState);
}
function log(msg) {
if(document.body) {
var text = document.createTextNode(msg);
document.body.appendChild(text);
}
}
connect();
If you run the example you may well see that the 'onopen called' log line is never output. This is because we missed the event.
However, if you keep the new WebSocket(...)
and the binding to the onopen
event in the same scope of execution then there's no chance you'll miss the event.
For more information on scope of execution
and how these are queued, scheduled and processed take a look at John Resig's post on Timers in JavaScript.