nickrenfo2
nickrenfo2

Reputation: 85

Call one of two async functions but not both

I have a project with the following basic setup... (I stripped it down to just the necessary parts, but there are other functions besides setHtml in the engine object)

var engine = {
    setHtml:function(){
        htmlGenerator(callback)
        //if htmlGenerator takes 5 or more seconds to respond, skip the item. 
        //Otherwise, continue with regular flow

        function skip(){
            //skip the current item
        }

        function callback(){
            //continue with regular flow
        }
    }

}

It seems simple enough, but there are a few challenges...

htmlGenerator is located in another file. I have no way of knowing whether that function is synchronous or asynchronous, and ideally that doesn't matter.

htmlGenerator will call callback() when it's ready.

Exactly one of either skip() or callback() should be called, not both or none.

If htmlGenerator takes longer than 5s to call callback, I want to call skip(). This does not mean htmlGenerator will not call callback().

skip() may be called recursively, but cannot be determined whether or not it will be called recursively. (i.e. part of the skip call is to call setHtml again with a new set of data, which may or may not result in another call taking 5 or more seconds)

This is a front-end application, so I would like to avoid bringing on additional libraries/modules/packages if possible, as load times are a top priority.

For a while, I had something like the following, but that didn't work for reasons I cannot explain.

var engine = {
    skipTimer:null
    setHtml:function(){
        htmlGenerator(callback)
        //if htmlGenerator takes 5 or more seconds to respond, skip the item. 
        //Otherwise, continue with regular flow

        engine.skipTimer = setTimeout(skip,5000)

        function skip(){
            console.log(engine.skipTimer)//this would correctly display the timer's ID.
            //skip the current item
        }

        function callback(){
            console.log(engine.skipTimer)//this was "false" or "null" or something to that effect
            clearTimeout(engine.skipTimer) //for some reason, the timer was never cleared

            //continue with regular flow
        }
    }

}

In my test cases, I didn't call the function with multiple failures (skips), however I do need it to handle that.

I also tried the following, with just as little success...

var engine = {
    setHtml:function(){
        var called = false;
        htmlGenerator(callback)
        //if htmlGenerator takes 5 or more seconds to respond, skip the item. 
        //Otherwise, continue with regular flow

        engine.skipTimer = setTimeout(skip,5000)

        function skip(){
            if (called)return;
            called = true;
            //skip the current item
        }

        function callback(){
            if(called)return;
            called = true;
            //continue with regular flow
        }
    }

}

If you understand why those results failed, or how to solve this problem, any help would be greatly appreciated.

Upvotes: 2

Views: 95

Answers (2)

ShuberFu
ShuberFu

Reputation: 709

Since htmlGenerator may be async or sync, it will be a good idea to use Promise. Specifically Promise.race().

var engine = {
    setHtml:function(){
        var called = false;
        var p1 = new Promise(function(resolve){
            htmlGenerator(function(){
                resolve(true)
            });
        }
        //if htmlGenerator takes 5 or more seconds to respond, skip the item. 
        //Otherwise, continue with regular flow
        var p2 = new Promise(function(resolve) { 
            setTimeout(function(){
                resolve(false)
            }, 5000);
        });

        return Promise.race([p1,p2]);
    }

}

The above means your engine.setHtml will return a Promise that resolves to true if htmlGenerator calls it's call back first, or false if the 5 seconds timeout beat it to it.

So the usage would be a bit like this (I'm assuming

engine.setHtml().then(function(generated){
    if (generated){
        // Do stuff here for when htmlGenerator "wins" the callback race.
    } else {
        // Generator lost, you want to skip, do whatever things you need to do to "skip".
    }
});

Note that if you want to continue within the setHtml function, instead of return Promise.race([p1,p2]), do this instead.

Promise.race([p1,p2]).then(function(generated){
    if (generated){
        // Do stuff here for when htmlGenerator "wins" the callback race.
    } else {
        // Generator lost, you want to skip, do whatever things you need to do to "skip".
    }
});

A little bit of an explanation. The first block of code essentially creates two "Promises" that can resolve individually. Promise p1 resolves to a value of true (resolve(true)) when htmlGenerator calls its callback. Promise p2 resolves to a value of false (resolve(false)) when setTimeout times out (in your case 5 seconds).

The Promise.race will resolve using which ever Promise that resolves first. So if htmlGenerator calls its callback first, the Promise.race resolves to the value of p1, which is resolved to true. Conversely, if setTimeout calls its callback first, Promise.race resolves to the value of p2, which is resolved to false. Finally, when you use Promise.race([p1,p2]).then(callback) the callback receives the resolved value of Promise.race([p1,p2]), which will be the resolved value of the promise [p1,p2] that won the race..

Upvotes: 3

Rory McCrossan
Rory McCrossan

Reputation: 337570

The problem you describe will occur when htmlGenerator() is processed synchronously. In this case the htmlGenerator, and therefore callback(), will be executed before the setTimeout is called, so you call clearTimeout on null. This means no timer is cleared. You then start the timer as soon as htmlGenerator completes, hence skip is always called.

To fix this you simply need to set the timer before you call htmlGenerator:

var engine = {
    skipTimer:null
    setHtml:function() {
        engine.skipTimer = setTimeout(skip, 5000);
        htmlGenerator(callback);

        function skip() {
            console.log(engine.skipTimer)
        }

        function callback(){
            console.log(engine.skipTimer);
            clearTimeout(engine.skipTimer);
        }
    }
}

To get a clear view of the difference check the console in this working fiddle, which only shows the timer id from the callback() function, against your original version where you first see null from callback, then the timer id from skip().

Upvotes: 1

Related Questions