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.
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;
});
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With