Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Execute multiple tasks asynchronously and return first successful result in JavaScript function

I have to write a javaScript function that return some data to the caller.

In that function I have multiple ways to retrieve data i.e.,

  1. Lookup from cache
  2. Retrieve from HTML5 LocalStorage
  3. Retrieve from REST Backend (bonus: put the fresh data back into cache)

Each option may take its own time to finish and it may succeed or fail.

What I want to do is, to execute all those three options asynchronously/parallely and return the result whoever return first.

I understand that parallel execution is not possible in JavaScript since it is single threaded, but I want to at least execute them asynchronously and cancel the other tasks if one of them return successfully result.

I have one more question.

Early return and continue executing the remaining task in a JavaScript function.

Example pseudo code:

function getOrder(id) {

    var order;

    // early return if the order is found in cache.
    if (order = cache.get(id)) return order;

    // continue to get the order from the backend REST API.
    order = cache.put(backend.get(id));

    return order;
}

Please advice how to implement those requirements in JavaScript.

Solutions discovered so far:

  1. Fastest Result

    JavaScript ES6 solution

    Ref: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise

Promise.race(iterable)

Returns a promise that resolves when the first promise in the iterable resolves.

var p1 = new Promise(function(resolve, reject) { setTimeout(resolve, 500, "one"); });
var p2 = new Promise(function(resolve, reject) { setTimeout(resolve, 100, "two"); });
Promise.race([p1, p2]).then(function(value) {
  // value == "two"
});

Java/Groovy solution

Ref: http://gpars.org/1.1.0/guide/guide/single.html

import groovyx.gpars.dataflow.Promise
import groovyx.gpars.dataflow.Select
import groovyx.gpars.group.DefaultPGroup
import java.util.concurrent.atomic.AtomicBoolean

/**
 * Demonstrates the use of dataflow tasks and selects to pick the fastest result of concurrently run calculations.
 * It shows a waz to cancel the slower tasks once a result is known
 */

final group = new DefaultPGroup()
final done = new AtomicBoolean()

group.with {
    Promise p1 = task {
        sleep(1000)
        if (done.get()) return
        10 * 10 + 1
    }
    Promise p2 = task {
        sleep(1000)
        if (done.get()) return
        5 * 20 + 2
    }
    Promise p3 = task {
        sleep(1000)
        if (done.get()) return
        1 * 100 + 3
    }

    final alt = new Select(group, p1, p2, p3, Select.createTimeout(500))
    def result = alt.select()
    done.set(true)
    println "Result: " + result
}
  1. Early Return and Interactive Function

    Angular Promises combined with ES6 generators???

    angular.module('org.common')
    .service('SpaceService', function ($q, $timeout, Restangular, $angularCacheFactory) {
    
    
    var _spacesCache = $angularCacheFactory('spacesCache', {
        maxAge: 120000, // items expire after two min
        deleteOnExpire: 'aggressive',
        onExpire: function (key, value) {
            Restangular.one('organizations', key).getList('spaces').then(function (data) {
                _spacesCache.put(key, data);
            });
        }
    });
    /**
     * @class SpaceService
     */
    return {
        getAllSpaces: function (orgId) {
            var deferred = $q.defer();
            var spaces;
            if (spaces = _spacesCache.get(orgId)) {
                deferred.resolve(spaces);
            } else {
                Restangular.one('organizations', orgId).getList('spaces').then(function (data) {
                    _spacesCache.put(orgId, data);
                    deferred.resolve(data);
                } , function errorCallback(err) {
                    deferred.reject(err);
                });
            }
            return deferred.promise;
        },
        getAllSpaces1: function (orgId) {
            var deferred = $q.defer();
            var spaces;
            var timerID = $timeout(
                Restangular.one('organizations', orgId).getList('spaces').then(function (data) {
                    _spacesCache.put(orgId, data);
                    deferred.resolve(data);
                }), function errorCallback(err) {
                    deferred.reject(err);
                }, 0);
            deferred.notify('Trying the cache now...'); //progress notification
            if (spaces = _spacesCache.get(orgId)) {
                $timeout.cancel(timerID);
                deferred.resolve(spaces);
            }
            return deferred.promise;
        },
        getAllSpaces2: function (orgId) {
            // set up a dummy canceler
            var canceler = $q.defer();
            var deferred = $q.defer();
            var spaces;
    
            $timeout(
                Restangular.one('organizations', orgId).withHttpConfig({timeout: canceler.promise}).getList('spaces').then(function (data) {
                    _spacesCache.put(orgId, data);
                    deferred.resolve(data);
                }), function errorCallback(err) {
                    deferred.reject(err);
                }, 0);
    
    
            if (spaces = _spacesCache.get(orgId)) {
                canceler.resolve();
                deferred.resolve(spaces);
            }
    
            return deferred.promise;
        },
        addSpace: function (orgId, space) {
            _spacesCache.remove(orgId);
            // do something with the data
            return '';
        },
        editSpace: function (space) {
            _spacesCache.remove(space.organization.id);
            // do something with the data
            return '';
        },
        deleteSpace: function (space) {
            console.table(space);
            _spacesCache.remove(space.organization.id);
            return space.remove();
        }
    };
    });
    
like image 547
xmlking Avatar asked Jan 20 '14 20:01

xmlking


1 Answers

Personally, I would try the three asynchronous retrievals sequentially, starting with the least expensive and ending with the most expensive. However, responding to the first of three parallel retrievals is an interesting problem.

You should be able to exploit the characteristic of $q.all(promises), by which :

  • as soon as any of the promises fails then the returned promise is rejected
  • if all promises are successful then the returned promise is resolved.

But you want to invert the logic such that :

  • as soon as any of the promises is successful then the returned promise is resolved
  • if all promises fail then the returned promise is rejected.

This should be achievable with an invert() utility which converts success to failure and vice versa.

function invert(promise) {
    return promise.then(function(x) {
        return $q.defer().reject(x).promise;
    }, function(x) {
        return $q.defer().resolve(x).promise;
    });
}

And a first() utility, to give the desired behaviour :

function first(arr) {
    return invert($q.all(arr.map(invert)));
}

Notes:

  • the input arr is an array of promises
  • a native implementation of array.map() is assumed (otherwise you can explicitly loop to achieve the same effect)
  • the outer invert() in first() restores the correct sense of the promise it returns
  • I'm not particularly experienced in angular, so I may have made syntactic errors - however I think the logic is correct.

Then getOrder() will be something like this :

function getOrder(id) {
    return first([
        cache.get(id),
        localStorage.get(id).then(cache.put),
        backend.get(id).then(cache.put).then(localStorage.put)
    ]);
}

Thus, getOrder(id) should return a Promise of an order (not the order directly).

like image 54
Beetroot-Beetroot Avatar answered Oct 16 '22 04:10

Beetroot-Beetroot