AngularJS : Enhancing or wrapping promises with pre/post resolve/reject actions

Issue

Goal

I’m trying to create a series of promise ‘enhancers’ which will add functionality (such as caching, queuing, redirect handling, etc.) around existing promises which are simple http requests.

Problem

The issue I’m experiencing with this method of enhancing promises is that if an enhancement adds any functions or publicly accessible properties to the promise (or if I’m wrapping an already-enhanced promise like a restangular request), those are lost when I wrap it in a new promise by returning a new $q.

Question

What pattern can I use to enhance or wrap promises (like in the two examples below), but without losing any other (non-conflicting) enhancements promises might have?

Example 1

Here is an example that will automatically handle 503-Retry-After errors:

function _enhancePromiseWithAutoRetry(promise) {
  var enhancedPromise = $q(function(resolve, reject) {
    var newReject = get503Handler(this, resolve, reject);
    promise.then(resolve, newReject);
  });

  // 503 handling isn't enabled until the user calls this function.
  enhancedPromise.withAutoRetry = function(onRetry, timeout) {
    var newPromise = angular.copy(this);
    newPromise._503handled = true;
    newPromise._503onRetry = onRetry;
    newPromise._503timeout = timeout;
    return newPromise;
  };

  return enhancedPromise;
}

The idea is that if I return a promise enhanced with the above function, the user can go:

someRequest.withAutoRetry().then(onSuccess, onError);

Or to be more clear (with chaining):

someRequest.then(onSuccess, onAnyError)
           .withAutoRetry().then(onSuccess, onNon503Error);

Here, the first call to then(...) might error out right away if the server is busy, but the calls after .withAutoRetry() will poll the server with repeated requests until the response is successful, or a non RetryAfter error is returned.

Example 2

Here is an another example which adds custom caching behaviour:

function _enhancePromiseWithCache(promise, cacheGet, cachePut) {
  // Wrap the old promise with a new one that will get called first.
  return $q(function(resolve, reject) {
    // Check if the value is cached using the provided function
    var cachedResponse = cacheGet !== undefined ? cacheGet() : undefined;
    if(cachedResponse !== undefined){
      resolve(cachedResponse);
    } else {
      // Evaluate the wrapped promise, cache the result, then return it.
      promise.then(cachePut);
      promise.then(resolve, reject);
    }
  });
}

This one allows the library to set up a cache of data which can be used instead of making requests to the server, and can be added to after a request is completed. For example:

lib.getNameOrigin = function(args) {
  var restRequest = Restangular.all('people').one(args.id).get('nameOrigin');
  // Cache, since all people with the same name will have the same name origin
  var enhancedPromise = _enhancePromiseWithCache(restRequest,
                          function(){ return nameOrigins[args.name]; },
                          function(val){ nameOrigins[args.name] = val; });
  return enhancedPromise;
}

Elsewhere

// Will transparently populate the cache
lib.getNameOrigin({id: 123, name:'john'}).then(onSuccess, onError).then(...);

And somewhere else entirely

// Will transparently retrieve the result from the cache rather than make request
lib.getNameOrigin({id: 928, name:'john'}).then(onSuccess, onError);

Possible Solution

I’ve considered copying the original promise, but then overwriting the new one’s then function with an implementation that references the original promise’s then (using the Proxy Pattern), but is this safe? I know there’s a lot more to promises than just the then function.

Solution

The solution is not to enhance the promises themselves, but the factories that create them.

Use functional programming and/or aspect-orientated programming approaches to decorate the original function. This will not only be less errorprone, but more concise, composable and reusable.

function decorate(makeThenable) {
    return function(...args) {
        … // before creating the thenable
        return makeThenable(...args).then(function(value) {
            … // handle fulfillment
            return …; // the resulting value
        }, function(error) {
            … // handle rejection
            return …; // (or throw)
        });
    };
}
var decorated = decorate(myThenablemaker);
decorated(…).then(whenFulfilled, whenRejected);

Example 1:

function withAutoRetry(request, timeout) {
    return function() {
        var args = arguments;
        return request.apply(null, args).catch(function handle(e) {
            if (e instanceof Http503Error) // or whatever
                return request.apply(null, args).catch(handle);
            else
                throw e;
        });
    };
}

withAutoRetry(someRequest)().then(onSuccess, onError);

withAutoRetry(function() {
    return someRequest().then(onSuccess, onAnyError);
})().then(onSuccess, onNon503Error);

Example 2:

function withCache(request, hash) {
    var cache = {};
    if (!hash) hash = String;
    return function() {
        var key = hash.apply(this, arguments);
        if (key in cache)
            return cache[key];
        else
            return cache[key] = request.apply(this, arguments);
    };
}

lib.getNameOrigin = withCache(function(args) {
    return Restangular.all('people').one(args.id).get('nameOrigin');
}, function(args) {
    return args.name;
});

Answered By – Bergi

Answer Checked By – Robin (AngularFixing Admin)

Leave a Reply

Your email address will not be published.