7

I would make one call to the server to get a list of items. How do I make sure that only one call is made and the collections is processed only once to create a key value map.

var itemMap = {};

function getItems(){
    getAllItemsFromServer().then(function(data){
     data.forEach(value){
       itemMap[value.key] = value;
     }});
     return itemMap;

}

//need to get the values from various places, using a loop here  
//to make multiple calls
var itemKeys = ['a', 'b', 'c'];
itemKeys.forEach(key){
   var value = getItems().then(function(data){ return data[key]});
   console.log('item for key=' + value);
}
dipu
  • 1,340
  • 3
  • 16
  • 28

6 Answers6

10

I'm going to add a generic method for caching a promise operation. The trick here is that by treating the promise as a real proxy for a value and caching it and not the value we avoid the race condition. This also works for non promise functions just as well, illustrating how promises capture the concept of async + value well. I think it'd help you understand the problem better:

function cache(fn){
    var NO_RESULT = {}; // unique, would use Symbol if ES2015-able
    var res = NO_RESULT;
    return function () { // if ES2015, name the function the same as fn
        if(res === NO_RESULT) return (res = fn.apply(this, arguments));
        return res;
    };
}

This would let you cache any promise (or non promise operation very easily:

 var getItems = cache(getAllItemsFromServer);


 getItems();
 getItems();
 getItems(); // only one call was made, can `then` to get data.

Note that you cannot make it "synchronous".

Community
  • 1
  • 1
Benjamin Gruenbaum
  • 270,886
  • 87
  • 504
  • 504
  • I've never thought of using Symbols as tokens, an object identity was always good enough for that. Clever! – Bergi Jul 29 '15 at 21:47
  • Excellent. I was trying something similar with some added complexity. Let me check by cleaning up my extra stuff. – dipu Jul 30 '15 at 02:49
  • This doesn't cache with arguments as key right? Would it be easy to modify it to do that? – Basil Jun 17 '17 at 00:51
6

A promise is stateful, and as soon as it's fulfilled, its value cannot be changed. You can use .then multiple times to get its contents, and you'll get the same result every time.

The getAllItemsFromServer function returns a promise, the then block manipulates the responses and returns the itemMap, which is wrapped in a response (promise chaining). The promise is then cached and can be used to get the itemMap repeatedly.

var itemsPromise = getItems(); // make the request once and get a promise

function getItems(){
    return getAllItemsFromServer().then(function(data){
       return data.reduce(function(itemMap, value){
          itemMap[value.key] = value;
          return itemMap;
       }, {});
    });
}

//need to get the values from various places, using a loop here  
//to make multiple calls
var itemKeys = ['a', 'b', 'c'];
itemKeys.forEach(function(key){
    itemsPromise.then(function(data){ 
        return data[key];
    }).then(function(value) {
       console.log('item for key=' + value);
    });
});
Ori Drori
  • 183,571
  • 29
  • 224
  • 209
6

I think what you're really looking for is

var cache = null; // cache for the promise
function getItems() {
    return cache || (cache = getAllItemsFromServer().then(function(data){
        var itemMap = {};
        data.forEach(function(value){
            itemMap[value.key] = value;
        });
        return itemMap;
    }));
}

var itemKeys = ['a', 'b', 'c'];
itemKeys.forEach(function(key){
    getItems().then(function(data){
        return data[key];
    }).then(function(value) {
        console.log('item for key=' + value);
    }); // notice that you get the value only asynchronously!
});
Bergi
  • 630,263
  • 148
  • 957
  • 1,375
1

You could also solve this by using a utils library, for an instance by using the memoizeAsync decorator from utils-decorator lib (npm install utils-decorators):

import {memoizeAsync} from 'utils-decorators';

class MyAppComponent {

  @memoizeAsync(30000)
  getData(input) {
   ...
  }
}

Or by this wrapper function

import {memoizeAsyncify} from 'utils-decorators';

const methodWithCache = memoizeAsyncify(yourFunction);
vlio20
  • 8,955
  • 18
  • 95
  • 180
0

Declare a Flag associative array, and set after the response from server and only query when flags[key] is undefined or null.

var flags = {}
itemKeys.forEach(key){
   if(!flags[key]) {
      var value = getItems().then(function(data){ flags[key] = true; return data[key]});
      console.log('item for key=' + value);
   }
}

may be you should also set flags in few other places depending on the key parameter.

0

Try npm dedup-async, which caches duplicated promise calls if there's a pending one, so concurrent promise calls trigger only one actual task.

const dedupa = require('dedup-async')

let evil
let n = 0

function task() {
    return new Promise((resolve, reject) => {
        if (evil)
            throw 'Duplicated concurrent call!'
        evil = true

        setTimeout(() => {
            console.log('Working...')
            evil = false
            resolve(n++)
        }, 100)
    })
}

function test() {
    dedupa(task)
        .then (d => console.log('Finish:', d))
        .catch(e => console.log('Error:', e))
}

test()                //Prints 'Working...', resolves 0
test()                //No print,            resolves 0
setTimeout(test, 200) //Prints 'Working...', resolves 1
Nan Wang
  • 65
  • 6