57

I'm trying to implement a WebSocket with a fallback to polling. If the WebSocket connection succeeds, readyState becomes 1, but if it fails, readyState is 3, and I should begin polling.

I tried something like this:

var socket = new WebSocket(url);
socket.onmessage = onmsg;
while (socket.readyState == 0)
{
}
if (socket.readyState != 1)
{
    // fall back to polling
    setInterval(poll, interval);
}

I was expecting socket.readyState to update asynchronously, and allow me to read it immediately. However, when I run this, my browser freezes (I left it open for about half a minute before giving up).

I thought perhaps there was an onreadyStateChanged event, but I didn't see one in the MDN reference.

How should I be implementing this? Apparently an empty loop won't work, and there is no event for this.

Kendall Frey
  • 43,130
  • 20
  • 110
  • 148

10 Answers10

46

This is simple and it work perfectly... you can add condition about maximal time, or number of try to make it more robust...

function sendMessage(msg){
    // Wait until the state of the socket is not ready and send the message when it is...
    waitForSocketConnection(ws, function(){
        console.log("message sent!!!");
        ws.send(msg);
    });
}

// Make the function wait until the connection is made...
function waitForSocketConnection(socket, callback){
    setTimeout(
        function () {
            if (socket.readyState === 1) {
                console.log("Connection is made")
                if (callback != null){
                    callback();
                }
            } else {
                console.log("wait for connection...")
                waitForSocketConnection(socket, callback);
            }

        }, 5); // wait 5 milisecond for the connection...
}
Dmitry Taipov
  • 47
  • 1
  • 8
