apps-resource-loader
Version:
Chrome Packaged Apps Resource Loader
1,329 lines (1,105 loc) • 34 kB
JavaScript
/**
* @namespace
*/
var RAL = {debug:false};
/**
* Simple max-heap for a priority queue.
*/
RAL.Heap = function() {
this.items = [];
};
RAL.Heap.prototype = {
/**
* Gets the next priority value based on the head's priority.
*/
getNextHighestPriority: function() {
var priority = 1;
if(this.items[0]) {
priority = this.items[0].priority + 1;
}
return priority;
},
/**
* Provides the index of the parent.
*
* @param {number} index The start position.
*/
parentIndex: function(index) {
return Math.floor(index * 0.5);
},
/**
* Provides the index of the left child.
*
* @param {number} index The start position.
*/
leftChildIndex: function(index) {
return index * 2;
},
/**
* Provides the index of the right child.
*
* @param {number} index The start position.
*/
rightChildIndex: function(index) {
return (index * 2) + 1;
},
/**
* Gets the value from a specific position
* in the heap.
*
* @param {number} index The position of the element.
*/
get: function(index) {
var value = null;
if(index >= 1 && this.items[index - 1]) {
value = this.items[index - 1];
}
return value;
},
/**
* Sets a value in the heap.
*
* @param {number} index The position in the heap.
* @param {number} value The value to set.
*/
set: function(index, value) {
this.items[index - 1] = value;
},
/**
* Swaps two values in the heap.
*
* @param {number} indexA Index of the first item to be swapped.
* @param {number} indexB Index of the second item to be swapped.
*/
swap: function(indexA, indexB) {
var temp = this.get(indexA);
this.set(indexA, this.get(indexB));
this.set(indexB, temp);
},
/**
* Sends a value up heap. The item is compared
* to its parent item. If its value is greater
* then it's swapped and the process is repeated.
*
* @param {number} startIndex The start position for the operation.
*/
upHeap: function(startIndex) {
var startValue = null,
parentValue = null,
parentIndex = null,
switched = false;
do {
switched = false;
parentIndex = this.parentIndex(startIndex);
startValue = this.get(startIndex);
parentValue = this.get(parentIndex);
switched = parentValue !== null &&
startValue.priority > parentValue.priority;
if(switched) {
this.swap(startIndex, parentIndex);
startIndex = parentIndex;
}
} while(switched);
},
/**
* Sends a value down heap. The item is compared
* to its two children item. If its value is less
* then it's swapped with the <em>highest value child</em>
* and the process is repeated.
*
* @param {number} startIndex The start position for the operation.
*/
downHeap: function(startIndex) {
var startValue = null,
leftChildValue = null,
rightChildValue = null,
leftChildIndex = null,
rightChildIndex = null,
switchValue = null,
switched = false;
do {
switched = false;
leftChildIndex = this.leftChildIndex(startIndex);
rightChildIndex = this.rightChildIndex(startIndex);
startValue = this.get(startIndex) && this.get(startIndex).priority;
leftChildValue = this.get(leftChildIndex) && this.get(leftChildIndex).priority;
rightChildValue = this.get(rightChildIndex) && this.get(rightChildIndex).priority;
if(leftChildValue === null) {
leftChildValue = Number.NEGATIVE_INFINITY;
}
if(rightChildValue === null) {
rightChildValue = Number.NEGATIVE_INFINITY;
}
switchValue = Math.max(leftChildValue, rightChildValue);
if(startValue < switchValue) {
if(rightChildValue === switchValue) {
this.swap(startIndex, rightChildIndex);
startIndex = rightChildIndex;
} else {
this.swap(startIndex, leftChildIndex);
startIndex = leftChildIndex;
}
switched = true;
}
} while(switched);
},
/**
* Adds a value to the heap. For now this is just
* numbers but a comparator function could be used
* for more complex comparisons.
*
* @param {number} value The value to be added to the heap.
*/
add: function(value) {
this.items.push(value);
this.upHeap(this.items.length);
},
/**
* Removes the head of the heap.
*/
remove: function() {
var value = null;
if(this.items.length) {
// swap with the last child
// in the heap
this.swap(1, this.items.length);
// grab the value and truncate
// the item array
value = this.get(this.items.length);
this.items.length -= 1;
// push the swapped item
// down the heap to wherever it needs
// to sit
this.downHeap(1);
}
return value;
}
};
/**
* Sanitises a file path.
*/
RAL.Sanitiser = (function() {
/**
* Cleans and removes the protocol from the URL.
* @param {string} url The URL to clean.
*/
function cleanURL(url) {
return url.replace(/.*?:\/\//, '', url);
}
return {
cleanURL: cleanURL
};
})();
/**
* Parses cache headers so that we can respect them.
*/
RAL.CacheParser = (function() {
/**
* Parses headers to determine whether there is
* anything in there that we need to know about, e.g.
* no-cache or no-store.
* @param {string} headers The XHR headers to parse.
* @see http://www.mnot.net/cache_docs/
* @returns {object} An object with the extracted expiry for the asset.
*/
function parse(headers) {
var rMaxAge = /max\-age=(\d+)/gi;
var rNoCache = /Cache-Control:.*?no\-cache/gi;
var rNoStore = /Cache-Control:.*?no\-store/gi;
var rMustRevalidate = /Cache-Control:.*?must\-revalidate/gi;
var rExpiry = /Expires:\s(.*)/gi;
var warnings = [];
var expires = rMaxAge.exec(headers);
var useBy = Date.now();
// check for no-store
if(rNoStore.test(headers)) {
warnings.push("Cache-Control: no-store is set");
}
// check for no-cache
if(rNoCache.test(headers)) {
warnings.push("Cache-Control: no-cache is set");
}
// check for must-revalidate
if(rMustRevalidate.test(headers)) {
warnings.push("Cache-Control: must-revalidate is set");
}
// if no max-age is set check
// for an Expires value
if(expires !== null) {
useBy = Date.now() + (expires[1] * 1000);
} else {
// attempt to use the Expires: header
expires = rExpiry.exec(headers);
// if that fails warn
if(expires !== null) {
useBy = Date.parse(expires[1]);
} else {
warnings.push("Cache-Control: max-age and Expires: headers are not set");
}
}
return {
headers: headers,
cacheable: (warnings.length === 0),
useBy: useBy,
warnings: warnings
};
}
return {
parse: parse
};
})();
/**
* FileSystem API wrapper. Makes extensive use of
* the FileSystem code from Eric Bidelman.
*
* @see http://www.html5rocks.com/en/tutorials/file/filesystem/
*/
RAL.FileSystem = (function() {
var ready = false,
readyListeners = [],
root = null,
callbacks = {
/**
* Generic error handler, simply warns the user
*/
onError: function(e) {
var msg = '';
switch (e.code) {
case FileError.QUOTA_EXCEEDED_ERR:
msg = 'QUOTA_EXCEEDED_ERR';
break;
case FileError.NOT_FOUND_ERR:
msg = 'NOT_FOUND_ERR';
break;
case FileError.SECURITY_ERR:
msg = 'SECURITY_ERR';
break;
case FileError.INVALID_MODIFICATION_ERR:
msg = 'INVALID_MODIFICATION_ERR';
break;
case FileError.INVALID_STATE_ERR:
msg = 'INVALID_STATE_ERR';
break;
default:
msg = 'Unknown Error';
break;
}
console.error('Error: ' + msg, e);
},
/**
* Callback for once the file system has been fired up.
* Informs any listeners waiting on it.
*/
onInitialised: function(fs) {
root = fs.root;
ready = true;
if(readyListeners.length) {
var listener = readyListeners.length;
while(listener--) {
readyListeners[listener]();
}
}
}
};
/**
* Determines if the file system is ready for use.
* @returns {boolean} If the file system is ready.
*/
function isReady() {
return ready;
}
/**
* Registers a listener for when the file system is
* ready for interaction.
* @param {Function} listener The listener function.
*/
function registerOnReady(listener) {
readyListeners.push(listener);
}
/**
* Gets the internal file system URL for a stored file
* @param {string} filePath The original URL of the asset.
* @param {Function} callbackSuccess Callback for successful path retrieval.
* @param {Function} callbackFail Callback for failed path retrieval.
*/
function getPath(filePath, callbackSuccess, callbackFail) {
if (ready) {
filePath = RAL.Sanitiser.cleanURL(filePath);
root.getFile(filePath, {}, function(fileEntry) {
callbackSuccess(fileEntry.toURL());
}, callbackFail);
}
}
/**
* Gets the file data as text for a stored file
* @param {string} filePath The original URL of the asset.
* @param {Function} callbackSuccess Callback for successful path retrieval.
* @param {Function} callbackFail Callback for failed path retrieval.
*/
function getDataAsText(filePath, callbackSuccess, callbackFail) {
if (ready) {
filePath = RAL.Sanitiser.cleanURL(filePath);
root.getFile(filePath, {}, function(fileEntry) {
fileEntry.file(function(file) {
var reader = new FileReader();
reader.onloadend = function(evt) {
callbackSuccess(this.result);
};
reader.readAsText(file);
});
}, callbackFail);
}
}
/**
* Puts the file data in the file system.
* @param {string} filePath The original URL of the asset.
* @param {Blob} fileData The file data blob to store.
* @param {Function} callback Callback for file storage.
*/
function set(filePath, fileData, callback) {
if(ready) {
filePath = RAL.Sanitiser.cleanURL(filePath);
var dirPath = filePath.split("/");
dirPath.pop();
// create the directories all the way
// down to the path
createDir(root, dirPath, function() {
// now get a reference to our file, create it
// if necessary
root.getFile(filePath, {create: true}, function(fileEntry) {
// create a writer on the file reference
fileEntry.createWriter(function(fileWriter) {
// catch on file ends
fileWriter.onwriteend = function(e) {
// update the writeend so when we have
// truncated the file data we call the callback
fileWriter.onwriteend = function(e) {
callback(fileEntry.toURL());
};
// now truncate the file contents
// for when we overwrite with a smaller file
fileWriter.truncate(fileData.size);
};
// warn on write fails but right now don't bail
fileWriter.onerror = function(e) {
console.warn('Write failed: ' + e.toString());
};
// start writing
fileWriter.write(fileData);
}, callbacks.onError);
}, callbacks.onError);
});
}
}
/**
* Recursively creates the directories in a path.
* @param {DirectoryEntry} rootDirEntry The base directory for this call.
* @param {Array.<string>} dirs The subdirectories in this path.
* @param {Function} onCreated The callback function to use when all
* directories have been created.
*/
function createDir(rootDirEntry, dirs, onCreated) {
// remove any empty or dot dirs
if(dirs[0] === '.' || dirs[0] === '') {
dirs = dirs.slice(1);
}
// on empty call this done
if(!dirs.length) {
onCreated();
} else {
// create the subdirectory and recursively call
rootDirEntry.getDirectory(dirs[0], {create: true}, function(dirEntry) {
if (dirs.length) {
createDir(dirEntry, dirs.slice(1), onCreated);
}
}, callbacks.onError);
}
}
/**
* Removes a directory.
* @param {string} path The directory to remove.
* @param {Function} onRemoved The callback for successful deletion.
* @param {Function} onCreated The callback for failed deletion.
*/
function removeDir(path, onRemoved, onError) {
if(ready) {
root.getDirectory(path, {}, function(dirEntry) {
dirEntry.removeRecursively(onRemoved, callbacks.onError);
}, onError || callbacks.onError);
}
}
/**
* Removes a file.
* @param {string} path The file to remove.
* @param {Function} onRemoved The callback for successful deletion.
* @param {Function} onCreated The callback for failed deletion.
*/
function removeFile(path, onRemoved, onError) {
if(ready) {
root.getFile(path, {}, function(fileEntry) {
fileEntry.remove(onRemoved, callbacks.onError);
}, onError || callbacks.onError);
}
}
/**
* Initializes the file system.
* @param {number} storageSize The storage size in MB.
*/
(function init(storageSize) {
storageSize = storageSize || 10;
window.requestFileSystem = window.requestFileSystem ||
window.webkitRequestFileSystem;
window.resolveLocalFileSystemURL = window.resolveLocalFileSystemURL ||
window.webkitResolveLocalFileSystemURL;
if(!!window.requestFileSystem) {
window.requestFileSystem(
window.TEMPORARY,
storageSize * 1024 * 1024,
callbacks.onInitialised,
callbacks.onError);
} else {
}
})();
return {
isReady: isReady,
registerOnReady: registerOnReady,
getPath: getPath,
getDataAsText: getDataAsText,
set: set,
removeFile: removeFile,
removeDir: removeDir
};
})();
/**
* Represents the internal log of all files that
* have been cached for offline use.
*/
RAL.FileManifest = (function() {
var manifest = null,
ready = false,
readyListeners = [],
saving = false,
hasUpdated = false;
/**
* Determines if the manifest is ready for use.
* @returns {boolean} If the manifest is ready.
*/
function isReady() {
return ready;
}
/**
* Registers a listener for when the manifest is
* ready for interaction.
* @param {Function} listener The listener function.
*/
function registerOnReady(listener) {
readyListeners.push(listener);
}
/**
* Gets the file information from the manifest.
* @param {string} src The source URL of the asset.
* @param {Function} callback The callback function to
* call with the asset details.
*/
function get(src, callback) {
var cleanSrc = RAL.Sanitiser.cleanURL(src);
var fileInfo = manifest[cleanSrc] || null;
callback(fileInfo);
}
/**
* Sets the file information in the manifest.
* @param {string} src The source URL of the asset.
* @param {object} info The information to store against the asset.
* @param {Function} callback The callback function to
* call once the asset details have been saved.
*/
function set(src, info, callback) {
var cleanSrc = RAL.Sanitiser.cleanURL(src);
manifest[cleanSrc] = info;
save(callback);
}
/**
* Resets the manifest of files.
*/
function reset() {
manifest = {};
save();
}
/**
* Callback for when there is no manifest available.
* @private
*/
function onManifestUnavailable() {
onManifestLoaded("{}");
}
/**
* Callback for when there is no manifest has laded. Passes through the
* registered ready callbacks and fires each in turn.
* @param {string} The JSON representation of the manifest.
* @private
*/
function onManifestLoaded(fileData) {
ready = true;
manifest = JSON.parse(fileData);
if (readyListeners.length) {
var listener = readyListeners.length;
while(listener--) {
readyListeners[listener]();
}
}
}
/**
* Saves the manifest file.
* @private
*/
function save(callback) {
var blob = new Blob([
JSON.stringify(manifest)
], {type: 'application/json'});
// Called whether or not the file exists
RAL.FileSystem.set("manifest.json", blob, function() {
if(!!callback) {
callback();
}
});
}
/**
* Requests the manifest JSON file from the file system.
*/
function init() {
RAL.FileSystem.getDataAsText("manifest.json",
onManifestLoaded,
onManifestUnavailable);
}
// check if the file system is good to go. If not, then
// flag that we want to know when it is.
if (RAL.FileSystem.isReady()) {
init();
} else {
RAL.FileSystem.registerOnReady(init);
}
return {
isReady: isReady,
registerOnReady: registerOnReady,
get: get,
set: set,
reset: reset
};
})();
/**
* Prototype for all remote files
*/
RAL.RemoteFile = function() {};
RAL.RemoteFile.prototype = {
/**
* The internal element used for dispatching events.
* @type {Element}
*/
element: null,
/**
* The source URL of the remote file.
* @type {string}
*/
src: null,
/**
* Whether or not this file should autoload.
* @type {boolean}
*/
autoLoad: false,
/**
* Whether or not this file should ignore the server's cache headers.
* @type {boolean}
*/
ignoreCacheHeaders: false,
/**
* The default TTL (in ms) should no expire header be provided.
* @type {number}
* @default 1209600000, 14 days
*/
timeToLive: 14 * 24 * 60 * 60 * 1000,
/**
* The file's priority in the queue.
* @type {number}
* @default 0
*/
priority: 0,
/**
* Whether or not the file has loaded.
* @type {boolean}
*/
loaded: false,
/**
* A reference to the URL object.
* @type {Function}
* @private
*/
wURL: window.URL || window.webkitURL,
callbacks: {
/**
* Callback for errors with caching this file, e.g. when the headers
* from the server imply as such.
* @param {object} fileInfo The file information including headers and
* warnings from RAL.CacheParser
*/
onCacheError: function(fileInfo) {
fileInfo.src = this.src;
this.sendEvent('cacheerror', fileInfo);
},
/**
* Callback for when the remote file has loaded.
* @param {Blob} fileData The file's data.
* @param {object} fileInfo The file information including headers and
* any warnings from RAL.CacheParser
*/
onRemoteFileLoaded: function(fileData, fileInfo) {
// check the ignorance status
if(this.ignoreCacheHeaders) {
fileInfo.cacheable = true;
fileInfo.useBy += this.timeToLive;
}
// check if the file can be cached and, if so,
// go ahead and store it in the file system
if(fileInfo.cacheable) {
RAL.FileSystem.set(this.src, fileData,
this.callbacks.onFileSystemSet.bind(this, fileInfo));
} else {
var dataURL = this.wURL.createObjectURL(fileData);
this.callbacks.onLocalFileLoaded.call(this, dataURL);
this.callbacks.onCacheError.call(this, fileInfo);
}
this.sendEvent('remoteloaded', fileInfo);
},
/**
* Called when the remote file is unavailable.
*/
onRemoteFileUnavailable: function() {
this.sendEvent('remoteunavailable');
},
/**
* Called when the local file has been loaded. Since the
* remote file will be stored as a local file, this should
* always be fired, but it may be preceded by a 'remoteloaded'
* event beforehand.
* @param {string} filePath The local file system path of the file.
*/
onLocalFileLoaded: function(filePath) {
this.loaded = true;
this.sendEvent('loaded', filePath);
},
/**
* Called when the locally stored version of the file is
* not available, e.g. after a file system purge. This
* automatically requests the remote file again and shows
* a placeholder.
*/
onLocalFileUnavailable: function() {
this.showPlaceholder();
this.loadFromRemote();
this.sendEvent('localunavailable');
},
/**
* Callback for once the file has been stored in the file system. Stores
* the file in the global manifest.
* @param {object} fileInfo The source URL and headers for the remote file.
*/
onFileSystemSet: function(fileInfo) {
RAL.FileManifest.set(this.src, fileInfo,
this.callbacks.onFileManifestSet.bind(this));
},
/**
* Callback for once the file has been stored in the file manifest.
*/
onFileManifestSet: function() {
// we stored the file, we should reattempt
// the load operation
this.load();
},
/**
* Callback for once the file has been retrieved from the file manifest.
* @param {object} fileInfo The file's information from the manifest.
*/
onFileManifestGet: function(fileInfo) {
var time = Date.now();
// see whether we have the file
if(fileInfo !== null) {
// see if it is still in date or we are offline
if(fileInfo.useBy > time || !RAL.NetworkMonitor.isOnline()) {
// go and grab it
RAL.FileSystem.getPath(this.src,
this.callbacks.onLocalFileLoaded.bind(this),
this.callbacks.onLocalFileUnavailable.bind(this));
} else {
// it's out of date, so now we
// need to get the remote file
this.loadFromRemote();
}
} else {
// we do not have the file, go
// and get it
this.loadFromRemote();
}
}
},
/**
* Helper function which uses the internal DOM element
* to create and dispatch events for the remote file.
* @see https://developer.mozilla.org/en-US/docs/DOM/document.createEvent
* @param {string} evtName The event name.
* @param {*} data The event data.
*/
sendEvent: function(evtName, data) {
this.checkForElement();
var evt = document.createEvent("Event");
evt.initEvent(evtName, true, true);
if(!!data) {
evt.data = data;
}
this.element.dispatchEvent(evt);
},
/**
* Loads a file from a remote source.
*/
loadFromRemote: function() {
RAL.Loader.load(this.src,
'blob',
this.callbacks.onRemoteFileLoaded.bind(this),
this.callbacks.onRemoteFileUnavailable.bind(this));
this.sendEvent('remoteloadstart');
},
/**
* Attempts to load a file from the local file system.
*/
load: function() {
// check the "manifest" to see if
// we should already have this file
RAL.FileManifest.get(this.src, this.callbacks.onFileManifestGet.bind(this));
},
/**
* Checks for and creates an element for handling events.
*/
checkForElement: function() {
if(!this.element) {
// create a placeholder element
// in lieu of having an actual one. Likely
// to be the case where someone has created
// a RemoteFile directly
this.element = document.createElement('span');
}
},
/**
* Wrapper for event listening on the internal element.
* @param {string} evtName The event name to listen for.
* @param {Function} callback The event callback.
* @param {boolean} useCapture Use capture for the event.
*/
addEventListener: function(evtName, callback, useCapture) {
this.checkForElement();
this.element.addEventListener(evtName, callback, useCapture);
}
};
/**
* Represents a remote image.
* @param {object} options The configuration options.
*/
RAL.RemoteImage = function(options) {
// make sure to override the prototype
// refs with the ones for this instance
RAL.RemoteFile.call(this);
options = options || {};
this.element = options.element || document.createElement('img');
this.src = this.element.dataset.src || options.src;
this.width = this.element.width || options.width || null;
this.height = this.element.height || options.height || null;
this.placeholder = this.element.dataset.placeholder || null;
this.priority = options.priority || 0;
// attach on specific events for images
this.addEventListener('remoteloadstart', this.showPlaceholder.bind(this));
this.addEventListener('loaded', this.showImage.bind(this));
if(typeof options.autoLoad !== "undefined") {
this.autoLoad = options.autoLoad;
}
if(typeof options.ignoreCacheHeaders !== "undefined") {
this.ignoreCacheHeaders = options.ignoreCacheHeaders;
}
// if there is a TTL use that instead of the default
if(this.ignoreCacheHeaders && typeof this.timeToLive !== "undefined") {
this.timeToLive = options.timeToLive;
}
if(this.autoLoad) {
this.load();
} else {
this.showPlaceholder();
}
};
RAL.RemoteImage.prototype = new RAL.RemoteFile();
/**
* Shows a placeholder image while we load in the main image
*/
RAL.RemoteImage.prototype.showPlaceholder = function() {
if(this.placeholder !== null) {
// add in transitions
this.element.style['-webkit-transition'] = "background-image 0.5s ease-out";
this.showImage({data:this.placeholder});
}
};
/**
* Shows the image.
* @param {event} evt The loaded event for the asset.
*/
RAL.RemoteImage.prototype.showImage = function(evt) {
var imageSrc = evt.data;
var image = new Image();
var revoke = (function(imageSrc) {
this.wURL.revokeObjectURL(imageSrc);
}).bind(this, imageSrc);
var imageLoaded = function() {
// infer the size from the image
var width = this.width || image.naturalWidth;
var height = this.height || image.naturalHeight;
this.element.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
this.element.style.width = width + 'px';
this.element.style.height = height + 'px';
this.element.style.backgroundImage = 'url(' + imageSrc + ')';
this.element.style.backgroundSize = width + 'px ' + height + 'px';
// if it's a blob make sure we go ahead
// and revoke it properly after a short timeout
if(/blob:/.test(imageSrc)) {
setTimeout(revoke, 100);
}
};
image.onload = imageLoaded.bind(this);
image.src = imageSrc;
};
/**
* Loads the remote files
*/
RAL.Loader = (function() {
var callbacks = {
/**
* Callback for loaded files.
* @param {string} source The remote file's URL.
* @param {Function} callbackSuccess The callback for successful loading.
* @param {Function} callbackError The callback for failed loading.
* @param {ProgressEvent} xhrProgressEvent The XHR progress event.
*/
onLoad: function(source, callbackSuccess, callbackError, xhrProgressEvent) {
// we have the file details
// so now we need to wrap the file up, including
// the caching information to return back
var xhr = xhrProgressEvent.target;
var fileData = xhr.response;
var fileInfo = RAL.CacheParser.parse(xhr.getAllResponseHeaders());
if(xhr.readyState === 4) {
if(xhr.status === 200) {
callbackSuccess(fileData, fileInfo);
} else {
callbackError(xhrProgressEvent);
}
}
},
/**
* Generic callback for erroring loads. Simply passes the progres event
* through to the assigned callback.
* @param {Function} callback The callback for failed loading.
* @param {ProgressEvent} xhrProgressEvent The XHR progress event.
*/
onError: function(callback, xhrProgressEvent) {
callback(xhrProgressEvent);
}
};
/**
* Aborts an in-flight XHR and reschedules it.
* @param {XMLHttpRequest} xhr The XHR to abort.
* @param {Function} callbackSuccess The callback for successful loading.
* @param {Function} callbackError The callback for failed loading.
* @param {ProgressEvent} xhrProgressEvent The XHR progress event.
*/
function abort(xhr, source, callbackSuccess, callbackFail) {
// kill the current request
xhr.abort();
// run it again, which will cause us to schedule up
this.load(source, callbackSuccess, callbackFail);
}
/**
* Aborts an in-flight XHR and reschedules it.
* @param {XMLHttpRequest} xhr The XHR to abort.
* @param {string} type The response type for the XHR, e.g. 'blob'
* @param {Function} callbackSuccess The callback for successful loading.
* @param {Function} callbackError The callback for failed loading.
*/
function load(source, type, callbackSuccess, callbackFail) {
// check we're online, or schedule the load
if(RAL.NetworkMonitor.isOnline()) {
// attempt to load the file
var xhr = new XMLHttpRequest();
xhr.onerror = callbacks.onError.bind(this, callbackFail);
xhr.onload = callbacks.onLoad.bind(this, source, callbackSuccess, callbackFail);
xhr.open('GET', source, true);
xhr.responseType = type;
xhr.send();
// register our interest in the connection
// being cut. If that happens we will reschedule.
RAL.NetworkMonitor.registerForOffline(
abort.bind(this,
xhr,
source,
callbackSuccess,
callbackFail));
} else {
// We are offline so register our interest in the
// connection being restored.
RAL.NetworkMonitor.registerForOnline(
load.bind(this,
source,
callbackSuccess,
callbackFail));
}
}
return {
load: load
};
})();
/**
* Tracks the online / offline nature of the
* browser so we can abort and reschedule any
* in-flight requests.
*/
RAL.NetworkMonitor = (function() {
var onlineListeners = [];
var offlineListeners = [];
/* Register for online events */
window.addEventListener("online", function() {
// go through each listener, pop it
// off and call it
var listenerCount = onlineListeners.length,
listener = null;
while(listenerCount--) {
listener = onlineListeners.pop();
listener();
}
});
/* Register for offline events */
window.addEventListener("offline", function() {
// go through each listener, pop it
// off and call it
var listenerCount = offlineListeners.length,
listener = null;
while(listenerCount--) {
listener = offlineListeners.pop();
listener();
}
});
/**
* Appends a function for notification
* when the browser comes back online.
* @param callback The callback function for online notifications.
*/
function registerForOnline(callback) {
onlineListeners.push(callback);
}
/**
* Appends a function for notification
* when the browser drops offline.
* @param callback The callback function for offline notifications.
*/
function registerForOffline(callback) {
offlineListeners.push(callback);
}
/**
* Simple wrapper for whether the browser
* is online or offline.
* @returns {boolean} The online / offline state of the browser.
*/
function isOnline() {
return window.navigator.onLine;
}
return {
registerForOnline: registerForOnline,
registerForOffline: registerForOffline,
isOnline: isOnline
};
})();
/**
* Represents the load queue for assets.
*/
RAL.Queue = (function() {
var heap = new RAL.Heap(),
connections = 0,
maxConnections = 6,
callbacks = {
/**
* Callback for when a file in the queue has been loaded
*/
onFileLoaded: function() {
if(RAL.debug) {
console.log("[Connections: " + connections + "] - File loaded");
}
connections--;
start();
}
};
/**
* Gets the queue's next priority value.
*/
function getNextHighestPriority() {
return heap.getNextHighestPriority();
}
/**
* Sets the queue's maximum number of concurrent requests.
* @param {number} newMaxConnections The maximum number of in-flight requests.
*/
function setMaxConnections(newMaxConnections) {
maxConnections = newMaxConnections;
}
/**
* Adds a file to the queue.
* @param {RAL.REmoteFile} remoteFile The file to enqueue.
* @param {boolean} startGetting Whether or not to try and get immediately.
*/
function add(remoteFile, startGetting) {
// ensure we have a priority, and
// go with a LIFO approach
// - thx courage@
if(typeof remoteFile.priority === "undefined") {
remoteFile.priority = heap.getNextHighestPriority();
}
heap.add(remoteFile);
if(startGetting) {
start();
}
}
/**
* Start requesting items from the queue.
*/
function start() {
while(connections < maxConnections) {
nextFile = heap.remove();
if(nextFile !== null) {
nextFile.addEventListener('loaded', callbacks.onFileLoaded);
nextFile.load();
if(RAL.debug) {
console.log("[Connections: " + connections + "] - Loading " + nextFile.src);
}
connections++;
} else {
if(RAL.debug) {
console.log("[Connections: " + connections + "] - No more images queued");
}
break;
}
}
}
/**
* Clears the queue.
*/
function clear() {
heap.clear();
}
return {
getNextHighestPriority: getNextHighestPriority,
setMaxConnections: setMaxConnections,
add: add,
clear: clear,
start: start
};
})();