Source: components/AppBundleComponent.js

import { Utils } from '../common/Utils';
import { Bundle } from '../loaders/Bundle';
import { BaseComponent } from './BaseComponent';
import { PrefabBundleLoader } from '../loaders/PrefabBundleLoader';
import { ModelBundleLoader } from '../loaders/ModelBundleLoader';

const __ = {
	private: Symbol('private'),
}

/**
 * @class AppBundleComponent
 * The application bundle component.
 * @memberof THING
 * @extends THING.BaseComponent
 */
class AppBundleComponent extends BaseComponent {

	/**
	 * The bundle loader interface for application.
	 */
	constructor() {
		super();

		this[__.private] = {};
		let _private = this[__.private];

		// Url + bundles
		_private.bundlesMap = new Map();

		_private.loaders = {
			'prefab': new PrefabBundleLoader(),
			'model': new ModelBundleLoader()
		};
	}

	// #region Private

	_collectBundles(urls, options) {
		let _private = this[__.private];

		// Make all url to string array type
		if (Utils.isString(urls)) {
			urls = [urls];
		}

		return urls.map(url => {
			let key = url;

			let bundles = _private.bundlesMap.get(key);
			if (!bundles) {
				// Prepare to create new bundle with key(url)
				bundles = [];
				_private.bundlesMap.set(key, bundles);
			}

			let bundle = new Bundle({ url });

			bundles.push(bundle);

			return bundle;
		});
	}

	_unloadBundle(bundle) {
		let _private = this[__.private];

		let type = bundle.info.type;
		let loader = _private.loaders[type];
		if (loader) {
			if (Utils.isFunction(loader.onUnload)) {
				loader.onUnload(bundle);
			}
		}

		bundle.dispose();
	}

	_loadBundles(bundles, options) {
		let _private = this[__.private];

		return bundles.waitForEachAsync(
			bundle => {
				if (bundle.loaded) {
					return bundle.reload(_private.loaders, options);
				}
				else {
					return bundle.load(_private.loaders, options);
				}
			},
			(ev) => {

			}
		);
	}

	// #endregion

	// #region BaseComponent Interface

	onBeforeRemove() {
		let _private = this[__.private];

		_private.bundlesMap.forEach(bundles => {
			bundles.forEach(bundle => {
				this._unloadBundle(bundle);
			});
		});

		_private.bundlesMap.clear();

		for (let key in _private.loaders) {
			let loader = _private.loaders[key];

			if (loader.onDispose) {
				loader.onDispose();
			}
		}

		_private.loades = {};
	}

	// #endregion

	/**
	 * Register bundle loader.
	 * @param {String} type The bundle type.
	 * @param {Object} loader The loader object.
	 */
	registerBundleLoader(type, loader) {
		let _private = this[__.private];

		_private.loaders[type] = loader;
	}

	/**
	 * Load bundle.
	 * @param {String|Array<String>} urls The bundle resource url(s).
	 * @param {Object} options The options.
	 * @returns {Object}
	 * @example
	 * 	let bundle = app.loadBundle('./bundle/scene-bundle');
	 * 	bundle.waitForComplete().then(() => {
	 * 		console.log(bundle.info.name);
	 * 		bundle.release();
	 * 	});
	 */
	loadBundle(urls, options = {}) {
		// Collect bundle(s)
		let bundles = this._collectBundles(urls, options);

		// Start to load bundle(s)
		this._loadBundles(bundles, options);

		// Feedback bundle(s), these may be still in loading state
		if (bundles.length === 1) {
			return bundles[0];
		}
		else {
			return bundles;
		}
	}

	/**
	 * Load bundle in async mode.
	 * @param {String|Array<String>} urls The bundle resource url(s).
	 * @param {Object} options The options.
	 * @returns {Promise<any>}
	 * @example
	 * 	let bundle = await app.loadBundleAsync('./bundle/scene-bundle');
	 *	console.log(bundle.info.name);
	 * 	bundle.release();
	 */
	loadBundleAsync(urls, options = {}) {
		return new Promise((resolve, reject) => {
			// Build bundle(s)
			let bundles = this._collectBundles(urls, options);

			// Start to load bundle(s)
			this._loadBundles(bundles, options).then(
				() => {
					if (bundles.length === 1) {
						return resolve(bundles[0]);
					}
					else {
						return resolve(bundles);
					}
				},
				(ex) => {
					reject(ex);
				}
			);
		});
	}

	/**
	 * Unload bundle by url.
	 * @param {String|Array<String>} urls The bundle resource url(s).
	 * @example
	 * 	THING.App.current.unloadBundle('./bundle/scene-bundle');
	 */
	unloadBundleByUrl(urls) {
		let _private = this[__.private];

		// Make all url to string array type
		if (Utils.isString(urls)) {
			urls = [urls];
		}

		urls.forEach(url => {
			let bundles = _private.bundlesMap.get(url);
			if (!bundles) {
				return;
			}

			// To prevent map delete conflict we need to copy bundles here
			bundles.slice(0).forEach(bundle => {
				this._unloadBundle(bundle);
			});

			_private.bundlesMap.delete(url);
		});
	}

	/**
	 * Unload bundle
	 * @param {Bundle} bundle The bundle
	 * @example THING.App.current.unloadBundle(bundle);
	 */
	unloadBundle(bundle) {
		if (Utils.isString(bundle)) {
			Utils.warn('Please use "unloadBundleByUrl" to unload!');
			this.unloadBundleByUrl(bundle);
			return;
		}
		else if (Utils.isArray(bundle)) {
			const first = bundle[0];
			if (Utils.isString(first)) {
				Utils.warn('Please use "unloadBundleByUrl" to unload!');
				this.unloadBundleByUrl(bundle);
				return;
			}
		}

		if (!bundle || !bundle.isBundle) {
			return;
		}

		let _private = this[__.private];
		const url = bundle.url;
		let bundles = _private.bundlesMap.get(url);
		if (bundles) {
			for (let i = 0; i < bundles.length; i++) {
				const element = bundles[i];
				if (element == bundle) {
					bundles.splice(i, 1);
					break;
				}
			}
		}

		this._unloadBundle(bundle);
	}

}

AppBundleComponent.exportFunctions = [
	'registerBundleLoader',
	'loadBundle',
	'loadBundleAsync',
	'unloadBundle',
	'unloadBundleByUrl'
];

export { AppBundleComponent }