UNPKG

playcanvas

Version:

Open-source WebGL/WebGPU 3D engine for the web

362 lines (361 loc) 12 kB
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 };