John Doe
John Doe

Reputation: 57

Using Bluebird Promises, how to solve this with the deferred anti-pattern?

I am learning Bluebird promises and I'm trying to learn to no longer use any Deferred(). The code below runs 100 % correctly and as intended. But this here is an exercise for me on refactoring code to use Bluebird promises properly rather than using the Deferred solution. I'm trying to learn to think differently (correctly) about Promises, but after many attempts I can still not figure out how to solve this particular problem here below without the help of Deferreds.

Does anyone have an idea?

Here is how to run it:

1) Run this program in your console. It will boot up a websocket server that will use port 8080.

2) Then run it a second time in another console window. That one will boot up and use port 8081 after 3 failed attempts with port 8080.

// Initialization stuff
const WebSocket = require('ws');
var wsServer;

// Main Program
// =================================================================
tryCreateWebsocket().then(
    function(){
        console.log("Websocket succesfully initialized.");
    },
    function(){
        console.log("Websocket startup has failed!");
    }
);
// =================================================================



// Helper function: Creating a websocket, with a port as parameter
function createWebsocket(port){
    return new Promise(function(resolve, reject){

        wsServer = new WebSocket.Server({
          perMessageDeflate: false, 
          port: port
        });

        wsServer.on("error", reject);
        wsServer.on("listening", resolve); 
    });
}


// Main function: I try to create a websocket on 5 different ports with a resursive function
function tryCreateWebsocket(attempt, myMainDfd){

    if(typeof attempt === "undefined"){
        attempt = 1;
        myMainDfd = deferred();
    }

    var ports = [8080, 8080, 8080, 8081, 8082]; // In the 2nd client, this should fail until port 8081
    var curPort = ports[attempt - 1];

    var maxAttempts = 5;


    createWebsocket(curPort)
        .then(
            function(){
                myMainDfd.resolve(); // Success
            },
            function(err){ // Error, retry
                if(attempt != maxAttempts){
                    console.log("- attempt " + attempt + " failed. Retry");
                    tryCreateWebsocket(++attempt, myMainDfd);
                }else{
                    myMainDfd.reject();
                }
            }
        );

    return myMainDfd.promise;
}


// Helper Function: I'm still using deferreds for now
function deferred() {
        var resolve, reject;
        var promise = new Promise(function() {
                resolve = arguments[0];
                reject = arguments[1];
        });
        return {
                resolve: resolve,
                reject: reject,
                promise: promise
        };
}

Upvotes: 1

Views: 216

Answers (2)

jfriend00
jfriend00

Reputation: 707626

In 3 years of programming with promises, I've only found one situation where using a deferred made my code simpler. I've come to the conclusion that it's a pretty rare situation. By learning the right techniques (using chaining here) one can nearly always avoid them and end up with simpler code that is at less risk of fairly common mistakes (such as incomplete error propagation or uncaught exceptions).

In this particular case, you can chain your subsequent attempts onto the previous promise by returning a new promise from inside a .then() handler. This allows you to return a promise from your connect function, but keep that promise going for future attempts (holding off its eventual resolution) until some future retry attempts succeeds or until you run out of retry attempts.

You can do that like this. Pay particular attempt to what happens inside the connect() function.

function tryCreateWebsocket(){

    var attempt = 1;
    var ports = [8080, 8080, 8080, 8081, 8082];
    var maxAttempts = ports.length;

    function connect() {
        var curPort = ports[attempt - 1];
        return createWebsocket(curPort).catch(function(err){ // Error, retry
            if(attempt < maxAttempts){
                console.log("- attempt " + attempt + " failed. Retry");
                ++attempt;

                // chain next attempt onto previous promise
                return connect();
            } else {
                // reject here with no more retries
                throw new Error("max retry attempts exceeded without successful connection");
            }
        });
    }

    // start trying to connect, return a promise
    // errors will be caught and subsequent retries will be chained
    // onto this first promise until it either succeeds or runs out
    // of retry attempts
    return connect();
}

// Main Program
// =================================================================
tryCreateWebsocket().then(function(wsServer){
    console.log("Websocket succesfully initialized.");
    // server instance is valid here, use it for further code
},function(){
    console.log("Websocket startup has failed!");
});
// =================================================================



// Helper function: Creating a websocket, with a port as parameter
function createWebsocket(port){
    return new Promise(function(resolve, reject){

        wsServer = new WebSocket.Server({
          perMessageDeflate: false, 
          port: port
        });

        wsServer.on("error", reject);
        wsServer.on("listening", function() {
           resolve(wsServer);
        }); 
    });
}

Note, I changed the design to make the wsServer instance the resolved value of the returned promise. Then, you aren't relying on side effects to set that higher scoped variable. You can just get it from the resolved promise and store it where you want at the time you know it's valid.

Upvotes: 2

John Doe
John Doe

Reputation: 57

Here is one possible solution I came up with. What do you think? It still uses a total of 2 promises ( one in the createWebsocket function and one in the tryCreateWebsocket function here below).

function tryCreateWebsocket(){

    var lstPorts = [8080, 8080, 8080, 8081, 8080];
    return new Promise(function(resolve, reject){

        function next(port) {

            createWebsocket(port)
                .then(
                    resolve,
                    function(){ // Reject, but not until you've tried a little
                        console.log("Port "+port+" failed. I might try the next port.");
                        // rejected
                        if(lstPorts.length >= 1){
                            next( lstPorts.shift() )
                        }else{
                            reject(); // do reject
                        }

                    }
                );
        }

        next( lstPorts.shift() ); // Start the loop
    });
}

Upvotes: 0

Related Questions