Source: asset/asset-list-loader.js

Object.assign(pc, function () {
    /**
     * @private
     * @constructor
     * @name pc.AssetListLoader
     * @classdesc Used to load a group of assets and fires a callback when all assets are loaded
     * @param {pc.Asset[] | Number[]} assetList An array of pc.Asset objects to load or an array of Asset IDs to load
     * @param {pc.AssetRegistry} assetRegistry The application's asset registry
     */
    var AssetListLoader = function (assetList, assetRegistry) {
        this._assets = [];
        this._registry = assetRegistry;
        this._loaded = false;
        this._count = 0; // running count of successfully loaded assets
        this._total = 0; // total assets loader is expecting to load
        this._failed = []; // list of assets that failed to load

        this._waitingAssets = [];

        if (assetList.length && assetList[0] instanceof pc.Asset) {
            // list of pc.Asset
            this._assets = assetList;
        } else {
            // list of Asset IDs
            for (var i = 0; i < assetList.length; i++) {
                var asset = assetRegistry.get(assetList[i]);
                if (asset) {
                    this._assets.push(asset);
                } else {
                    this._waitForAsset(assetList[i]);
                    this._total++;
                }

            }
        }


        pc.events.attach(this);
    };

    AssetListLoader.prototype.destroy = function () {
        // remove any outstanding listeners

        var self = this;

        this._registry.off("load", this._onLoad);
        this._registry.off("error", this._onError);

        this._waitingAssets.forEach(function (id) {
            self._registry.off("add:" + id, this._onAddAsset);
        });

        this.off("progress");
        this.off("load");
    };

    /**
     * @function
     * @name pc.AssetListLoader#load
     * @description  Start loading asset list, call done() when all assets have loaded or failed to load
     * @param {Function} done Callback called when all assets in the list are loaded. Passed (err, failed) where err is the undefined if no errors are encountered and failed contains a list of assets that failed to load
     * @param {Object} scope Scope to use when calling callback
     *
     */
    AssetListLoader.prototype.load = function (done, scope) {
        var i = 0;
        var l = this._assets.length;
        var asset;

        // this._total = l;
        this._count = 0;
        this._failed = [];
        this._callback = done;
        this._scope = scope;

        this._registry.on("load", this._onLoad, this);
        this._registry.on("error", this._onError, this);

        for (i = 0; i < l; i++) {
            asset = this._assets[i];

            if (!asset.loading && !asset.loaded) {
                this._registry.load(asset);
                this._total++;
            }
        }
    };

    /**
     * @function
     * @name pc.AssetListLoader#ready
     * @param {Function} done Callback called when all assets in the list are loaded
     * @param {Object} scope Scope to use when calling callback
     */
    AssetListLoader.prototype.ready = function (done, scope) {
        scope = scope || this;

        if (this._loaded) {
            done.call(scope, this._assets);
        } else {
            this.once("load", function (assets) {
                done.call(scope, assets);
            });
        }
    };

    // called when all assets are loaded
    AssetListLoader.prototype._loadingComplete = function () {
        this._loaded = true;
        this._registry.off("load", this._onLoad, this);
        this._registry.off("error", this._onError, this);

        if (this._failed && this._failed.length) {
            if (this._callback) {
                this._callback.call(this._scope, "Failed to load some assets", this._failed);
            }
            this.fire("error", this._failed);
        } else {
            if (this._callback) {
                this._callback.call(this._scope);
            }
            this.fire("load", this._assets);
        }
    };

    // called when an (any) asset is loaded
    AssetListLoader.prototype._onLoad = function (asset) {
        var self = this;

        // check this is an asset we care about
        if (this._assets.indexOf(asset) >= 0) {
            this._count++;
            this.fire("progress", asset);
        }

        if (this._count === this._total) {
            // call next tick because we want
            // this to be fired after any other
            // asset load events
            setTimeout(function () {
                self._loadingComplete(self._failed);
            }, 0);
        }
    };

    // called when an asset fails to load
    AssetListLoader.prototype._onError = function (err, asset) {
        var self = this;

        // check this is an asset we care about
        if (this._assets.indexOf(asset) >= 0) {
            this._count++;
            this._failed.push(asset);
        }

        if (this._count === this._total) {
            // call next tick because we want
            // this to be fired after any other
            // asset load events
            setTimeout(function () {
                self._loadingComplete(self._failed);
            }, 0);
        }
    };

    // called when a expected asset is added to the asset registry
    AssetListLoader.prototype._onAddAsset = function (asset) {
        // remove from waiting list
        var index = this._waitingAssets.indexOf(asset);
        if (index >= 0) {
            this._waitingAssets.splice(index, 1);
        }

        this._assets.push(asset);
        var i;
        var l = this._assets.length;
        for (i = 0; i < l; i++) {
            asset = this._assets[i];

            if (!asset.loading && !asset.loaded) {
                this._registry.load(asset);
            }
        }
    };

    AssetListLoader.prototype._waitForAsset = function (assetId) {
        this._waitingAssets.push(assetId);
        this._registry.once('add:' + assetId, this._onAddAsset, this);
    };

    return {
        AssetListLoader: AssetListLoader
    };

}());