import { Cache } from './Cache.js'; import { Loader } from './Loader.js'; const loading = {}; class HttpError extends Error { constructor( message, response ) { super( message ); this.response = response; } } 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 } ); // record states ( avoid data race ) const mimeType = this.mimeType; const responseType = this.responseType; // 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.' ); } // Workaround: Checking if response.body === undefined for Alipay browser #23548 if ( typeof ReadableStream === 'undefined' || response.body === undefined || response.body.getReader === undefined ) { return response; } const callbacks = loading[ url ]; const reader = response.body.getReader(); // Nginx needs X-File-Size check // https://serverfault.com/questions/482875/why-does-nginx-remove-content-length-header-for-chunked-content const contentLength = response.headers.get( 'Content-Length' ) || response.headers.get( 'X-File-Size' ); 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 new HttpError( `fetch for "${response.url}" responded with ${response.status}: ${response.statusText}`, response ); } } ) .then( response => { switch ( 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, mimeType ); } ); case 'json': return response.json(); default: if ( mimeType === undefined ) { return response.text(); } else { // sniff encoding const re = /charset="?([^;"\s]*)"?/i; const exec = re.exec( mimeType ); const label = exec && exec[ 1 ] ? exec[ 1 ].toLowerCase() : undefined; const decoder = new TextDecoder( label ); return response.arrayBuffer().then( ab => decoder.decode( ab ) ); } } } ) .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 };