user3215378
  • 547
  • 4
  • 4
  • It might just be me missing something, but when I run your code I get an error telling me that "callback is not a function". Why is the callback as function in the `if(callback != null){ callback(); }` – Michael Tot Korsgaard Mar 10 '16 at 12:49
  • 4
    The appropriate solution is to use eventhandlers, per [Axel's answer](http://stackoverflow.com/a/13590558/752843). – Richard Apr 21 '17 at 17:06
  • What seems weird to me is the idea of websockets being a push technology, but having readyState as a syncronous, non-push call. – Ray Booysen May 16 '17 at 01:18
42

Here is a more elaborate explanation. First off, check the specific browser API, as not all browsers will be on the latest RFC. You can consult the

You don't want to run a loop to constantly check the readystate, it's extra overhead you don't need. A better approach is to understand all of the events relevant to a readystate change, and then wire them up appropriately. They are as follows:

onclose An event listener to be called when the WebSocket connection's readyState changes to CLOSED. The listener receives a CloseEvent named "close".

onerror An event listener to be called when an error occurs. This is a simple event named "error".

onmessage An event listener to be called when a message is received from the server. The listener receives a MessageEvent named "message".

onopen An event listener to be called when the WebSocket connection's readyState changes to OPEN; this indicates that the connection is ready to send and receive data. The event is a simple one with the name "open".

JS is entirely event driven, so you need to just wire up all of these events and check for the readystate, this way you can switch from WS to polling accordingly.

I recommend you look at the Mozilla reference, it's easier to read than the RFC document and it will give you a good overview of the API and how it works (link).

Don't forget to do a callback for a retry if you have a failure and poll until the callback for a successful reconnect is fired.

Richard
  • 56,349
  • 34
  • 180
  • 251
Axel
  • 867
  • 6
  • 9
  • 2
    "you can consult the"...? (I know it's old) – akauppi Aug 03 '16 at 07:49
  • 1
    Reading this, makes all sense, except all the `on` events do not expose closing and connecting. Only the onopen, onclose and error events. I don't want to be sending a message down a ws in the closing state. – Ray Booysen May 16 '17 at 01:18
  • which is also demonstrated here: https://www.pegaxchange.com/2018/03/23/websocket-client/ – Yordan Georgiev Jul 14 '19 at 19:05
  • It's disappointing that we can't rely on `open` event to know that it's up and running. Sometimes this event is not firing when the connection was already opened (often happens for me on page refresh). In other words, it's unreliable for anything but new connections. – Robo Robok May 23 '23 at 23:29
19

I am not using pooling at all. Instead, I use queuing. First I create new send function and a queue:

var msgs = []
function send (msg) {
  if (ws.readyState !== 1) {
    msgs.push(msg)
  } else {
    ws.send(msg)
  }
}

Then I need to read and send when the connection is first established:

function my_element_click () {
  if (ws == null){
    ws = new WebSocket(websocket_url)
    ws.onopen = function () {
      while (msgs.length > 0) {
        ws.send(msgs.pop())
      }
    }
    ws.onerror = function(error) {
      // do sth on error
    }
  } 
  msg = {type: 'mymessage', data: my_element.value}
  send(JSON.stringify(msg))
}

WebSocket connection in this example is created only on the first click. Usually, on second messages start to be sent directly.

Jan Topiński
  • 211
  • 2
  • 3
8

If you use async/await and you just want to wait until the connection is available I would suggest this function :

async connection (socket, timeout = 10000) {
  const isOpened = () => (socket.readyState === WebSocket.OPEN)

  if (socket.readyState !== WebSocket.CONNECTING) {
    return isOpened()
  }
  else {
    const intrasleep = 100
    const ttl = timeout / intrasleep // time to loop
    let loop = 0
    while (socket.readyState === WebSocket.CONNECTING && loop < ttl) {
      await new Promise(resolve => setTimeout(resolve, intrasleep))
      loop++
    }
    return isOpened()
  }
}

Usage (in async function) :

const websocket = new WebSocket('...')
const opened = await connection(websocket)
if (opened) {
  websocket.send('hello')
}
else {
  console.log("the socket is closed OR couldn't have the socket in time, program crashed");
  return
}
vdegenne
  • 12,272
  • 14
  • 80
  • 106
7

Look on http://dev.w3.org/html5/websockets/

Search for "Event handler" and find the Table.

onopen -> open
onmessage -> message
onerror ->error
onclose ->close

function update(e){ /*Do Something*/};
var ws = new WebSocket("ws://localhost:9999/");

ws.onmessage = update;
Stefan Höltker
  • 311
  • 5
  • 21
  • I'd be nice if w3 provides a real state machine diagram for the readyState. E.g. it can also go from CONNECTING to CLOSED when the connection is refused by the destination. – Thomas Zeman Nov 11 '18 at 07:10
3

tl;dr

A simple proxy wrapper to add state event to WebSocket which will be emitted when its readyState changes:

const WebSocketProxy = new Proxy(WebSocket, {
    construct: function(target, args) {
        // create WebSocket instance
        const instance = new target(...args);

        //internal function to dispatch 'state' event when readyState changed
        function _dispatchStateChangedEvent() {
            instance.dispatchEvent(new Event('state'));
            if (instance.onstate && typeof instance.onstate === 'function') {
                instance.onstate();
            }
        }

        //dispatch event immediately after websocket was initiated
        //obviously it will be CONNECTING event
        setTimeout(function () {
            _dispatchStateChangedEvent();
        }, 0);

        // WebSocket "onopen" handler
        const openHandler = () => {
            _dispatchStateChangedEvent();
        };

        // WebSocket "onclose" handler
        const closeHandler = () => {
            _dispatchStateChangedEvent();
            instance.removeEventListener('open', openHandler);
            instance.removeEventListener('close', closeHandler);
        };

        // add event listeners
        instance.addEventListener('open', openHandler);
        instance.addEventListener('close', closeHandler);

        return instance;
    }
});

A long explanation:

You can use a Proxy object to monitor inner WebSocket state.

This is a good article which explains how to do it Debugging WebSockets using JS Proxy Object

And here is an example of code snippet from the article above in case the site won't be available in the future:

// proxy the window.WebSocket object
var WebSocketProxy = new Proxy(window.WebSocket, {  
  construct: function(target, args) {
    // create WebSocket instance
    const instance = new target(...args);

    // WebSocket "onopen" handler
    const openHandler = (event) => {
      console.log('Open', event);
    };

    // WebSocket "onmessage" handler
    const messageHandler = (event) => {
      console.log('Message', event);
    };

    // WebSocket "onclose" handler
    const closeHandler = (event) => {
      console.log('Close', event);
      // remove event listeners
      instance.removeEventListener('open', openHandler);
      instance.removeEventListener('message', messageHandler);
      instance.removeEventListener('close', closeHandler);
    };  

    // add event listeners
    instance.addEventListener('open', openHandler);
    instance.addEventListener('message', messageHandler);
    instance.addEventListener('close', closeHandler);

    // proxy the WebSocket.send() function
    const sendProxy = new Proxy(instance.send, {
      apply: function(target, thisArg, args) {
        console.log('Send', args);
        target.apply(thisArg, args);
      }
    });

    // replace the native send function with the proxy
    instance.send = sendProxy;

    // return the WebSocket instance
    return instance;
  }
});

// replace the native WebSocket with the proxy
window.WebSocket = WebSocketProxy; 
Anatoly
  • 5,056
  • 9
  • 62
  • 136
0

Your while loop is probably locking up your thread. Try using:

setTimeout(function(){
    if(socket.readyState === 0) {
        //do nothing
    } else if (socket.readyState !=1) {
        //fallback
        setInterval(poll, interval);
    }
}, 50);
Solomon Ucko
  • 5,724
  • 3
  • 24
  • 45
agryson
  • 297
  • 2
  • 9
  • And the first `if` is needed for what exactly? – raina77ow Nov 24 '12 at 22:52
  • If I understand it correctly, `readyState` is not updated immediately. That's why I used a loop. – Kendall Frey Nov 24 '12 at 22:53
  • Honestly I don't know, I'm assuming the same as the while loop in OP question. It can of course be removed unless OP needs something done while `socket.readyState === 0`, I'm trying not to leave anything up to assumption. – agryson Nov 24 '12 at 22:55
  • I do need to do something while `readyState` is zero, and that is wait. – Kendall Frey Nov 24 '12 at 22:58
  • I'd try an if loop to check for the socket change @KendallFrey, with a short setTimeout to check again if it's still 0, and exit out if it has changed. Not ideal, the while _should_ work, but it'll make diagnosing easier without it. – agryson Nov 24 '12 at 23:01
0

Just like you defined an onmessage handler, you can also define an onerror handler. This one will be called when the connection fails.

var socket = new WebSocket(url);
socket.onmessage = onmsg;
socket.onerror = function(error) {
    // connection failed - try polling
}
Philipp
  • 67,764
  • 9
  • 118
  • 153
  • I tested, and it doesn't fire. – Kendall Frey Nov 24 '12 at 23:58
  • I am using that in my application, and it is called pretty reliable whenever there is something wrong with the connection to the server. Do you still have that busy-spin in your code? It might keep the JS engine too busy to execute the error handling. – Philipp Nov 25 '12 at 00:01
  • I commented out the while loop, and did a quick test with `onerror`. It was not run. I'm pretty sure the server isn't closing immediately either, since `onopen` isn't run either. I think that rather than an error occurring, the server is rejecting the connection. – Kendall Frey Nov 25 '12 at 00:05
  • Fast-forward to 2016... It doesn't fire on Safari (9.1.2) but does on Chrome and Firefox. – akauppi Aug 03 '16 at 07:47
0

In my use case, I wanted to show an error on screen if the connection fails.

let $connectionError = document.getElementById("connection-error");

setTimeout( () => {
  if (ws.readyState !== 1) {
    $connectionError.classList.add( "show" );
  }
}, 100 );  // ms

Note that in Safari (9.1.2) no error event gets fired - otherwise I would have placed this in the error handler.

akauppi
  • 17,018
  • 15
  • 95
  • 120
0

There are some pretty... out there... answers in this thread. OP, I think what your looking for is using the advantage of a newer technology (by newer I mean its existed sense the 80s but just now getting included in JS) called futexes, which can be used by javascript's Atomic API.

// set up the futex memory
let shm = new SharedArrayBuffer(1024); // (see below notes!)
let futexBuffer = new Int32Array(shm);
let futexOnState = 0;

// initialize our state to readyState "NEW" (redundant because we're
// just setting the value to 0, but just clarifying.
futexBuffer[futexOnState] = 0; 

// ... 

async function connect() {

    // Set up our websocket
    let wsocket = new Websocket(/* ... */);
    wsocket.onclose = wsocket.onopen = function() {
        futexBuffer[futexOnState] = websocket.readyState;
        Atomics.notify(futexBuffer, futexOnState);
    }

    // Now we wait until the websocket's readystate changes from 0 (NEW)
    await Atomics.waitAsync(futexBuffer, futexOnState, 0).value

    // If we're here, we know that the websocket's readyState is no longer
    // CONNECTING. Thus, we can perform logic on it.

    if(wsocket.readyState == 1) {
         // websocket is OPEN (1)
    } else {
         // websocket is CLOSED (3)
    }

}

Note about SharedArrayBuffer: for complicated reasons, browsers require that your webserver send these headers, otherwise SharedArrayBuffer will not work:

Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp