addon/services/asset-loader.js
import RSVP from 'rsvp';
import Ember from 'ember';
import AssetLoadError from '../errors/asset-load';
import BundleLoadError from '../errors/bundle-load';
import JsLoader from '../loaders/js';
import CssLoader from '../loaders/css';
export function RETRY_LOAD_SECRET() { }
/**
* Merges two manifests' bundles together and returns a new manifest.
*
* @param {AssetManifest} input
* @param {AssetManifest} manifest
* @return {AssetManifest} output
*/
function reduceManifestBundles(input, manifest) {
// If manifest doesn't have any bundles, then no reducing to do
if (!manifest.bundles) {
return input;
}
// Merge the bundles together, checking for collisions
return Object.keys(manifest.bundles).reduce((output, bundle) => {
Ember.assert(`The bundle "${bundle}" already exists.`, !output.bundles[bundle]);
output.bundles[bundle] = manifest.bundles[bundle];
return output;
}, input);
}
/**
* A Service class to load additional assets into the Ember application.
*
* @class AssetLoader
*/
export default Ember.Service.extend({
/**
* Setup the caches for the service to use when loading assets.
*
* @override
*/
init() {
this._super(...arguments);
this.__manifests = [];
this._setupCache();
this._initAssetLoaders();
},
/**
* Adds a manifest to the service by merging its bundles with any previously
* added manifests. Bundle collisions result in an error being thrown.
*
* @public
* @method pushManifest
* @param {AssetManifest} manifest
* @return {Void}
*/
pushManifest(manifest) {
this.__manifests.push(manifest);
this.__manifest = this.__manifests.reduce(reduceManifestBundles, { bundles: {} });
},
/**
* 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
* @param {Boolean} retryLoad Warning: only used internally to re-initiate loads, NOT public API
* @return {Promise} bundlePromise
*/
loadBundle(name, retryLoad) {
const cachedPromise = this._getFromCache('bundle', name, retryLoad === RETRY_LOAD_SECRET);
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) {
// Evict rejected promise.
this._getFromCache('bundle', name, true);
throw new BundleLoadError(this, name, errors);
}
return name;
});
return this._setInCache('bundle', name, bundleWithFail);
},
/**
* Loads a single asset into the application. Expects a given asset to specify
* a URI and type.
*
* @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', cacheKey, retryLoad === RETRY_LOAD_SECRET);
if (cachedPromise) {
return cachedPromise;
}
const loader = this._getAssetLoader(type);
const assetPromise = loader(uri);
const assetWithFail = assetPromise.then(
() => ({ uri, type }),
(error) => {
// Evict rejected promise.
this._getFromCache('asset', cacheKey, true);
throw new AssetLoadError(this, { uri, type }, error);
}
);
return this._setInCache('asset', cacheKey, assetWithFail);
},
/**
* Define a loader function for assets of a specified type. Any previously
* defined loaders for that type will be overriden.
*
* @public
* @param {String} type
* @param {Funciton} loader
* @return {Void}
*/
defineLoader(type, loader) {
this.__assetLoaders[type] = loader;
},
/**
* Gets the current, reduced manifest.
*
* @private
* @method getManifest
* @return {AssetManifest} manifest
*/
getManifest() {
const manifest = this.__manifest;
Ember.assert('No asset manifest found. Ensure you call pushManifest before attempting to use the AssetLoader.', manifest);
return manifest;
},
/**
* Sets up the cache used to store Promise values for asset/bundle requests.
*
* @private
* @return {Void}
*/
_setupCache() {
this.__cache = {};
this.__cache.asset = {};
this.__cache.bundle = {};
},
/**
* Gets a value from the cache according to the type and key it was stored
* under. Optionally, evicts the cached value and returns undefined.
*
* @private
* @param {String} type
* @param {String} key
* @param {Boolean} evict
* @return {Any}
*/
_getFromCache(type, key, evict) {
if (evict) {
this.__cache[type][key] = undefined;
return;
}
return this.__cache[type][key];
},
/**
* Sets a value in the cache under a type and key.
*
* @private
* @param {String} type
* @param {String} key
* @param {Any} value
* @return {Any}
*/
_setInCache(type, key, value) {
return (this.__cache[type][key] = value);
},
/**
* Gets the info for a bundle from the reduced 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.', Object.keys(bundles).length);
const bundle = bundles[name];
Ember.assert(`No bundle with name "${name}" exists in the asset manifest.`, bundle);
return bundle;
},
/**
* Gets the asset loader method for a specified type.
*
* @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}" defined.`, loader);
return loader;
},
/**
* Initializes the __assetLoaders object and defines our default loaders.
*/
_initAssetLoaders() {
this.__assetLoaders = {};
this.defineLoader('js', JsLoader);
this.defineLoader('css', CssLoader);
},
/**
* 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: undefined
});