import { Cache } from './Cache.js'; import { Loader } from './Loader.js'; const loading = {}; class FileLoader extends Loader { constructor(manager) { super(manager); } load(url, onLoad, onProgress, onError) { if (url === undefined) url = ''; if (this.path !== undefined) url = this.path + url; url = this.manager.resolveURL(url); const cached = Cache.get(url); if (cached !== undefined) { this.manager.itemStart(url); setTimeout(() => { if (onLoad) onLoad(cached); this.manager.itemEnd(url); }, 0); return cached; } // Check if request is duplicate if (loading[url] !== undefined) { loading[url].push({ onLoad: onLoad, onProgress: onProgress, onError: onError, }); return; } // Initialise array for duplicate requests loading[url] = []; loading[url].push({ onLoad: onLoad, onProgress: onProgress, onError: onError, }); // create request const req = new Request(url, { headers: new Headers(this.requestHeader), credentials: this.withCredentials ? 'include' : 'same-origin', // An abort controller could be added within a future PR }); // start the fetch fetch(req) .then((response) => { if (response.status === 200 || response.status === 0) { // Some browsers return HTTP Status 0 when using non-http protocol // e.g. 'file://' or 'data://'. Handle as success. if (response.status === 0) { console.warn('THREE.FileLoader: HTTP Status 0 received.'); } if (typeof ReadableStream === 'undefined' || response.body.getReader === undefined) { return response; } const callbacks = loading[url]; const reader = response.body.getReader(); const contentLength = response.headers.get('Content-Length'); const total = contentLength ? parseInt(contentLength) : 0; const lengthComputable = total !== 0; let loaded = 0; // periodically read data into the new stream tracking while download progress const stream = new ReadableStream({ start(controller) { readData(); function readData() { reader.read().then(({ done, value }) => { if (done) { controller.close(); } else { loaded += value.byteLength; const event = new ProgressEvent('progress', { lengthComputable, loaded, total }); for (let i = 0, il = callbacks.length; i < il; i++) { const callback = callbacks[i]; if (callback.onProgress) callback.onProgress(event); } controller.enqueue(value); readData(); } }); } }, }); return new Response(stream); } else { throw Error(`fetch for "${response.url}" responded with ${response.status}: ${response.statusText}`); } }) .then((response) => { switch (this.responseType) { case 'arraybuffer': return response.arrayBuffer(); case 'blob': return response.blob(); case 'document': return response.text().then((text) => { const parser = new DOMParser(); return parser.parseFromString(text, this.mimeType); }); case 'json': return response.json(); default: return response.text(); } }) .then((data) => { // Add to cache only on HTTP success, so that we do not cache // error response bodies as proper responses to requests. Cache.add(url, data); const callbacks = loading[url]; delete loading[url]; for (let i = 0, il = callbacks.length; i < il; i++) { const callback = callbacks[i]; if (callback.onLoad) callback.onLoad(data); } }) .catch((err) => { // Abort errors and other errors are handled the same const callbacks = loading[url]; if (callbacks === undefined) { // When onLoad was called and url was deleted in `loading` this.manager.itemError(url); throw err; } delete loading[url]; for (let i = 0, il = callbacks.length; i < il; i++) { const callback = callbacks[i]; if (callback.onError) callback.onError(err); } this.manager.itemError(url); }) .finally(() => { this.manager.itemEnd(url); }); this.manager.itemStart(url); } setResponseType(value) { this.responseType = value; return this; } setMimeType(value) { this.mimeType = value; return this; } } export { FileLoader };