Perform multiple tasks asynchronously and return the first successful result in a JavaScript function

I need to write a javaScript function that returns some data to the caller.

In this function, I have several ways to retrieve data, i.e.

  • Cache Search
  • Get from HTML5 LocalStorage
  • Get from REST Backend (bonus: return new data to the cache)

Each option may take its time to complete, and it may succeed or fail.

I want to do to execute all these three parameters asynchronously / in parallel and return the result to those who return first.

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

I've got one more question.

Return early and continue with the remaining task in the JavaScript function.

Pseudo-code example:

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; } 

Consult how to implement these requirements in JavaScript.

Solutions discovered so far:

Promise.race (iterable)

Returns the promise that is resolved when the first promise is resolved in the fighter.

 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

Link: 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 } 
  • Early Feedback and Interactive Feature

    Angular Promises in combination 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(); } }; }); 
+6
source share
3 answers

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

You should use the characteristic $q.all(promises) , according to which:

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

But you want to invert the logic so that:

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

This should be achieved using the 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 the first() utility to give the desired behavior:

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

Notes:

  • arr input is an array of promises
  • assumes a built-in implementation of array.map() (otherwise you can explicitly create a loop to achieve the same effect)
  • the outer invert() in first() restores the correct sense of promise that it returns.
  • I am not particularly good at angular, so I may have made syntax errors, but I think the logic is correct.

Then getOrder() would 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 the promise of the order (and not the order directly).

+7
source

Create a broadcast event in your api calls, then create $ scope. $ on to listen to these broadcasts when $ on get activates a function that updates these objects.

So, you have a function that makes ajax calls for your a priori. You will have 3 ajax calls. And 3 listeners. Each of them will look something like this.

This is just sudo code, but this format is how you do it, something like

  $http({ method: "GET", url: url_of_api, }).success(function(data, *args, *kwargs){ $rooteScope.$braodcast('success', data) }) 

There is a listener in your controller, something like this

 $scope.$on('success', function(event, args){ // Check the state of the other objects, have they been refreshed - you probably want to set flags to check if (No Flags are Set): $scope.data = args // which would be the returned data adn $scope.data would be what you're trying to refresh. } 
0
source

The problem in your getOrder example is that if the three search functions are asynchronous, you will not immediately receive an order from them and, since they are not blocked, getOrder will return null ; You would be better off defining a callback function that takes action on the first order data returned and simply ignores the rest.

 var doSomethingWithTheOrder = function CallBackOnce (yourResult) { if (!CallBackOnce.returned) { CallBackOnce.returned = true; // Handle the returned data console.log('handle', yourResult); } else { // Ignore the rest console.log('you are too late'); } } 

Make your data lookup functions accepted by callbacks

 function cacheLookUp(id, callback) { // Make a real lookup here setTimeout(function () { callback('order data from cache'); }, 3000); } function localeStorageLookUp(id, callback) { // Make a real lookup here setTimeout(function () { callback('order data from locale storage'); }, 1500); } function restLookUp(id, callback) { // Make a real lookup here setTimeout(function () { callback('order data from rest'); }, 5000); } 

And pass a callback function to each one

 function getOrder(id) { cacheLookUp(id, doSomethingWithTheOrder); localeStorageLookUp(id, doSomethingWithTheOrder); restLookUp(id, doSomethingWithTheOrder); } 
0
source

All Articles