10
\$\begingroup\$

I'm wondering what the best approach might be.

One approach is to chain cachedPromise and "regular", thus, if cachedPromise fails, we call a regular one (and caching results).

The second is to have a wrapper function, something like a decorator in python, and being able to wrap any promise-returning function when needed.

Here's code illustrating both approaches:

// this is for node.js environment
// in modern browsers Promise is already implemented 
// as a standard feature
var Promise = require('bluebird');

var cachedPromise = {
    _cache: {},
    get: function(key) {
        var isCached = this._cache.hasOwnProperty(key);
        var data = this._cache[key];
        return new Promise(function(resolve, reject) {
            isCached ? resolve(data) : reject();
        });
    }, 
    store: function(key, value) {
        this._cache[key] = value;   
    }
}

var WrappedPromise = function(promiseFunc) {
    var cache = {};
    return function(key) {
        if (cache.hasOwnProperty(key)) {
            return Promise.cast(cache[key]);
        } else {
            var promise = promiseFunc(key);
            return promise.then(function(data) {
                cache[key] = data;
                return promise;
            });
        }
    }
}

var mockRequestPromise = function(name){
    return new Promise(function(resolve, reject) {
        setTimeout(function() {
            resolve({'name': name});
        }, 1000);
    });
}

var requestChained = function(name) {
    return cachedPromise.get(name).catch(function() {
        var promise = mockRequestPromise(name);
        return promise.then(function(data) {
                   cachedPromise.store(name, data);
                   return promise;
               })
    });
}

var requestWrapped = WrappedPromise(mockRequestPromise);

If you have any considerations regarding this issue, I will be much obliged.

\$\endgroup\$

2 Answers 2

16
\$\begingroup\$

Code looks fine - but it strikes me as somewhat unnecessary.

A promise can only be resolved/rejected once, so if you just keep the promise object itself around, its result is automatically cached within. I.e. if you call .then() on an already-resolved/already-rejected promise object, it'll just immediately invoke your handler(s) with whatever data it was originally resolved/rejected. It won't re-do whatever caused it to be resolved/rejected. So just keep the promise object itself around and you can use it whenever you want.

In order words, caching promise objects makes sense (e.g. caching web request promises), but caching promise results is built-in and happens automatically.

So to that end, it'd be smarter to add caching at a higher level. I.e. don't wrap generic promises, instead wrap specific tasks like network requests and such where caching the promise is desired and you can have more control over what keys to use, when to invalidate the cache etc..

I'd do something like this

// *specialized* function for, say, network requests,
// where caching is desired. In this case, we're caching
// promises by the URL they're fetching
var fetch = (function () {
  var cache = {};
  return function (url) {
    cache[url] = cache.hasOwnProperty(url) ? cache[url] : new Promise(/* request magic goes here */);
    return cache[url];
  };
});

(Obviously, you'll want a way to invalidate the cache etc., this is just to illustrate the point)

\$\endgroup\$
4
  • \$\begingroup\$ I think the last line in the code should be })(); \$\endgroup\$
    – Spike
    Oct 5, 2015 at 20:38
  • 1
    \$\begingroup\$ @Spike, it's not being immediately invoked, so no. \$\endgroup\$
    – Quill
    Oct 6, 2015 at 0:58
  • \$\begingroup\$ @Spike Both you and @Quill are correct, actually. If you leave it as-is it's a function factory that'll create functions with their own cache. If you invoke it immediately with () you get 1 function with a cache. So either thing would work, depending on your needs \$\endgroup\$
    – Flambino
    Oct 6, 2015 at 21:40
  • 1
    \$\begingroup\$ I'm a huge fan of ternary expressions, but unconditionally assigning cache[url] doesn't smell right. How about return cache.hasOwnProperty(url) ? cache[url] : (cache[url] = new Promise(...)) instead? \$\endgroup\$ Jul 30, 2016 at 0:19
3
\$\begingroup\$

Since promises just return a value you can simply use the something like the Lodash memoize on them. Just like with regular functions.

import { memoize } from 'lodash/function';

const memoizedFetchItemsById = memoize(fetchItemsById, (ids) => {
  return ids.sort().join('_');
});

In this example I use a promise returning function fetchItemsById, which takes an array of string ids as its argument. The memoize() second parameter is a resolver function. Its return value becomes the cache key. I sort the array and convert it to a string to be used as the cache key so it will always be the same regardless of the order in the array.

The returned memoized function can be called just like the original fetchItemsById. Depending on the function you will want to use a different resolve function.

\$\endgroup\$
2
  • \$\begingroup\$ I'm unfamiliar with what a Lodash memoize entails exactly. Would you mind expanding a little bit on the subject? Is it something you could provide an example implementation for? \$\endgroup\$ Oct 5, 2015 at 21:43
  • \$\begingroup\$ ok I've included an example above \$\endgroup\$ Oct 6, 2015 at 22:24

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.