/**
* Assets loader: Basically a wrap around $.ajax in order
* to priorize and serialize resource loading.
*
* @fileoverview Assets Loader, wrap around $.ajax
*
* @author Deux Huit Huit <https://deuxhuithuit.com>
* @license MIT <https://deuxhuithuit.mit-license.org>
* @namespace loader
* @memberof App
* @requires App
*/
(function ($, global, undefined) {
'use strict';
// Forked: https://gist.github.com/nitriques/6583457
(function addXhrProgressEvent () {
var originalXhr = $.ajaxSettings.xhr;
$.ajaxSetup({
progress: $.noop,
upload: $.noop,
xhr: function () {
var self = this;
var req = originalXhr();
if (req) {
if ($.isFunction(req.addEventListener)) {
req.addEventListener('progress', function (e) {
self.progress($.Event(e)); // make sure it's jQuery-ize
}, false);
}
if (!!req.upload && $.isFunction(req.upload.addEventListener)) {
req.upload.addEventListener('progress', function (e) {
self.upload($.Event(e)); // make sure it's jQuery-ize
}, false);
}
}
return req;
}
});
})();
var assets = []; // FIFO
var loaderIsWorking = false;
var currentUrl = null;
/**
* Check if a given url is loading (Only GET request)
* @name isLoading
* @method
* @memberof loader
* @param {Object} url Url object to check
* @returns {Boolean}
* @private
*/
var isLoading = function (url) {
if (!$.isPlainObject(url)) {
url = {url: url};
}
if (!!url.method && url.method !== 'GET') {
return false;
}
return !!currentUrl && currentUrl === url.url;
};
/**
* Check if a given url is in the queue
* @name inQueue
* @method
* @memberof loader
* @param {Object} url Url object to check
* @returns {Boolean}
* @private
*/
var inQueue = function (url) {
return assets.findIndex(function eachAsset (asset) {
return asset.url === url;
});
};
/**
* Return the appropriate storage engine for the given url
* @name getStorageEngine
* @method
* @memberof loader
* @param {Object} url Url object to check
* @private
*/
var getStorageEngine = function (url) {
if (url.cache === true) {
url.cache = 'session';
}
return global.App.storage && global.App.storage[url.cache];
};
// This breaks the call dependency cycle
var recursiveLoad = $.noop;
var loadAsset = $.noop;
var defaultParameters = function (asset) {
return {
progress: function (e) {
// callback
App.callback(asset.progress, [e]);
},
success: function (data, textStatus, jqXHR) {
// clear pointer
currentUrl = null;
// register next
recursiveLoad();
// callback
App.callback(asset.success, [data, textStatus, jqXHR]);
// store in cache
if (!!asset.cache) {
var storage = getStorageEngine(asset);
if (!!storage) {
storage.set(asset.url, data);
}
}
},
error: function (jqXHR, textStatus, errorThrown) {
var maxRetriesFactor = !!asset.vip ? 2 : 1;
// clear pointer
currentUrl = null;
App.log({
fx: 'error',
args: ['Error loading url %s: %s', asset.url, textStatus + ' ' + errorThrown],
me: 'Loader'
});
// if no vip access is granted
//if (!asset.vip) {
// decrease priority
// this avoids looping for a unload-able asset
asset.priority += ++asset.retries; // out of bounds checking is done later
//}
// Check the error code:
// No need to replay client errors (4xx)
var clientError = jqXHR.status >= 400 && jqXHR.status < 500;
if (!!clientError) {
// Early exit
App.callback(asset.clienterror, [jqXHR.status, jqXHR, textStatus, errorThrown]);
// if we already re-tried less than x times
} else if (asset.retries <= (asset.maxRetries * maxRetriesFactor)) {
// push it back into the queue and retry
loadAsset(asset);
} else {
// we give up!
App.callback(asset.giveup, [jqXHR, textStatus, errorThrown]);
}
// next
recursiveLoad();
// callback
App.callback(asset.error, [jqXHR, textStatus, errorThrown]);
}
};
};
/**
* Load the first item in the queue
* @name loadOneAsset
* @method
* @private
* @memberof loader
*/
var loadOneAsset = function () {
// grab first item
var asset = assets.shift();
// extend it
var param = $.extend({}, asset, defaultParameters(asset));
// actual loading
$.ajax(param);
// set the pointer
currentUrl = param.url;
};
/**
* Trigger loadOneAsset as long as there's entries in the queue
* @name recursiveLoad
* @method
* @memberof loader
* @private
*/
recursiveLoad = function () {
if (!!assets.length) {
// start next one
setTimeout(loadOneAsset, 0);
} else {
// work is done
loaderIsWorking = false;
}
};
/**
* Validate and format url's data
* @name valideUrlArags
* @method
* @memberof loader
* @private
* @param {Object} url Url object
* @param {Integer} priority Priority of the url
* @returns {Object} Url object
*/
var validateUrlArgs = function (url, priority) {
// ensure we are dealing with an object
if (!$.isPlainObject(url)) {
url = {url: url};
}
// pass the priority param into the object
if ($.isNumeric(priority) && Math.abs(priority) < assets.length) {
url.priority = priority;
}
// ensure that the priority is valid
if (!$.isNumeric(url.priority) || Math.abs(url.priority) > assets.length) {
url.priority = assets.length;
}
// ensure we have a value for the retries
if (!$.isNumeric(url.retries)) {
url.retries = 0;
}
if (!$.isNumeric(url.maxRetries)) {
url.maxRetries = 2;
}
return url;
};
/**
* Trigger the loading if nothing is happening
* @name launchLoad
* @method
* @private
* @memberof loader
*/
var launchLoad = function () {
// start now if nothing is loading
if (!loaderIsWorking) {
loaderIsWorking = true;
loadOneAsset();
App.log({fx: 'info', args: 'Load worker has been started', me: 'Loader'});
}
};
/**
* Get the value from the cache if it's available
* @name getValueFromCache
* @method
* @memberof loader
* @param {Object} url Url object
* @returns {Boolean}
* @private
*/
var getValueFromCache = function (url) {
var storage = getStorageEngine(url);
if (!!storage) {
var item = storage.get(url.url);
if (!!item) {
// if the cache-hit is valid
if (App.callback(url.cachehit, [item]) !== false) {
// return the cache
App.callback(url.success, [item]);
return true;
}
}
}
return false;
};
/**
* Update a request priority in the queue
* @name updatePriority
* @method
* @memberof loader
* @private
* @param {Object} url Url object
* @param {Integer} index
*/
var updatePriority = function (url, index) {
// promote if new priority is different
var oldAsset = assets[index];
if (oldAsset.priority != url.priority) {
// remove
assets.splice(index, 1);
// add
assets.splice(url.priority, 1, url);
}
App.log({
args: [
'Url %s was shifted from %s to %s',
url.url,
oldAsset.priority, url.priority
],
me: 'Loader'
});
};
/**
* Put the request in the queue and trigger the load
* @name loadAsset
* @method
* @memberof loader
* @private
* @param {Object} url Url Object
* @param {Integer} priority
* @this App
* @returns this
*/
loadAsset = function (url, priority) {
if (!url) {
App.log({fx: 'error', args: 'No url given', me: 'Loader'});
return this;
}
url = validateUrlArgs(url, priority);
// ensure that asset is not current
if (isLoading(url)) {
App.log({fx: 'error', args: ['Url %s is already loading', url.url], me: 'Loader'});
return this;
}
// check cache
if (!!url.cache) {
if (getValueFromCache(url)) {
return this;
}
}
var index = inQueue(url.url);
// ensure that asset is not in the queue
if (!~index) {
// insert in array
assets.splice(url.priority, 1, url);
App.log({
fx: 'info',
args: ['Url %s has been insert at %s', url.url, url.priority],
me: 'Loader'
});
} else {
updatePriority(url, index);
}
launchLoad();
return this;
};
global.App = $.extend(true, global.App, {
loader: {
/**
* Put the request in the queue and trigger the load
* @name load
* @method
* @memberof loader
* @public
* @param {Object} url Url Object
* @param {Integer} priority
* @this App
* @returns this
*/
load: loadAsset,
/**
* Check if a given url is loading (Only GET request)
* @name isLoading
* @method
* @memberof loader
* @param {Object} url Url object to check
* @returns {Boolean}
* @public
*/
isLoading: isLoading,
/**
* Check if a given url is in the queue
* @name inQueue
* @method
* @memberof loader
* @param {Object} url Url object to check
* @returns {Boolean}
* @public
*/
inQueue: inQueue,
/**
* Get the flag if the loader is working or not
* @name working
* @method
* @memberof loader
* @public
* @returns {Boolean}
*/
working: function () {
return loaderIsWorking;
}
}
});
})(jQuery, window);