loader.js

/**
 *  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
 */
(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) {
		var foundIndex = -1;
		$.each(assets, function eachAsset (index, asset) {
			if (asset.url === url) {
				foundIndex = index;
				return false; // early exit
			}
			return true;
		});
		return foundIndex;
	};
	
	/**
	 * 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.AppStorage && global.AppStorage[url.cache];
	};
	
	// This breaks the call dependency cycle
	var recursiveLoad = $.noop;
	var loadAsset = $.noop;
	
	var defaultParameters = function (asset) {
		return {
			progress: function () {
				// callback
				App.callback.call(this, asset.progress, arguments);
			},
			success: function (data) {
				// clear pointer
				currentUrl = null;
				
				// register next
				recursiveLoad();
				
				// callback
				App.callback.call(this, asset.success, arguments);
				
				// store in cache
				if (!!asset.cache) {
					var storage = getStorageEngine(asset);
					if (!!storage) {
						storage.set(asset.url, data);
					}
				}
			},
			error: function () {
				var maxRetriesFactor = !!asset.vip ? 2 : 1;
				
				// clear pointer
				currentUrl = null;
				
				App.log({args: ['Error loading url %s', asset.url], 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
				//}
				
				// @todo: check for the error code
				// and do something smart with it
				// 404 will sometimes wait for timeout, so it's better to skip it fast
				
				// if we already re-tried  less than x times
				if (asset.retries <= (asset.maxRetries * maxRetriesFactor)) {
					// push it back into the queue and retry
					loadAsset(asset);
				} else {
					// we give up!
					App.callback.call(this, asset.giveup, arguments);
				}
				
				// next
				recursiveLoad();
				
				// callback
				App.callback.call(this, asset.error, arguments);
			}
		};
	};
	
	/**
	 * 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
			loadOneAsset();
		} 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({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.call(this, url.cachehit, item) !== false) {
					// return the cache
					App.callback.call(this, 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 updatePrioriy = 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({args: 'No url given', me: 'Loader'});
			return this;
		}
		
		url = validateUrlArgs(url, priority);
		
		// ensure that asset is not current
		if (isLoading(url)) {
			App.log({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({args: ['Url %s has been insert at %s', url.url, url.priority], me: 'Loader'});
			
		} else {
			updatePrioriy(url, index);
		}
		
		launchLoad();
		
		return this;
	};
	
	global.Loader = $.extend(global.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);