Buckets:
| /** | |
| * @license | |
| * Copyright 2016 The Emscripten Authors | |
| * SPDX-License-Identifier: MIT | |
| */ | |
| #if FETCH_STREAMING | |
| /** | |
| * A class that mimics the XMLHttpRequest API using the modern Fetch API. | |
| * This implementation is specifically tailored to only handle 'arraybuffer' | |
| * responses. | |
| */ | |
| class FetchXHR { | |
| // --- Public XHR Properties --- | |
| // Event Handlers | |
| onload = null; | |
| onerror = null; | |
| onprogress = null; | |
| onreadystatechange = null; | |
| ontimeout = null; | |
| // Request Configuration | |
| responseType = 'arraybuffer'; | |
| withCredentials = false; | |
| timeout = 0; // Standard XHR timeout property | |
| // Response / State Properties | |
| readyState = 0; // 0: UNSENT | |
| response = null; | |
| responseURL = ''; | |
| status = 0; | |
| statusText = ''; | |
| // --- Internal Properties --- | |
| _method = ''; | |
| _url = ''; | |
| _headers = {}; | |
| _abortController = null; | |
| _aborted = false; | |
| _responseHeaders = null; | |
| // When enabled the the data will be streamed using progress events. If the full result is needed | |
| // the data must be collected during the progress events. | |
| _streamData = false; | |
| // --- Private state management --- | |
| _changeReadyState(state) { | |
| this.readyState = state; | |
| this.onreadystatechange?.(); | |
| } | |
| // --- Public XHR Methods --- | |
| /** | |
| * Initializes a request. | |
| * @param {string} method The HTTP request method (e.g., 'GET', 'POST'). | |
| * @param {string} url The URL to send the request to. | |
| * @param {boolean} [async=true] This parameter is ignored as Fetch is always async. | |
| * @param {string|null} [user=null] The username for basic authentication. | |
| * @param {string|null} [password=null] The password for basic authentication. | |
| */ | |
| open(method, url, async = true, user = null, password = null) { | |
| if (this.readyState !== 0 && this.readyState !== 4) { | |
| console.warn("FetchXHR.open() called while a request is in progress."); | |
| this.abort(); | |
| } | |
| // Reset internal state for the new request | |
| this._method = method; | |
| this._url = url; | |
| this._headers = {}; | |
| this._responseHeaders = null; | |
| // The async parameter is part of the XHR API but is an error here because | |
| // the Fetch API is inherently asynchronous and does not support synchronous requests. | |
| if (!async) { | |
| throw new Error("FetchXHR does not support synchronous requests."); | |
| } | |
| // Handle Basic Authentication if user/password are provided. | |
| // This creates a base64-encoded string and sets the Authorization header. | |
| if (user) { | |
| const credentials = btoa(`${user}:${password || ''}`); | |
| this._headers['Authorization'] = `Basic ${credentials}`; | |
| } | |
| this._changeReadyState(1); // 1: OPENED | |
| } | |
| /** | |
| * Sets the value of an HTTP request header. | |
| * @param {string} header The name of the header. | |
| * @param {string} value The value of the header. | |
| */ | |
| setRequestHeader(header, value) { | |
| if (this.readyState !== 1) { | |
| throw new Error('setRequestHeader can only be called when state is OPENED.'); | |
| } | |
| this._headers[header] = value; | |
| } | |
| /** | |
| * This method is not effectively implemented because Fetch API relies on the | |
| * server's Content-Type header and does not support overriding the MIME type | |
| * on the client side in the same way as XHR. | |
| * @param {string} mimetype The MIME type to use. | |
| */ | |
| overrideMimeType(mimetype) { | |
| throw new Error("overrideMimeType is not supported by the Fetch API and has no effect."); | |
| } | |
| /** | |
| * Returns a string containing all the response headers, separated by CRLF. | |
| * @returns {string} The response headers. | |
| */ | |
| getAllResponseHeaders() { | |
| if (!this._responseHeaders) { | |
| return ''; | |
| } | |
| let headersString = ''; | |
| // The Headers object is iterable. | |
| for (const [key, value] of this._responseHeaders.entries()) { | |
| headersString += `${key}: ${value}\r\n`; | |
| } | |
| return headersString; | |
| } | |
| /** | |
| * Sends the request. | |
| * @param body The body of the request. | |
| */ | |
| async send(body = null) { | |
| if (this.readyState !== 1) { | |
| throw new Error('send() can only be called when state is OPENED.'); | |
| } | |
| this._abortController = new AbortController(); | |
| const signal = this._abortController.signal; | |
| // Handle timeout | |
| let timeoutID; | |
| if (this.timeout > 0) { | |
| timeoutID = setTimeout( | |
| () => this._abortController.abort(new DOMException('The user aborted a request.', 'TimeoutError')), | |
| this.timeout | |
| ); | |
| } | |
| const fetchOptions = { | |
| method: this._method, | |
| headers: this._headers, | |
| body: body, | |
| signal: signal, | |
| credentials: this.withCredentials ? 'include' : 'same-origin', | |
| }; | |
| try { | |
| const response = await fetch(this._url, fetchOptions); | |
| // Populate response properties once headers are received | |
| this.status = response.status; | |
| this.statusText = response.statusText; | |
| this.responseURL = response.url; | |
| this._responseHeaders = response.headers; | |
| this._changeReadyState(2); // 2: HEADERS_RECEIVED | |
| // Start processing the body | |
| this._changeReadyState(3); // 3: LOADING | |
| if (!response.body) { | |
| throw new Error("Response has no body to read."); | |
| } | |
| const reader = response.body.getReader(); | |
| const contentLength = +response.headers.get('Content-Length'); | |
| let receivedLength = 0; | |
| // When streaming data don't collect all of the chunks into one large chunk. It's up to the | |
| // user to collect the data as it comes in. | |
| const chunks = this._streamData ? null : []; | |
| while (true) { | |
| const { done, value } = await reader.read(); | |
| if (done) { | |
| break; | |
| } | |
| if (!this._streamData) { | |
| chunks.push(value); | |
| } | |
| receivedLength += value.length; | |
| if (this.onprogress) { | |
| // Convert to ArrayBuffer as requested by responseType. | |
| this.response = value.buffer; | |
| const progressEvent = { | |
| lengthComputable: contentLength > 0, | |
| loaded: receivedLength, | |
| total: contentLength | |
| }; | |
| this.onprogress(progressEvent); | |
| } | |
| } | |
| if (this._streamData) { | |
| this.response = null; | |
| } else { | |
| // Combine chunks into a single Uint8Array. | |
| const allChunks = new Uint8Array(receivedLength); | |
| let position = 0; | |
| for (const chunk of chunks) { | |
| allChunks.set(chunk, position); | |
| position += chunk.length; | |
| } | |
| // Convert to ArrayBuffer as requested by responseType | |
| this.response = allChunks.buffer; | |
| } | |
| } catch (error) { | |
| this.statusText = error.message; | |
| if (error.name === 'AbortError') { | |
| // Do nothing. | |
| } else if (error.name === 'TimeoutError') { | |
| this.ontimeout?.(); | |
| } else { | |
| // This is a network error | |
| this.onerror?.(); | |
| } | |
| } finally { | |
| clearTimeout(timeoutID); | |
| if (!this._aborted) { | |
| this._changeReadyState(4); // 4: DONE | |
| // The XHR 'load' event fires for successful HTTP statuses (2xx) as well as | |
| // unsuccessful ones (4xx, 5xx). The 'error' event is for network failures. | |
| this.onload?.(); | |
| } | |
| } | |
| } | |
| /** | |
| * Aborts the request if it has already been sent. | |
| */ | |
| abort() { | |
| this._aborted = true; | |
| this.status = 0; | |
| this._changeReadyState(4); // 4: DONE | |
| this._abortController?.abort(); | |
| } | |
| } | |
| #endif | |
| var Fetch = { | |
| // HandleAllocator for XHR request object | |
| // xhrs: undefined, | |
| // The web worker that runs proxied file I/O requests. (this field is | |
| // populated on demand, start as undefined to save code size) | |
| // worker: undefined, | |
| // Specifies an instance to the IndexedDB database. The database is opened | |
| // as a preload step before the Emscripten application starts. (this field is | |
| // populated on demand, start as undefined to save code size) | |
| // dbInstance: undefined, | |
| #if FETCH_SUPPORT_INDEXEDDB | |
| async openDatabase(dbname, dbversion) { | |
| return new Promise((resolve, reject) => { | |
| try { | |
| #if FETCH_DEBUG | |
| dbg(`fetch: indexedDB.open(dbname="${dbname}", dbversion="${dbversion}");`); | |
| #endif | |
| var openRequest = indexedDB.open(dbname, dbversion); | |
| } catch (e) { | |
| return reject(e); | |
| } | |
| openRequest.onupgradeneeded = (event) => { | |
| #if FETCH_DEBUG | |
| dbg('fetch: IndexedDB upgrade needed. Clearing database.'); | |
| #endif | |
| var db = /** @type {IDBDatabase} */ (event.target.result); | |
| if (db.objectStoreNames.contains('FILES')) { | |
| db.deleteObjectStore('FILES'); | |
| } | |
| db.createObjectStore('FILES'); | |
| }; | |
| openRequest.onsuccess = (event) => resolve(event.target.result); | |
| openRequest.onerror = reject; | |
| }); | |
| }, | |
| #endif | |
| async init() { | |
| Fetch.xhrs = new HandleAllocator(); | |
| #if FETCH_SUPPORT_INDEXEDDB | |
| #if PTHREADS | |
| if (ENVIRONMENT_IS_PTHREAD) return; | |
| #endif | |
| addRunDependency('library_fetch_init'); | |
| try { | |
| var db = await Fetch.openDatabase('emscripten_filesystem', 1); | |
| #if FETCH_DEBUG | |
| dbg('fetch: IndexedDB successfully opened.'); | |
| #endif | |
| Fetch.dbInstance = db; | |
| } catch (e) { | |
| #if FETCH_DEBUG | |
| dbg('fetch: IndexedDB open failed.'); | |
| #endif | |
| Fetch.dbInstance = false; | |
| } finally { | |
| removeRunDependency('library_fetch_init'); | |
| } | |
| #endif // ~FETCH_SUPPORT_INDEXEDDB | |
| } | |
| } | |
| #if FETCH_SUPPORT_INDEXEDDB | |
| function fetchDeleteCachedData(db, fetch, onsuccess, onerror) { | |
| if (!db) { | |
| #if FETCH_DEBUG | |
| dbg('fetch: IndexedDB not available!'); | |
| #endif | |
| onerror(fetch, 0, 'IndexedDB not available!'); | |
| return; | |
| } | |
| var fetch_attr = fetch + {{{ C_STRUCTS.emscripten_fetch_t.__attributes }}}; | |
| var path = {{{ makeGetValue('fetch_attr', C_STRUCTS.emscripten_fetch_attr_t.destinationPath, '*') }}}; | |
| path ||= {{{ makeGetValue('fetch', C_STRUCTS.emscripten_fetch_t.url, '*') }}}; | |
| var pathStr = UTF8ToString(path); | |
| try { | |
| var transaction = db.transaction(['FILES'], 'readwrite'); | |
| var packages = transaction.objectStore('FILES'); | |
| var request = packages.delete(pathStr); | |
| request.onsuccess = (event) => { | |
| var value = event.target.result; | |
| #if FETCH_DEBUG | |
| dbg(`fetch: Deleted file ${pathStr} from IndexedDB`); | |
| #endif | |
| {{{ makeSetValue('fetch', C_STRUCTS.emscripten_fetch_t.data, 0, '*') }}}; | |
| writeI53ToI64(fetch + {{{ C_STRUCTS.emscripten_fetch_t.numBytes }}}, 0); | |
| writeI53ToI64(fetch + {{{ C_STRUCTS.emscripten_fetch_t.dataOffset }}}, 0); | |
| writeI53ToI64(fetch + {{{ C_STRUCTS.emscripten_fetch_t.totalBytes }}}, 0); | |
| // Mimic XHR readyState 4 === 'DONE: The operation is complete' | |
| {{{ makeSetValue('fetch', C_STRUCTS.emscripten_fetch_t.readyState, 4, 'i16') }}}; | |
| // Mimic XHR HTTP status code 200 "OK" | |
| {{{ makeSetValue('fetch', C_STRUCTS.emscripten_fetch_t.status, 200, 'i16') }}}; | |
| stringToUTF8("OK", fetch + {{{ C_STRUCTS.emscripten_fetch_t.statusText }}}, 64); | |
| onsuccess(fetch, 0, value); | |
| }; | |
| request.onerror = (error) => { | |
| #if FETCH_DEBUG | |
| dbg(`fetch: Failed to delete file ${pathStr} from IndexedDB! error: ${error}`); | |
| #endif | |
| {{{ makeSetValue('fetch', C_STRUCTS.emscripten_fetch_t.readyState, 4, 'i16') }}} // Mimic XHR readyState 4 === 'DONE: The operation is complete' | |
| {{{ makeSetValue('fetch', C_STRUCTS.emscripten_fetch_t.status, 404, 'i16') }}} // Mimic XHR HTTP status code 404 "Not Found" | |
| stringToUTF8("Not Found", fetch + {{{ C_STRUCTS.emscripten_fetch_t.statusText }}}, 64); | |
| onerror(fetch, 0, error); | |
| }; | |
| } catch(e) { | |
| #if FETCH_DEBUG | |
| dbg(`fetch: Failed to load file ${pathStr} from IndexedDB! Got exception ${e}`); | |
| #endif | |
| onerror(fetch, 0, e); | |
| } | |
| } | |
| function fetchLoadCachedData(db, fetch, onsuccess, onerror) { | |
| if (!db) { | |
| #if FETCH_DEBUG | |
| dbg('fetch: IndexedDB not available!'); | |
| #endif | |
| onerror(fetch, 0, 'IndexedDB not available!'); | |
| return; | |
| } | |
| var fetch_attr = fetch + {{{ C_STRUCTS.emscripten_fetch_t.__attributes }}}; | |
| var path = {{{ makeGetValue('fetch_attr', C_STRUCTS.emscripten_fetch_attr_t.destinationPath, '*') }}}; | |
| path ||= {{{ makeGetValue('fetch', C_STRUCTS.emscripten_fetch_t.url, '*') }}}; | |
| var pathStr = UTF8ToString(path); | |
| try { | |
| var transaction = db.transaction(['FILES'], 'readonly'); | |
| var packages = transaction.objectStore('FILES'); | |
| var getRequest = packages.get(pathStr); | |
| getRequest.onsuccess = (event) => { | |
| if (event.target.result) { | |
| var value = event.target.result; | |
| var len = value.byteLength || value.length; | |
| #if FETCH_DEBUG | |
| dbg(`fetch: Loaded file ${pathStr} from IndexedDB, length: ${len}`); | |
| #endif | |
| // The data pointer malloc()ed here has the same lifetime as the emscripten_fetch_t structure itself has, and is | |
| // freed when emscripten_fetch_close() is called. | |
| var ptr = _malloc(len); | |
| HEAPU8.set(new Uint8Array(value), ptr); | |
| {{{ makeSetValue('fetch', C_STRUCTS.emscripten_fetch_t.data, 'ptr', '*') }}}; | |
| writeI53ToI64(fetch + {{{ C_STRUCTS.emscripten_fetch_t.numBytes }}}, len); | |
| writeI53ToI64(fetch + {{{ C_STRUCTS.emscripten_fetch_t.dataOffset }}}, 0); | |
| writeI53ToI64(fetch + {{{ C_STRUCTS.emscripten_fetch_t.totalBytes }}}, len); | |
| {{{ makeSetValue('fetch', C_STRUCTS.emscripten_fetch_t.readyState, 4, 'i16') }}} // Mimic XHR readyState 4 === 'DONE: The operation is complete' | |
| {{{ makeSetValue('fetch', C_STRUCTS.emscripten_fetch_t.status, 200, 'i16') }}} // Mimic XHR HTTP status code 200 "OK" | |
| stringToUTF8("OK", fetch + {{{ C_STRUCTS.emscripten_fetch_t.statusText }}}, 64); | |
| onsuccess(fetch, 0, value); | |
| } else { | |
| // Succeeded to load, but the load came back with the value of undefined, treat that as an error since we never store undefined in db. | |
| #if FETCH_DEBUG | |
| dbg(`fetch: File ${pathStr} not found in IndexedDB`); | |
| #endif | |
| {{{ makeSetValue('fetch', C_STRUCTS.emscripten_fetch_t.readyState, 4, 'i16') }}} // Mimic XHR readyState 4 === 'DONE: The operation is complete' | |
| {{{ makeSetValue('fetch', C_STRUCTS.emscripten_fetch_t.status, 404, 'i16') }}} // Mimic XHR HTTP status code 404 "Not Found" | |
| stringToUTF8("Not Found", fetch + {{{ C_STRUCTS.emscripten_fetch_t.statusText }}}, 64); | |
| onerror(fetch, 0, 'no data'); | |
| } | |
| }; | |
| getRequest.onerror = (error) => { | |
| #if FETCH_DEBUG | |
| dbg(`fetch: Failed to load file ${pathStr} from IndexedDB!`); | |
| #endif | |
| {{{ makeSetValue('fetch', C_STRUCTS.emscripten_fetch_t.readyState, 4, 'i16') }}} // Mimic XHR readyState 4 === 'DONE: The operation is complete' | |
| {{{ makeSetValue('fetch', C_STRUCTS.emscripten_fetch_t.status, 404, 'i16') }}} // Mimic XHR HTTP status code 404 "Not Found" | |
| stringToUTF8("Not Found", fetch + {{{ C_STRUCTS.emscripten_fetch_t.statusText }}}, 64); | |
| onerror(fetch, 0, error); | |
| }; | |
| } catch(e) { | |
| #if FETCH_DEBUG | |
| dbg(`fetch: Failed to load file ${pathStr} from IndexedDB! Got exception ${e}`); | |
| #endif | |
| onerror(fetch, 0, e); | |
| } | |
| } | |
| function fetchCacheData(/** @type {IDBDatabase} */ db, fetch, data, onsuccess, onerror) { | |
| if (!db) { | |
| #if FETCH_DEBUG | |
| dbg('fetch: IndexedDB not available!'); | |
| #endif | |
| onerror(fetch, 0, 'IndexedDB not available!'); | |
| return; | |
| } | |
| var fetch_attr = fetch + {{{ C_STRUCTS.emscripten_fetch_t.__attributes }}}; | |
| var destinationPath = {{{ makeGetValue('fetch_attr', C_STRUCTS.emscripten_fetch_attr_t.destinationPath, '*') }}}; | |
| destinationPath ||= {{{ makeGetValue('fetch', C_STRUCTS.emscripten_fetch_t.url, '*') }}}; | |
| var destinationPathStr = UTF8ToString(destinationPath); | |
| try { | |
| var transaction = db.transaction(['FILES'], 'readwrite'); | |
| var packages = transaction.objectStore('FILES'); | |
| var putRequest = packages.put(data, destinationPathStr); | |
| putRequest.onsuccess = (event) => { | |
| #if FETCH_DEBUG | |
| dbg(`fetch: Stored file "${destinationPathStr}" to IndexedDB cache.`); | |
| #endif | |
| {{{ makeSetValue('fetch', C_STRUCTS.emscripten_fetch_t.readyState, 4, 'i16') }}} // Mimic XHR readyState 4 === 'DONE: The operation is complete' | |
| {{{ makeSetValue('fetch', C_STRUCTS.emscripten_fetch_t.status, 200, 'i16') }}} // Mimic XHR HTTP status code 200 "OK" | |
| stringToUTF8("OK", fetch + {{{ C_STRUCTS.emscripten_fetch_t.statusText }}}, 64); | |
| onsuccess(fetch, 0, destinationPathStr); | |
| }; | |
| putRequest.onerror = (error) => { | |
| #if FETCH_DEBUG | |
| dbg(`fetch: Failed to store file "${destinationPathStr}" to IndexedDB cache!`); | |
| #endif | |
| // Most likely we got an error if IndexedDB is unwilling to store any more data for this page. | |
| // TODO: Can we identify and break down different IndexedDB-provided errors and convert those | |
| // to more HTTP status codes for more information? | |
| {{{ makeSetValue('fetch', C_STRUCTS.emscripten_fetch_t.readyState, 4, 'i16') }}} // Mimic XHR readyState 4 === 'DONE: The operation is complete' | |
| {{{ makeSetValue('fetch', C_STRUCTS.emscripten_fetch_t.status, 413, 'i16') }}} // Mimic XHR HTTP status code 413 "Payload Too Large" | |
| stringToUTF8("Payload Too Large", fetch + {{{ C_STRUCTS.emscripten_fetch_t.statusText }}}, 64); | |
| onerror(fetch, 0, error); | |
| }; | |
| } catch(e) { | |
| #if FETCH_DEBUG | |
| dbg(`fetch: Failed to store file "${destinationPathStr}" to IndexedDB cache! Exception: ${e}`); | |
| #endif | |
| onerror(fetch, 0, e); | |
| } | |
| } | |
| #endif // ~FETCH_SUPPORT_INDEXEDDB | |
| function fetchXHR(fetch, onsuccess, onerror, onprogress, onreadystatechange) { | |
| var url = {{{ makeGetValue('fetch', C_STRUCTS.emscripten_fetch_t.url, '*') }}}; | |
| if (!url) { | |
| #if FETCH_DEBUG | |
| dbg('fetch: XHR failed, no URL specified!'); | |
| #endif | |
| onerror(fetch, 'no url specified!'); | |
| return; | |
| } | |
| var url_ = UTF8ToString(url); | |
| var fetch_attr = fetch + {{{ C_STRUCTS.emscripten_fetch_t.__attributes }}}; | |
| var requestMethod = UTF8ToString(fetch_attr + {{{ C_STRUCTS.emscripten_fetch_attr_t.requestMethod }}}); | |
| requestMethod ||= 'GET'; | |
| var timeoutMsecs = {{{ makeGetValue('fetch_attr', C_STRUCTS.emscripten_fetch_attr_t.timeoutMSecs, 'u32') }}}; | |
| var userName = {{{ makeGetValue('fetch_attr', C_STRUCTS.emscripten_fetch_attr_t.userName, '*') }}}; | |
| var password = {{{ makeGetValue('fetch_attr', C_STRUCTS.emscripten_fetch_attr_t.password, '*') }}}; | |
| var requestHeaders = {{{ makeGetValue('fetch_attr', C_STRUCTS.emscripten_fetch_attr_t.requestHeaders, '*') }}}; | |
| var overriddenMimeType = {{{ makeGetValue('fetch_attr', C_STRUCTS.emscripten_fetch_attr_t.overriddenMimeType, '*') }}}; | |
| var dataPtr = {{{ makeGetValue('fetch_attr', C_STRUCTS.emscripten_fetch_attr_t.requestData, '*') }}}; | |
| var dataLength = {{{ makeGetValue('fetch_attr', C_STRUCTS.emscripten_fetch_attr_t.requestDataSize, '*') }}}; | |
| var fetchAttributes = {{{ makeGetValue('fetch_attr', C_STRUCTS.emscripten_fetch_attr_t.attributes, 'u32') }}}; | |
| var fetchAttrLoadToMemory = !!(fetchAttributes & {{{ cDefs.EMSCRIPTEN_FETCH_LOAD_TO_MEMORY }}}); | |
| var fetchAttrStreamData = !!(fetchAttributes & {{{ cDefs.EMSCRIPTEN_FETCH_STREAM_DATA }}}); | |
| var fetchAttrSynchronous = !!(fetchAttributes & {{{ cDefs.EMSCRIPTEN_FETCH_SYNCHRONOUS }}}); | |
| var userNameStr = userName ? UTF8ToString(userName) : undefined; | |
| var passwordStr = password ? UTF8ToString(password) : undefined; | |
| #if FETCH_STREAMING == 1 | |
| if (fetchAttrStreamData) { | |
| var xhr = new FetchXHR(); | |
| } else { | |
| var xhr = new XMLHttpRequest(); | |
| } | |
| #elif FETCH_STREAMING == 2 | |
| // This setting forces using FetchXHR for all requests. Used only in testing. | |
| var xhr = new FetchXHR(); | |
| #else | |
| var xhr = new XMLHttpRequest(); | |
| #endif | |
| xhr.withCredentials = !!{{{ makeGetValue('fetch_attr', C_STRUCTS.emscripten_fetch_attr_t.withCredentials, 'u8') }}};; | |
| xhr._streamData = fetchAttrStreamData; | |
| #if FETCH_DEBUG | |
| dbg(`fetch: xhr.timeout: ${xhr.timeout}, xhr.withCredentials: ${xhr.withCredentials}`); | |
| dbg(`fetch: xhr.open(requestMethod="${requestMethod}", url: "${url}", userName: ${userNameStr}, password: ${passwordStr}`); | |
| #endif | |
| xhr.open(requestMethod, url_, !fetchAttrSynchronous, userNameStr, passwordStr); | |
| if (!fetchAttrSynchronous) xhr.timeout = timeoutMsecs; // XHR timeout field is only accessible in async XHRs, and must be set after .open() but before .send(). | |
| xhr.url_ = url_; // Save the url for debugging purposes (and for comparing to the responseURL that server side advertised) | |
| #if ASSERTIONS && !FETCH_STREAMING | |
| assert(!fetchAttrStreamData, 'streaming is only supported when FETCH_STREAMING is enabled'); | |
| #endif | |
| xhr.responseType = 'arraybuffer'; | |
| if (overriddenMimeType) { | |
| var overriddenMimeTypeStr = UTF8ToString(overriddenMimeType); | |
| #if FETCH_DEBUG | |
| dbg(`fetch: xhr.overrideMimeType("${overriddenMimeTypeStr}");`); | |
| #endif | |
| xhr.overrideMimeType(overriddenMimeTypeStr); | |
| } | |
| if (requestHeaders) { | |
| for (;;) { | |
| var key = {{{ makeGetValue('requestHeaders', 0, '*') }}}; | |
| if (!key) break; | |
| var value = {{{ makeGetValue('requestHeaders', POINTER_SIZE, '*') }}}; | |
| if (!value) break; | |
| requestHeaders += {{{ 2 * POINTER_SIZE }}}; | |
| var keyStr = UTF8ToString(key); | |
| var valueStr = UTF8ToString(value); | |
| #if FETCH_DEBUG | |
| dbg(`fetch: xhr.setRequestHeader("${keyStr}", "${valueStr}");`); | |
| #endif | |
| xhr.setRequestHeader(keyStr, valueStr); | |
| } | |
| } | |
| var id = Fetch.xhrs.allocate(xhr); | |
| #if FETCH_DEBUG | |
| dbg(`fetch: id=${id}`); | |
| #endif | |
| {{{ makeSetValue('fetch', C_STRUCTS.emscripten_fetch_t.id, 'id', 'u32') }}}; | |
| var data = (dataPtr && dataLength) ? HEAPU8.slice(dataPtr, dataPtr + dataLength) : null; | |
| // TODO: Support specifying custom headers to the request. | |
| // Share the code to save the response, as we need to do so both on success | |
| // and on error (despite an error, there may be a response, like a 404 page). | |
| // This receives a condition, which determines whether to save the xhr's | |
| // response, or just 0. | |
| function saveResponseAndStatus() { | |
| var ptr = 0; | |
| var ptrLen = 0; | |
| if (xhr.response && fetchAttrLoadToMemory && {{{ makeGetValue('fetch', C_STRUCTS.emscripten_fetch_t.data, '*') }}} === 0) { | |
| ptrLen = xhr.response.byteLength; | |
| } | |
| if (ptrLen > 0) { | |
| #if FETCH_DEBUG | |
| dbg(`fetch: allocating ${ptrLen} bytes in Emscripten heap for xhr data`); | |
| #endif | |
| // The data pointer malloc()ed here has the same lifetime as the emscripten_fetch_t structure itself has, and is | |
| // freed when emscripten_fetch_close() is called. | |
| ptr = _realloc({{{ makeGetValue('fetch', C_STRUCTS.emscripten_fetch_t.data, '*') }}}, ptrLen); | |
| HEAPU8.set(new Uint8Array(/** @type{Array<number>} */(xhr.response)), ptr); | |
| } | |
| {{{ makeSetValue('fetch', C_STRUCTS.emscripten_fetch_t.data, 'ptr', '*') }}} | |
| writeI53ToI64(fetch + {{{ C_STRUCTS.emscripten_fetch_t.numBytes }}}, ptrLen); | |
| writeI53ToI64(fetch + {{{ C_STRUCTS.emscripten_fetch_t.dataOffset }}}, 0); | |
| var len = xhr.response ? xhr.response.byteLength : 0; | |
| if (len) { | |
| // If the final XHR.onload handler receives the bytedata to compute total length, report that, | |
| // otherwise don't write anything out here, which will retain the latest byte size reported in | |
| // the most recent XHR.onprogress handler. | |
| writeI53ToI64(fetch + {{{ C_STRUCTS.emscripten_fetch_t.totalBytes }}}, len); | |
| } | |
| {{{ makeSetValue('fetch', C_STRUCTS.emscripten_fetch_t.readyState, 'xhr.readyState', 'i16') }}} | |
| {{{ makeSetValue('fetch', C_STRUCTS.emscripten_fetch_t.status, 'xhr.status', 'i16') }}} | |
| if (xhr.statusText) stringToUTF8(xhr.statusText, fetch + {{{ C_STRUCTS.emscripten_fetch_t.statusText }}}, 64); | |
| if (fetchAttrSynchronous) { | |
| // The response url pointer malloc()ed here has the same lifetime as the emscripten_fetch_t structure itself has, and is | |
| // freed when emscripten_fetch_close() is called. | |
| var ruPtr = stringToNewUTF8(xhr.responseURL); | |
| {{{ makeSetValue('fetch', C_STRUCTS.emscripten_fetch_t.responseUrl, 'ruPtr', '*') }}} | |
| } | |
| } | |
| xhr.onload = (e) => { | |
| // check if xhr was aborted by user and don't try to call back | |
| if (!Fetch.xhrs.has(id)) { | |
| return; | |
| } | |
| saveResponseAndStatus(); | |
| if (xhr.status >= 200 && xhr.status < 300) { | |
| #if FETCH_DEBUG | |
| dbg(`fetch: xhr of URL "${xhr.url_}" / responseURL "${xhr.responseURL}" succeeded with status ${xhr.status}`); | |
| #endif | |
| #if ASSERTIONS | |
| if (fetchAttrStreamData) { | |
| assert(xhr.response === null); | |
| } | |
| #endif | |
| onsuccess(fetch, xhr, e); | |
| } else { | |
| #if FETCH_DEBUG | |
| dbg(`fetch: xhr of URL "${xhr.url_}" / responseURL "${xhr.responseURL}" failed with status ${xhr.status}`); | |
| #endif | |
| onerror(fetch, e); | |
| } | |
| }; | |
| xhr.onerror = (e) => { | |
| // check if xhr was aborted by user and don't try to call back | |
| if (!Fetch.xhrs.has(id)) { | |
| return; | |
| } | |
| #if FETCH_DEBUG | |
| dbg(`fetch: xhr of URL "${xhr.url_}" / responseURL "${xhr.responseURL}" finished with error, readyState ${xhr.readyState} and status ${xhr.status}`); | |
| #endif | |
| saveResponseAndStatus(); | |
| onerror(fetch, e); | |
| }; | |
| xhr.ontimeout = (e) => { | |
| // check if xhr was aborted by user and don't try to call back | |
| if (!Fetch.xhrs.has(id)) { | |
| return; | |
| } | |
| #if FETCH_DEBUG | |
| dbg(`fetch: xhr of URL "${xhr.url_}" / responseURL "${xhr.responseURL}" timed out, readyState ${xhr.readyState} and status ${xhr.status}`); | |
| #endif | |
| onerror(fetch, e); | |
| }; | |
| xhr.onprogress = (e) => { | |
| // check if xhr was aborted by user and don't try to call back | |
| if (!Fetch.xhrs.has(id)) { | |
| return; | |
| } | |
| var ptrLen = (fetchAttrLoadToMemory && fetchAttrStreamData && xhr.response) ? xhr.response.byteLength : 0; | |
| var ptr = 0; | |
| if (ptrLen > 0 && fetchAttrLoadToMemory && fetchAttrStreamData) { | |
| #if FETCH_DEBUG | |
| dbg(`fetch: allocating ${ptrLen} bytes in Emscripten heap for xhr data`); | |
| #endif | |
| #if ASSERTIONS | |
| assert(onprogress, 'streaming fetch requires an onprogress handler'); | |
| #endif | |
| // The data pointer malloc()ed here has the same lifetime as the emscripten_fetch_t structure itself has, and is | |
| // freed when emscripten_fetch_close() is called. | |
| ptr = _realloc({{{ makeGetValue('fetch', C_STRUCTS.emscripten_fetch_t.data, '*') }}}, ptrLen); | |
| HEAPU8.set(new Uint8Array(/** @type{Array<number>} */(xhr.response)), ptr); | |
| } | |
| {{{ makeSetValue('fetch', C_STRUCTS.emscripten_fetch_t.data, 'ptr', '*') }}} | |
| writeI53ToI64(fetch + {{{ C_STRUCTS.emscripten_fetch_t.numBytes }}}, ptrLen); | |
| writeI53ToI64(fetch + {{{ C_STRUCTS.emscripten_fetch_t.dataOffset }}}, e.loaded - ptrLen); | |
| writeI53ToI64(fetch + {{{ C_STRUCTS.emscripten_fetch_t.totalBytes }}}, e.total); | |
| {{{ makeSetValue('fetch', C_STRUCTS.emscripten_fetch_t.readyState, 'xhr.readyState', 'i16') }}} | |
| var status = xhr.status; | |
| // If loading files from a source that does not give HTTP status code, assume success if we get data bytes | |
| if (xhr.readyState >= 3 && xhr.status === 0 && e.loaded > 0) status = 200; | |
| {{{ makeSetValue('fetch', C_STRUCTS.emscripten_fetch_t.status, 'status', 'i16') }}} | |
| if (xhr.statusText) stringToUTF8(xhr.statusText, fetch + {{{ C_STRUCTS.emscripten_fetch_t.statusText }}}, 64); | |
| onprogress(fetch, e); | |
| }; | |
| xhr.onreadystatechange = (e) => { | |
| // check if xhr was aborted by user and don't try to call back | |
| if (!Fetch.xhrs.has(id)) { | |
| {{{ runtimeKeepalivePop() }}} | |
| return; | |
| } | |
| {{{ makeSetValue('fetch', C_STRUCTS.emscripten_fetch_t.readyState, 'xhr.readyState', 'i16') }}} | |
| if (xhr.readyState >= 2) { | |
| {{{ makeSetValue('fetch', C_STRUCTS.emscripten_fetch_t.status, 'xhr.status', 'i16') }}} | |
| } | |
| if (!fetchAttrSynchronous && (xhr.readyState === 2 && xhr.responseURL.length > 0)) { | |
| // The response url pointer malloc()ed here has the same lifetime as the emscripten_fetch_t structure itself has, and is | |
| // freed when emscripten_fetch_close() is called. | |
| var ruPtr = stringToNewUTF8(xhr.responseURL); | |
| {{{ makeSetValue('fetch', C_STRUCTS.emscripten_fetch_t.responseUrl, 'ruPtr', '*') }}} | |
| } | |
| onreadystatechange(fetch, e); | |
| }; | |
| #if FETCH_DEBUG | |
| dbg(`fetch: xhr.send(data=${data})`); | |
| #endif | |
| try { | |
| xhr.send(data); | |
| } catch(e) { | |
| #if FETCH_DEBUG | |
| dbg(`fetch: xhr failed with exception: ${e}`); | |
| #endif | |
| onerror(fetch, e); | |
| } | |
| } | |
| function startFetch(fetch, successcb, errorcb, progresscb, readystatechangecb) { | |
| // Avoid shutting down the runtime since we want to wait for the async | |
| // response. | |
| {{{ runtimeKeepalivePush() }}} | |
| var fetch_attr = fetch + {{{ C_STRUCTS.emscripten_fetch_t.__attributes }}}; | |
| var onsuccess = {{{ makeGetValue('fetch_attr', C_STRUCTS.emscripten_fetch_attr_t.onsuccess, '*') }}}; | |
| var onerror = {{{ makeGetValue('fetch_attr', C_STRUCTS.emscripten_fetch_attr_t.onerror, '*') }}}; | |
| var onprogress = {{{ makeGetValue('fetch_attr', C_STRUCTS.emscripten_fetch_attr_t.onprogress, '*') }}}; | |
| var onreadystatechange = {{{ makeGetValue('fetch_attr', C_STRUCTS.emscripten_fetch_attr_t.onreadystatechange, '*') }}}; | |
| var fetchAttributes = {{{ makeGetValue('fetch_attr', C_STRUCTS.emscripten_fetch_attr_t.attributes, '*') }}}; | |
| var fetchAttrSynchronous = !!(fetchAttributes & {{{ cDefs.EMSCRIPTEN_FETCH_SYNCHRONOUS }}}); | |
| function doCallback(f) { | |
| if (fetchAttrSynchronous) { | |
| f(); | |
| } else { | |
| callUserCallback(f); | |
| } | |
| } | |
| var reportSuccess = (fetch, xhr, e) => { | |
| #if FETCH_DEBUG | |
| dbg(`fetch: operation success. e: ${e}`); | |
| #endif | |
| {{{ runtimeKeepalivePop() }}} | |
| doCallback(() => { | |
| if (onsuccess) {{{ makeDynCall('vp', 'onsuccess') }}}(fetch); | |
| else successcb?.(fetch); | |
| }); | |
| }; | |
| var reportProgress = (fetch, e) => { | |
| doCallback(() => { | |
| if (onprogress) {{{ makeDynCall('vp', 'onprogress') }}}(fetch); | |
| else progresscb?.(fetch); | |
| }); | |
| }; | |
| var reportError = (fetch, e) => { | |
| #if FETCH_DEBUG | |
| dbg(`fetch: operation failed: ${e}`); | |
| #endif | |
| {{{ runtimeKeepalivePop() }}} | |
| doCallback(() => { | |
| if (onerror) {{{ makeDynCall('vp', 'onerror') }}}(fetch); | |
| else errorcb?.(fetch); | |
| }); | |
| }; | |
| var reportReadyStateChange = (fetch, e) => { | |
| #if FETCH_DEBUG | |
| dbg(`fetch: ready state change. e: ${e}`); | |
| #endif | |
| doCallback(() => { | |
| if (onreadystatechange) {{{ makeDynCall('vp', 'onreadystatechange') }}}(fetch); | |
| else readystatechangecb?.(fetch); | |
| }); | |
| }; | |
| var performUncachedXhr = (fetch, xhr, e) => { | |
| #if FETCH_DEBUG | |
| dbg(`fetch: starting (uncached) XHR: ${e}`); | |
| #endif | |
| fetchXHR(fetch, reportSuccess, reportError, reportProgress, reportReadyStateChange); | |
| }; | |
| #if FETCH_SUPPORT_INDEXEDDB | |
| var cacheResultAndReportSuccess = (fetch, xhr, e) => { | |
| #if FETCH_DEBUG | |
| dbg(`fetch: operation success. Caching result.. e: ${e}`); | |
| #endif | |
| var storeSuccess = (fetch, xhr, e) => { | |
| #if FETCH_DEBUG | |
| dbg('fetch: IndexedDB store succeeded.'); | |
| #endif | |
| {{{ runtimeKeepalivePop() }}} | |
| doCallback(() => { | |
| if (onsuccess) {{{ makeDynCall('vp', 'onsuccess') }}}(fetch); | |
| else successcb?.(fetch); | |
| }); | |
| }; | |
| var storeError = (fetch, xhr, e) => { | |
| #if FETCH_DEBUG | |
| dbg('fetch: IndexedDB store failed.'); | |
| #endif | |
| {{{ runtimeKeepalivePop() }}} | |
| doCallback(() => { | |
| if (onsuccess) {{{ makeDynCall('vp', 'onsuccess') }}}(fetch); | |
| else successcb?.(fetch); | |
| }); | |
| }; | |
| fetchCacheData(Fetch.dbInstance, fetch, xhr.response, storeSuccess, storeError); | |
| }; | |
| var performCachedXhr = (fetch, xhr, e) => { | |
| #if FETCH_DEBUG | |
| dbg(`fetch: starting (cached) XHR: ${e}`); | |
| #endif | |
| fetchXHR(fetch, cacheResultAndReportSuccess, reportError, reportProgress, reportReadyStateChange); | |
| }; | |
| var requestMethod = UTF8ToString(fetch_attr + {{{ C_STRUCTS.emscripten_fetch_attr_t.requestMethod }}}); | |
| var fetchAttrReplace = !!(fetchAttributes & {{{ cDefs.EMSCRIPTEN_FETCH_REPLACE }}}); | |
| var fetchAttrPersistFile = !!(fetchAttributes & {{{ cDefs.EMSCRIPTEN_FETCH_PERSIST_FILE }}}); | |
| var fetchAttrNoDownload = !!(fetchAttributes & {{{ cDefs.EMSCRIPTEN_FETCH_NO_DOWNLOAD }}}); | |
| if (requestMethod === 'EM_IDB_STORE') { | |
| // TODO(?): Here we perform a clone of the data, because storing shared typed arrays to IndexedDB does not seem to be allowed. | |
| var ptr = {{{ makeGetValue('fetch_attr', C_STRUCTS.emscripten_fetch_attr_t.requestData, '*') }}}; | |
| var size = {{{ makeGetValue('fetch_attr', C_STRUCTS.emscripten_fetch_attr_t.requestDataSize, '*') }}}; | |
| fetchCacheData(Fetch.dbInstance, fetch, HEAPU8.slice(ptr, ptr + size), reportSuccess, reportError); | |
| } else if (requestMethod === 'EM_IDB_DELETE') { | |
| fetchDeleteCachedData(Fetch.dbInstance, fetch, reportSuccess, reportError); | |
| } else if (!fetchAttrReplace) { | |
| fetchLoadCachedData(Fetch.dbInstance, fetch, reportSuccess, fetchAttrNoDownload ? reportError : (fetchAttrPersistFile ? performCachedXhr : performUncachedXhr)); | |
| } else if (!fetchAttrNoDownload) { | |
| fetchXHR(fetch, fetchAttrPersistFile ? cacheResultAndReportSuccess : reportSuccess, reportError, reportProgress, reportReadyStateChange); | |
| } else { | |
| #if FETCH_DEBUG | |
| dbg('fetch: Invalid combination of flags passed.'); | |
| #endif | |
| return 0; // todo: free | |
| } | |
| return fetch; | |
| #else // !FETCH_SUPPORT_INDEXEDDB | |
| fetchXHR(fetch, reportSuccess, reportError, reportProgress, reportReadyStateChange); | |
| return fetch; | |
| #endif // ~FETCH_SUPPORT_INDEXEDDB | |
| } | |
| function fetchGetResponseHeadersLength(id) { | |
| return lengthBytesUTF8(Fetch.xhrs.get(id).getAllResponseHeaders()); | |
| } | |
| function fetchGetResponseHeaders(id, dst, dstSizeBytes) { | |
| var responseHeaders = Fetch.xhrs.get(id).getAllResponseHeaders(); | |
| return stringToUTF8(responseHeaders, dst, dstSizeBytes) + 1; | |
| } | |
| // Delete the xhr JS object, allowing it to be garbage collected. | |
| function fetchFree(id) { | |
| #if FETCH_DEBUG | |
| dbg(`fetch: fetchFree id:${id}`); | |
| #endif | |
| if (Fetch.xhrs.has(id)) { | |
| var xhr = Fetch.xhrs.get(id); | |
| Fetch.xhrs.free(id); | |
| // check if fetch is still in progress and should be aborted | |
| if (xhr.readyState > 0 && xhr.readyState < 4) { | |
| xhr.abort(); | |
| } | |
| } | |
| } | |
Xet Storage Details
- Size:
- 33.9 kB
- Xet hash:
- 38ffad6f2a3b7f3669f75aaeae2331008d7cdcf643bf87c18ee2828939a19527
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.