playcanvas
Version:
Open-source WebGL/WebGPU 3D engine for the web
362 lines (361 loc) • 12 kB
JavaScript
import { Debug } from "../../core/debug.js";
import { http } from "../../platform/net/http.js";
class ResourceLoader {
/**
* Create a new ResourceLoader instance.
*
* @param {AppBase} app - The application.
*/
constructor(app) {
this._handlers = {};
this._requests = {};
this._cache = {};
this._app = app;
}
/**
* Add a {@link ResourceHandler} for a resource type. Handler should support at least `load()`
* and `open()`. Handlers can optionally support patch(asset, assets) to handle dependencies on
* other assets.
*
* @param {string} type - The name of the resource type that the handler will be registered
* with. Can be:
*
* - {@link ASSET_ANIMATION}
* - {@link ASSET_AUDIO}
* - {@link ASSET_IMAGE}
* - {@link ASSET_JSON}
* - {@link ASSET_MODEL}
* - {@link ASSET_MATERIAL}
* - {@link ASSET_TEXT}
* - {@link ASSET_TEXTURE}
* - {@link ASSET_CUBEMAP}
* - {@link ASSET_SHADER}
* - {@link ASSET_CSS}
* - {@link ASSET_HTML}
* - {@link ASSET_SCRIPT}
* - {@link ASSET_CONTAINER}
*
* @param {ResourceHandler} handler - An instance of a resource handler
* supporting at least `load()` and `open()`.
* @example
* const loader = new ResourceLoader();
* loader.addHandler("json", new JsonHandler());
*/
addHandler(type, handler) {
this._handlers[type] = handler;
handler._loader = this;
}
/**
* Remove a {@link ResourceHandler} for a resource type.
*
* @param {string} type - The name of the type that the handler will be removed.
*/
removeHandler(type) {
delete this._handlers[type];
}
/**
* Get a {@link ResourceHandler} for a resource type.
*
* @param {string} type - The name of the resource type that the handler is registered with.
* @returns {ResourceHandler|undefined} The registered handler, or
* undefined if the requested handler is not registered.
*/
getHandler(type) {
return this._handlers[type];
}
static makeKey(url, type) {
return `${url}-${type}`;
}
/**
* Make a request for a resource from a remote URL. Parse the returned data using the handler
* for the specified type. When loaded and parsed, use the callback to return an instance of
* the resource.
*
* @param {string} url - The URL of the resource to load.
* @param {string} type - The type of resource expected.
* @param {ResourceLoaderCallback} callback - The callback used when the resource is loaded or
* an error occurs. Passed (err, resource) where err is null if there are no errors.
* @param {Asset} [asset] - Optional asset that is passed into
* handler.
* @param {object} [options] - Additional options for loading.
* @param {boolean} [options.bundlesIgnore] - If set to true, then asset will not try to load
* from a bundle. Defaults to false.
* @param {BundlesFilterCallback} [options.bundlesFilter] - A callback that will be called
* when loading an asset that is contained in any of the bundles. It provides an array of
* bundles and will ensure asset is loaded from bundle returned from a callback. By default,
* the smallest filesize bundle is chosen.
* @example
* app.loader.load("../path/to/texture.png", "texture", function (err, texture) {
* // use texture here
* });
*/
load(url, type, callback, asset, options) {
const handler = this._handlers[type];
if (!handler) {
const err = `No resource handler for asset type: '${type}' when loading [${url}]`;
Debug.errorOnce(err);
callback(err);
return;
}
if (!url) {
this._loadNull(handler, callback, asset);
return;
}
const key = ResourceLoader.makeKey(url, type);
if (this._cache[key] !== void 0) {
callback(null, this._cache[key]);
} else if (this._requests[key]) {
this._requests[key].push(callback);
} else {
this._requests[key] = [callback];
const self = this;
const handleLoad = function(err, urlObj) {
if (err) {
self._onFailure(key, err);
return;
}
if (urlObj.load instanceof DataView) {
if (handler.openBinary) {
if (!self._requests[key]) {
return;
}
try {
const data = handler.openBinary(urlObj.load);
self._onSuccess(key, data);
} catch (err2) {
self._onFailure(key, err2);
}
return;
}
urlObj.load = URL.createObjectURL(new Blob([urlObj.load]));
if (asset) {
if (asset.urlObject) {
URL.revokeObjectURL(asset.urlObject);
}
asset.urlObject = urlObj.load;
}
}
handler.load(urlObj, (err2, data, extra) => {
if (!self._requests[key]) {
return;
}
if (err2) {
self._onFailure(key, err2);
return;
}
try {
self._onSuccess(key, handler.open(urlObj.original, data, asset), extra);
} catch (e) {
self._onFailure(key, e);
}
}, asset);
};
const normalizedUrl = url.split("?")[0];
if (this._app.enableBundles && this._app.bundles.hasUrl(normalizedUrl) && !(options && options.bundlesIgnore)) {
if (!this._app.bundles.urlIsLoadedOrLoading(normalizedUrl)) {
const bundles = this._app.bundles.listBundlesForAsset(asset);
let bundle;
if (options && options.bundlesFilter) {
bundle = options.bundlesFilter(bundles);
}
if (!bundle) {
bundles?.sort((a, b) => {
return a.file.size - b.file.size;
});
bundle = bundles?.[0];
}
if (bundle) this._app.assets?.load(bundle);
}
this._app.bundles.loadUrl(normalizedUrl, (err, fileUrlFromBundle) => {
handleLoad(err, {
load: fileUrlFromBundle,
original: normalizedUrl
});
});
} else {
handleLoad(null, {
load: url,
original: asset && asset.file.filename || url
});
}
}
}
// load an asset with no url, skipping bundles and caching
_loadNull(handler, callback, asset) {
const onLoad = function(err, data, extra) {
if (err) {
callback(err);
} else {
try {
callback(null, handler.open(null, data, asset), extra);
} catch (e) {
callback(e);
}
}
};
handler.load(null, onLoad, asset);
}
_onSuccess(key, result, extra) {
if (result !== null) {
this._cache[key] = result;
} else {
delete this._cache[key];
}
for (let i = 0; i < this._requests[key].length; i++) {
this._requests[key][i](null, result, extra);
}
delete this._requests[key];
}
_onFailure(key, err) {
console.error(`Failed to load resource [${key}]: ${err?.message ?? err}`, err);
if (this._requests[key]) {
for (let i = 0; i < this._requests[key].length; i++) {
this._requests[key][i](err);
}
delete this._requests[key];
}
}
/**
* Convert raw resource data into a resource instance. E.g. Take 3D model format JSON and
* return a {@link Model}.
*
* @param {string} type - The type of resource.
* @param {*} data - The raw resource data.
* @returns {*} The parsed resource data.
*/
open(type, data) {
const handler = this._handlers[type];
if (!handler) {
console.warn(`No resource handler found for: ${type}`);
return data;
}
return handler.open(null, data);
}
/**
* Perform any operations on a resource, that requires a dependency on its asset data or any
* other asset data.
*
* @param {Asset} asset - The asset to patch.
* @param {AssetRegistry} assets - The asset registry.
*/
patch(asset, assets) {
const handler = this._handlers[asset.type];
if (!handler) {
console.warn(`No resource handler found for: ${asset.type}`);
return;
}
if (handler.patch) {
handler.patch(asset, assets);
}
}
/**
* Remove resource from cache.
*
* @param {string} url - The URL of the resource.
* @param {string} type - The type of resource.
*/
clearCache(url, type) {
const key = ResourceLoader.makeKey(url, type);
delete this._cache[key];
}
/**
* Check cache for resource from a URL. If present, return the cached value.
*
* @param {string} url - The URL of the resource to get from the cache.
* @param {string} type - The type of the resource.
* @returns {*} The resource loaded from the cache.
*/
getFromCache(url, type) {
const key = ResourceLoader.makeKey(url, type);
if (this._cache[key]) {
return this._cache[key];
}
return void 0;
}
/**
* Enables retrying of failed requests when loading assets. Retries use exponential backoff and
* are also enabled by default for new applications.
*
* @param {number} [maxRetries] - The maximum number of times to retry loading an asset.
* Defaults to 5.
*/
enableRetry(maxRetries = 5) {
maxRetries = Math.max(0, Math.floor(maxRetries)) || 0;
for (const key in this._handlers) {
this._handlers[key].maxRetries = maxRetries;
}
}
/**
* Disables retrying of failed requests when loading assets.
*/
disableRetry() {
for (const key in this._handlers) {
this._handlers[key].maxRetries = 0;
}
}
/**
* Sets the maximum number of asset requests that can be in flight at the same time. Additional
* requests are queued and dispatched as earlier ones complete. This prevents browsers from
* rejecting requests with `net::ERR_INSUFFICIENT_RESOURCES` when an app loads a very large
* number of assets at once. Set to `0` to disable throttling. Defaults to 128.
*
* Note: this is a process-global limit (it applies to the shared HTTP layer, matching the
* browser's per-process resource limit), so with multiple applications the last value set wins.
* It applies to all XHR-based requests, which covers the large majority of asset loads.
*
* @type {number}
* @example
* // never have more than 50 asset requests in flight at once
* app.loader.maxConcurrentRequests = 50;
*/
set maxConcurrentRequests(value) {
http.maxConcurrentRequests = Math.max(0, Math.floor(value)) || 0;
}
/**
* Gets the maximum number of asset requests that can be in flight at the same time.
*
* @type {number}
*/
get maxConcurrentRequests() {
return http.maxConcurrentRequests;
}
/**
* Sets whether asset requests are sent with credentials. When true, cross-origin requests
* include credentials (cookies, client TLS certificates and HTTP authentication), allowing
* assets to be loaded from an authenticated cross-origin host. The server must respond with a
* non-wildcard `Access-Control-Allow-Origin` and `Access-Control-Allow-Credentials: true`.
* Defaults to false.
*
* Set this before assets start loading (i.e. before {@link AppBase#preload} or
* {@link AssetRegistry#load}). Note this is a process-global setting (it applies to the shared
* HTTP layer), so with multiple applications the last value set wins. It applies to all
* XHR-based requests, which covers the large majority of asset loads.
*
* @type {boolean}
* @example
* // load all assets from an authenticated cross-origin host
* app.loader.withCredentials = true;
*/
set withCredentials(value) {
http.withCredentials = !!value;
}
/**
* Gets whether asset requests are sent with credentials.
*
* @type {boolean}
*/
get withCredentials() {
return http.withCredentials;
}
/**
* Destroys the resource loader.
*/
destroy() {
this._handlers = {};
this._requests = {};
this._cache = {};
}
}
export {
ResourceLoader
};