headjs/headjs — pull request #259 by CommanderQ (2013-09-11 21:09:56+00:00)
Title: Promise-based with multiple resource sources, automatic fail-over, and "loaded " tests

This is a significant rewrite of the existing loader to use a promise-based mechanism for loading resources and provide more reliable resource loading features with minimal increase in size. The major changes are as follows:

1) Promise-based loading: the loader now utilizes the "Promises, Promises..." library functionality to better represent the resource loading process and support dependency management and sequencing much more meaningfully. This provides notification for both success and failure, which improves reliability and error handling scenarios, as well as the other features that were added. The return value of calls to "load" and "ready" still return the headJs API object, but it's enhanced as a promise (i.e. it possesses a "then" method) that represents when the script or scripts have completed or failed. When groups of scripts are specified, the returned promise represents the completion of the group, or failure of any member; when one resource is specified, the promise represents only that resource.

2) Multiple fail-over sources for a resource: When specifying a resource using the object syntax (i.e. "{name: location}", you can now use the following syntax with any number of locations: "{name: [location1, location2, location3]}" to specify fail-over locations. When this is preference-ordered list is provided, the failure to load the resource from one location will automatically cause it to try the next, until it either succeeds or runs out of locations. The promise returned by the "load" call that uses this syntax is fulfilled if the resource is loaded from any location, and is rejected if it cannot be loaded from any location. Because "success" isn't well-reported by older browsers, I suggest using the syntax outlined in feature 3 to ensure proper detection. This syntax change does not affect the existing ways to define a resource.

3) Testing for successful resource loading: As we're all aware, older browsers don't support reporting resource load errors well, leading to false positives. When specifying a resource using the object syntax "{name: [location1, location2, location3]}", you can include a "load test" predicate within the array of locations to verify that a resource was loaded - this makes the syntax "{name: [location1, location2, location3, loadTest]}". Using jQuery as an example: "{jQuery: ['http://jquery.com/jquery.js', function(){ return !!window.jQuery; }]}" loads jQuery from the specified location(s), running the "load test" when the browser reports loading is complete to verify successful loading. Providing a "load test" predicate ensures that the resource is loaded, regardless of the age of the browser, enhancing the utility and reliability of the fail-over feature. The test predicate can be anywhere in the array of sources for a resource. If no "load test" is provided, the behavior is exactly the same as the existing headJs implementation in that a script is assumed to have loaded unless the browser reports an error (which many older ones do not).

4) Lots of additional unit tests: In revising the existing code, I encountered scenarios where the tests were either too simple or made implicit assumptions about execution order (which were valid for the existing version). With the introduction of promises and asynchronous execution, I had to revise the tests and expanded their breadth significantly.

5) The objects from the "Promises, Promises..." project / library are exposed via the head.Deferred and head.Promise objects for reuse.

6) "ready" method uses promises: Calls to "ready" still return the API, but are enhanced as promises when they match existing assets. If no arguments are provided, the returned promise represents the completion of all currently-known resources being loaded. This is a slight semantic change that better represents overall readiness. All other features remain unchanged.

7) Pre-loading: Pre-loading via hackish mechanisms for older browsers is now more cleanly supported and leverages fail-over sources for resources. Pre-loading is treated as best-effort caching while other scripts load, which is the design goal.

8) Cleaner DOM: Failed scripts are now removed from the DOM when their failure is detected. All pre-loaded / cached elements are also removed when their underlying resource is loaded. It's not necessary, but it's tidier.

The goal was to make take into account fail-over automatically ,and it blossomed into this rewrite - there are only around 2 dozen lines of the original code. But this is still compatible with the headJs API, making it a viable enhancement when more reliable asynchronous script loading and fail-over are desired. Please send feedback. :-)