src/httpDecorator.js
import angular from 'angular'
import objectKeys from 'object-keys'
import arrayMap from 'array-map'
export default httpEtagHttpDecorator
httpEtagHttpDecorator.$inject = ['$delegate', 'httpEtag']
function httpEtagHttpDecorator ($delegate, httpEtag) {
var $http = $delegate
var cachableHttpMethods = [
'GET',
'JSONP'
]
function $httpDecorator (httpConfig) {
var useLegacyPromiseExtensions = httpEtagHttpDecorator.useLegacyPromiseExtensions()
var hasConfig = !!httpConfig.etagCache
var isCacheableMethod = cachableHttpMethods.indexOf(httpConfig.method) >= 0
var isCachable = hasConfig && isCacheableMethod
var httpPromise
if (isCachable) {
var etagCacheConfig = processHttpConfigEtagValue(httpConfig)
if (etagCacheConfig) {
var itemCache = httpEtag.getItemCache(etagCacheConfig.id, etagCacheConfig.itemKey)
if (!itemCache) throw new Error('No defined ETag caches match specified cache ID')
var cacheInfo = itemCache.info()
var rawCacheData = itemCache.$get()
var cachedEtag = rawCacheData && rawCacheData.etagHeader
var cachedResponse = cachedEtag && rawCacheData.responseData
// Allow easy access to cache in interceptor
httpConfig.$$_itemCache = itemCache
if (cachedEtag) {
httpConfig.headers = angular.extend({}, httpConfig.headers, {
'If-None-Match': cachedEtag
})
}
}
}
httpPromise = $http.apply($http, arguments)
httpEtagPromiseFactory(httpPromise)
function httpEtagPromiseFactory (httpPromise) {
var then = httpPromise.then
var success = httpPromise.success
if (useLegacyPromiseExtensions) {
httpPromise.cached = function httpEtagPromiseCached (callback) {
if (rawCacheData && cacheInfo.cacheResponseData) {
callback(cachedResponse, 'cached', undefined, httpConfig, itemCache)
}
return httpPromise
}
} else {
httpPromise.cached = function deprecatedEtagPromiseCached () {
throw new Error('The method `cached` on the promise returned from `$http` has been disabled in favor of `ifCached`.')
}
}
httpPromise.ifCached = function httpEtagPromiseIfCached (callback) {
if (rawCacheData && cacheInfo.cacheResponseData) {
// We're simply mimicking the Angular API here, so... sorry, Standard.
// eslint-disable-next-line standard/no-callback-literal
callback({
data: cachedResponse,
status: 'cached',
headers: undefined,
config: httpConfig
}, itemCache)
}
return httpPromise
}
httpPromise.then = function httpEtagThenWrapper (successCallback, errorCallback, progressBackCallback) {
var thenPromise = then.apply(httpPromise, [
successCallback ? httpEtagSuccessWrapper : undefined,
errorCallback ? httpEtagErrorWrapper : undefined,
progressBackCallback
])
function httpEtagSuccessWrapper (response) {
return successCallback(response, itemCache)
}
function httpEtagErrorWrapper (response) {
return errorCallback(response, itemCache)
}
return httpEtagPromiseFactory(thenPromise)
}
if (useLegacyPromiseExtensions && itemCache) {
httpPromise.success = function httpEtagPromiseSuccess (callback) {
var partializedCallback = partial(callback, undefined, undefined, undefined, undefined, itemCache)
return success.apply(httpPromise, [partializedCallback])
}
}
return httpPromise
}
return httpPromise
}
// Decorate the cachable shortcut methods, too
angular.forEach(cachableHttpMethods, function (httpMethod) {
var method = httpMethod.toLowerCase()
$httpDecorator[method] = function httpEtagHttpShortcutWrapper (url, config) {
config = angular.extend({}, config, {
method: httpMethod,
url: url
})
return $httpDecorator.call($http, config)
}
})
// Copy over all other properties and methods
angular.forEach($http, function copyHttpPropertyToDectorator (value, key) {
if (!$httpDecorator[key]) $httpDecorator[key] = value
})
/**
* HELPERS
*/
function processHttpConfigEtagValue (httpConfig) {
var etagValue = httpConfig.etagCache
var etagValueType = typeof etagValue
var etagCacheConfig = {}
// Evaluate function first
if (etagValueType === 'function') {
etagValue = etagValue(httpConfig)
etagValueType = typeof etagValue
}
// Plain, cache, or itemCache objects
if (etagValueType === 'object') {
var id, itemKey
if (etagValue.isCache) {
id = etagValue.info().id
itemKey = generateCacheItemKey(httpConfig)
} else if (etagValue.isItemCache) {
id = etagValue.info().id
itemKey = etagValue.info().itemKey
} else {
id = etagValue.id
itemKey = etagValue.itemKey || generateCacheItemKey(httpConfig)
}
etagCacheConfig.id = id
etagCacheConfig.itemKey = itemKey
} else if (etagValueType === 'string') {
etagCacheConfig.id = etagValue
etagCacheConfig.itemKey = generateCacheItemKey(httpConfig)
} else if (etagValue === true) {
// Undefined cacheId will use the default cacheId as defined in provider
etagCacheConfig.itemKey = generateCacheItemKey(httpConfig)
} else return
return etagCacheConfig
}
function generateCacheItemKey (httpConfig) {
var url = httpConfig.url
var params = stringifyParams(httpConfig.params)
var joiner = ((params && url.indexOf('?') > 0) ? '&' : '?')
var queryString = (params && joiner + params) || ''
return url + queryString
}
// Based on npm package "query-string"
function stringifyParams (obj) {
return obj ? arrayMap(objectKeys(obj).sort(), function (key) {
var val = obj[key]
if (angular.isArray(val)) {
return arrayMap(val.sort(), function (val2) {
return encodeURIComponent(key) + '=' + encodeURIComponent(val2)
}).join('&')
}
return encodeURIComponent(key) + '=' + encodeURIComponent(val)
}).join('&') : ''
}
// http://ejohn.org/blog/partial-functions-in-javascript/
function partial (fn) {
var args = Array.prototype.slice.call(arguments, 1)
return function () {
var arg = 0
for (var i = 0; i < args.length && arg < arguments.length; i++) {
if (args[i] === undefined) args[i] = arguments[arg++]
}
return fn.apply(this, args)
}
}
return $httpDecorator
}