Skip to content

Instantly share code, notes, and snippets.

@trentmwillis
Last active July 19, 2016 16:57
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save trentmwillis/d47c34fdab04950bf6fd733f625e71a9 to your computer and use it in GitHub Desktop.
Save trentmwillis/d47c34fdab04950bf6fd733f625e71a9 to your computer and use it in GitHub Desktop.
Ember Asset Loading Service
import RSVP from 'rsvp';
import Ember from 'ember';
const RETRY_LOAD = Symbol('RETRY_LOAD');
function createLoadElement(tag, load, error) {
const el = document.createElement(tag);
el.onload = load;
el.onerror = error;
return el;
}
class LoadError extends Error {
constructor(message, assetLoader) {
this.message = message;
this.loader = assetLoader;
}
}
class AssetLoadError extends LoadError {
constructor(message, assetLoader, asset) {
super(...arguments);
this.asset = asset;
}
retryLoad() {
return assetLoader.loadAsset(this.asset, RETRY_LOAD);
}
}
class BundleLoadError extends LoadError {
constructor(message, assetLoader, bundleName, errors) {
super(...arguments);
this.bundle = bundleName;
this.errors = errors;
}
retryLoad() {
return assetLoader.loadBundle(this.bundle, RETRY_LOAD);
}
}
const AssetLoader = Ember.Service.extend({
/**
* Init the __bundlePromises and __assetPromises caches to empty objects.
*
* @override
*/
init() {
this._setupCache();
},
/**
* @public
* @method setManifest
* @param {AssetManifest} manifest
* @return {AssetManifest} manifest
*/
setManifest(manifest) {
this.__manifest = manifest;
},
/**
* Loads a bundle by fetching all of its assets and its dependencies.
*
* Returns a Promise that resolve when all assets have been loaded or rejects
* when one of the assets fails to load. Subsequent calls will return the same
* Promise.
*
* @public
* @method loadBundle
* @param {String} name
* @return {Promise} bundlePromise
*/
loadBundle(name, retryLoad) {
const cachedPromise = this._getFromCache('bundle', name, retryLoad === RETRY_LOAD);
if (cachedPromise) {
return cachedPromise;
}
const bundle = this._getBundle(name);
const dependencies = bundle.dependencies || [];
const dependencyPromises = dependencies.map((dependency) => this.loadBundle(dependency, retryLoad));
const assets = bundle.assets || [];
const assetPromises = assets.map((asset) => this.loadAsset(asset, retryLoad));
const bundlePromise = RSVP.allSettled([ ...dependencyPromises, ...assetPromises ]);
const bundleWithFail = bundlePromise.then((promises) => {
const rejects = promises.filter((promise) => promise.state === 'rejected');
const errors = rejects.map((reject) => reject.reason);
if (errors.length) {
throw new BundleLoadError('Bundle failed to load', this, name, errors);
}
return name;
});
return this._setInCache('bundle', name, bundleWithFail);
},
_setupCache() {
this.__cache = {};
this.__cache.asset = {};
this.__cache.bundle = {};
},
_getFromCache(type, key, evict) {
if (evict) {
this.__cache[type][key] = undefined;
return;
}
return this.__cache[type][key];
},
_setInCache(type, key, value) {
this.__cache[type][key] = value;
},
/**
* @public
* @method loadAsset
* @param {Object} asset
* @param {String} asset.uri
* @param {String} asset.type
* @return {Promise} assetPromise
*/
loadAsset({ uri, type }, retryLoad) {
const cacheKey = `${type}:${uri}`;
const cachedPromise = this._getFromCache('asset', key, retryLoad === RETRY_LOAD);
if (cachedPromise) {
return cachedPromise;
}
const loader = this._getAssetLoader(type);
const assetPromise = loader(uri);
const assetWithFail = assetPromise.catch((error) => throw new AssetLoadError(error.message, this, { uri, type }));
return this._setInCache('asset', cacheKey, assetWithFail);
},
/**
* @private
* @method getManifest
* @return {AssetManifest} manifest
*/
getManifest() {
const manifest = this.__manifest;
Ember.assert('No asset manifest found. Ensure you call setManifest before attempting to use the AssetLoader.', manifest);
return manifest;
},
/**
* @private
* @method _getBundle
* @param {String} name
* @return {Bundle} bundle
*/
_getBundle(name) {
const manifest = this.getManifest();
const bundles = manifest.bundles;
Ember.assert('Asset manifest does not list any available bundles.', bundles);
const bundle = bundles[name];
Ember.assert(`No bundle with name '${name}' exists in the asset manifest.`, bundle);
return bundle;
},
/**
* @private
* @method _getAssetLoader
* @param {String} type
* @return {Function} loader
*/
_getAssetLoader(type) {
const loader = this.__assetLoaders[type];
Ember.assert(`No loader for assets of type '${type}' could be found.`, loader);
return loader;
},
/**
* Defines loader methods for various types of assets. Each loader is stored
* under a key corresponding to the type of asset it loads.
*
* @private
* @property __assetLoaders
* @type {Object}
*/
__assetLoaders: {
js(uri) {
return new RSVP.Promise((resolve, reject) => {
const script = createLoadElement('script', resolve, reject);
script.src = uri;
document.head.appendChild(script);
});
},
css(uri) {
return new RSVP.Promise((resolve, reject) => {
const link = createLoadElement('link', resolve, reject);
link.rel = 'stylesheet';
link.href = uri;
document.head.appendChild(link);
});
}
}
});
export default AssetLoader;
@nathanhammond
Copy link

@trentmwillis
Copy link
Author

trentmwillis commented Jul 19, 2016

@nathanhammond:

I'm a bit concerned about this being a single manifest.

Will be changing this per other conversations to allow multiple manifests.

document.head isn't guaranteed to exist.

Under what circumstances does it not exist? Looks like browser support aligns and for pages that omit the <head> it gets auto-inserted.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment