9
\$\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\$
14
\$\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\$
  • \$\begingroup\$ I think the last line in the code should be })(); \$\endgroup\$ – Spike Oct 5 '15 at 20:38
  • 1
    \$\begingroup\$ @Spike, it's not being immediately invoked, so no. \$\endgroup\$ – Quill Oct 6 '15 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 '15 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\$ – fearless_fool Jul 30 '16 at 0:19
2
\$\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\$
  • \$\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\$ – Jeroen Vannevel Oct 5 '15 at 21:43
  • \$\begingroup\$ ok I've included an example above \$\endgroup\$ – Thijs Koerselman Oct 6 '15 at 22:24

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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