Search code examples
javascripttimercallbackpromisereconnect

Failing to resolve promise within my websocket acknowledgement reconnect logic


I have a custom connect function that creates a promise I want resolved once I make a websocket call and receive an acknowledgement. The remote server may be up, it may be down, but if it's unavailable I want to keep trying until I'm successful.

const socketIOClient = require('socket.io-client');

function createTimeoutCallback(callback)
{
  let called = false;

  let timerID = setTimeout(() => {
      if (called) return;
      called = true;
      callback(new TimeoutError());
    },
    60*1000);

  return function() {
    if (called) return;
    called = true;
    clearTimeout(timerID);
    callback.apply(this, arguments);
  }
}

async function myConnect()
{
  let mysocket = socketIOClient(url);

  return new Promise((resolve, reject) => {

    mysocket.emit('clientconnect', args, createTimeoutCallback((resp) => {
      if (!(resp instanceof TimeoutError)) {

        // SUCCESS
        doSomething();
        resolve();
      }

      // We timed out, try again
      else {
        mysocket.close();
        setTimeout(myConnect, 60*1000);
      }  
    }));
  });
}

await connect();
// doSomething() gets called but we never get here

In the above code, if the endpoint is available, everything works fine. But I'm never returning from the myConnect() function when (1) I wait on its promise; and (2) the function needs to make several connection attempts (e.g., the server is not initially up); and (3) the endpoint finally comes back online and the connection succeeds. I suspect this has everything to do with me essentially abandoning the original promise on a reconnect attempt, but I don't want to reject it or the operation will prematurely fail.


Solution

  • I did find a workaround, which relies on an embedded function. With this technique there is only one promise which is always accessible and in scope, and the timer-based recursion (i.e., not stack-based) is always wrapped in that single promise that is never abandoned. I put this together before Jaromanda answered, so I can't confirm that his solution would work.

    async function myConnect()
    {
      return new Promise((resolve, reject) => {
    
        function innerConnect()
        {
          let mysocket = socketIOClient(url);
    
          mysocket.emit('clientconnect', args, createTimeoutCallback((resp) => {
            if (!(resp instanceof TimeoutError)) {
    
              // SUCCESS
              doSomething();
              resolve();
            }
    
            // We timed out, try again
            else {
              mysocket.close();
              setTimeout(innerConnect, 60*1000);
            }  
          }));
        }
    
        innerConnect();
      });
    }