UNPKG

playcanvas

Version:

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

471 lines (470 loc) 19.4 kB
var __defProp = Object.defineProperty; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); import { extend } from "../../core/core.js"; import { now } from "../../core/time.js"; import { path } from "../../core/path.js"; import { URI } from "../../core/uri.js"; import { math } from "../../core/math/math.js"; const _Http = class _Http { /** * Perform an HTTP GET request to the given url with additional options such as headers, * retries, credentials, etc. * * @param {string} url - The URL to make the request to. * @param {object} options - Additional options. * @param {Object<string, string>} [options.headers] - HTTP headers to add to the request. * @param {boolean} [options.async] - Make the request asynchronously. Defaults to true. * @param {boolean} [options.cache] - If false, then add a timestamp to the request to prevent caching. * @param {boolean} [options.withCredentials] - Send cookies with this request. Defaults to false. * @param {string} [options.responseType] - Override the response type. * @param {Document|object} [options.postdata] - Data to send in the body of the request. * Some content types are handled automatically. If postdata is an XML Document, it is handled. If * the Content-Type header is set to 'application/json' then the postdata is JSON stringified. * Otherwise, by default, the data is sent as form-urlencoded. * @param {boolean} [options.retry] - If true then if the request fails it will be retried with an exponential backoff. * @param {number} [options.maxRetries] - If options.retry is true this specifies the maximum number of retries. Defaults to 5. * @param {number} [options.maxRetryDelay] - If options.retry is true this specifies the maximum amount of time to wait between retries in milliseconds. Defaults to 5000. * @param {EventHandler} [options.progress] - Object to use for firing progress events. * @param {HttpResponseCallback} callback - The callback used when the response has returned. Passed (err, data) * where data is the response (format depends on response type: text, Object, ArrayBuffer, XML) and * err is the error code. * @example * pc.http.get("http://example.com/", { * "retry": true, * "maxRetries": 5 * }, (err, response) => { * console.log(response); * }); * @returns {XMLHttpRequest} The request object. */ get(url, options, callback) { if (typeof options === "function") { callback = options; options = {}; } const result = this.request("GET", url, options, callback); const { progress } = options; if (progress) { const handler = (event) => { if (event.lengthComputable) { progress.fire("progress", event.loaded, event.total); } }; const endHandler = (event) => { handler(event); result.removeEventListener("loadstart", handler); result.removeEventListener("progress", handler); result.removeEventListener("loadend", endHandler); }; result.addEventListener("loadstart", handler); result.addEventListener("progress", handler); result.addEventListener("loadend", endHandler); } return result; } /** * Perform an HTTP POST request to the given url with additional options such as headers, * retries, credentials, etc. * * @param {string} url - The URL to make the request to. * @param {object} data - Data to send in the body of the request. * Some content types are handled automatically. If postdata is an XML Document, it is handled. * If the Content-Type header is set to 'application/json' then the postdata is JSON * stringified. Otherwise, by default, the data is sent as form-urlencoded. * @param {object} options - Additional options. * @param {Object<string, string>} [options.headers] - HTTP headers to add to the request. * @param {boolean} [options.async] - Make the request asynchronously. Defaults to true. * @param {boolean} [options.cache] - If false, then add a timestamp to the request to prevent caching. * @param {boolean} [options.withCredentials] - Send cookies with this request. Defaults to false. * @param {string} [options.responseType] - Override the response type. * @param {boolean} [options.retry] - If true then if the request fails it will be retried with an exponential backoff. * @param {number} [options.maxRetries] - If options.retry is true this specifies the maximum * number of retries. Defaults to 5. * @param {number} [options.maxRetryDelay] - If options.retry is true this specifies the * maximum amount of time to wait between retries in milliseconds. Defaults to 5000. * @param {HttpResponseCallback} callback - The callback used when the response has returned. * Passed (err, data) where data is the response (format depends on response type: text, * Object, ArrayBuffer, XML) and err is the error code. * @example * pc.http.post("http://example.com/", { * "name": "Alex" * }, { * "retry": true, * "maxRetries": 5 * }, (err, response) => { * console.log(response); * }); * @returns {XMLHttpRequest} The request object. */ post(url, data, options, callback) { if (typeof options === "function") { callback = options; options = {}; } options.postdata = data; return this.request("POST", url, options, callback); } /** * Perform an HTTP PUT request to the given url with additional options such as headers, * retries, credentials, etc. * * @param {string} url - The URL to make the request to. * @param {Document|object} data - Data to send in the body of the request. Some content types * are handled automatically. If postdata is an XML Document, it is handled. If the * Content-Type header is set to 'application/json' then the postdata is JSON stringified. * Otherwise, by default, the data is sent as form-urlencoded. * @param {object} options - Additional options. * @param {Object<string, string>} [options.headers] - HTTP headers to add to the request. * @param {boolean} [options.async] - Make the request asynchronously. Defaults to true. * @param {boolean} [options.cache] - If false, then add a timestamp to the request to prevent caching. * @param {boolean} [options.withCredentials] - Send cookies with this request. Defaults to false. * @param {string} [options.responseType] - Override the response type. * @param {boolean} [options.retry] - If true then if the request fails it will be retried with * an exponential backoff. * @param {number} [options.maxRetries] - If options.retry is true this specifies the maximum * number of retries. Defaults to 5. * @param {number} [options.maxRetryDelay] - If options.retry is true this specifies the * maximum amount of time to wait between retries in milliseconds. Defaults to 5000. * @param {HttpResponseCallback} callback - The callback used when the response has returned. * Passed (err, data) where data is the response (format depends on response type: text, * Object, ArrayBuffer, XML) and err is the error code. * @example * pc.http.put("http://example.com/", { * "name": "Alex" * }, { * "retry": true, * "maxRetries": 5 * }, (err, response) => { * console.log(response); * }); * @returns {XMLHttpRequest} The request object. */ put(url, data, options, callback) { if (typeof options === "function") { callback = options; options = {}; } options.postdata = data; return this.request("PUT", url, options, callback); } /** * Perform an HTTP DELETE request to the given url with additional options such as headers, * retries, credentials, etc. * * @param {string} url - The URL to make the request to. * @param {object} options - Additional options. * @param {Object<string, string>} [options.headers] - HTTP headers to add to the request. * @param {boolean} [options.async] - Make the request asynchronously. Defaults to true. * @param {boolean} [options.cache] - If false, then add a timestamp to the request to prevent caching. * @param {boolean} [options.withCredentials] - Send cookies with this request. Defaults to false. * @param {string} [options.responseType] - Override the response type. * @param {Document|object} [options.postdata] - Data to send in the body of the request. * Some content types are handled automatically. If postdata is an XML Document, it is handled. * If the Content-Type header is set to 'application/json' then the postdata is JSON * stringified. Otherwise, by default, the data is sent as form-urlencoded. * @param {boolean} [options.retry] - If true then if the request fails it will be retried with * an exponential backoff. * @param {number} [options.maxRetries] - If options.retry is true this specifies the maximum * number of retries. Defaults to 5. * @param {number} [options.maxRetryDelay] - If options.retry is true this specifies the * maximum amount of time to wait between retries in milliseconds. Defaults to 5000. * @param {HttpResponseCallback} callback - The callback used when the response has returned. * Passed (err, data) where data is the response (format depends on response type: text, * Object, ArrayBuffer, XML) and err is the error code. * @example * pc.http.del("http://example.com/", { * "retry": true, * "maxRetries": 5 * }, (err, response) => { * console.log(response); * }); * @returns {XMLHttpRequest} The request object. */ del(url, options, callback) { if (typeof options === "function") { callback = options; options = {}; } return this.request("DELETE", url, options, callback); } /** * Make a general purpose HTTP request with additional options such as headers, retries, * credentials, etc. * * @param {string} method - The HTTP method "GET", "POST", "PUT", "DELETE". * @param {string} url - The url to make the request to. * @param {object} options - Additional options. * @param {Object<string, string>} [options.headers] - HTTP headers to add to the request. * @param {boolean} [options.async] - Make the request asynchronously. Defaults to true. * @param {boolean} [options.cache] - If false, then add a timestamp to the request to prevent caching. * @param {boolean} [options.withCredentials] - Send cookies with this request. Defaults to false. * @param {boolean} [options.retry] - If true then if the request fails it will be retried with * an exponential backoff. * @param {number} [options.maxRetries] - If options.retry is true this specifies the maximum * number of retries. Defaults to 5. * @param {number} [options.maxRetryDelay] - If options.retry is true this specifies the * maximum amount of time to wait between retries in milliseconds. Defaults to 5000. * @param {string} [options.responseType] - Override the response type. * @param {Document|object} [options.postdata] - Data to send in the body of the request. * Some content types are handled automatically. If postdata is an XML Document, it is handled. * If the Content-Type header is set to 'application/json' then the postdata is JSON * stringified. Otherwise, by default, the data is sent as form-urlencoded. * @param {HttpResponseCallback} callback - The callback used when the response has returned. * Passed (err, data) where data is the response (format depends on response type: text, * Object, ArrayBuffer, XML) and err is the error code. * @example * pc.http.request("get", "http://example.com/", { * "retry": true, * "maxRetries": 5 * }, (err, response) => { * console.log(response); * }); * @returns {XMLHttpRequest} The request object. */ request(method, url, options, callback) { let uri, query, postdata; let errored = false; if (typeof options === "function") { callback = options; options = {}; } if (options.retry) { options = Object.assign({ retries: 0, maxRetries: 5 }, options); } options.callback = callback; if (options.async == null) { options.async = true; } if (options.headers == null) { options.headers = {}; } if (options.postdata != null) { if (options.postdata instanceof Document) { postdata = options.postdata; } else if (options.postdata instanceof FormData) { postdata = options.postdata; } else if (options.postdata instanceof Object) { let contentType = options.headers["Content-Type"]; if (contentType === void 0) { options.headers["Content-Type"] = _Http.ContentType.FORM_URLENCODED; contentType = options.headers["Content-Type"]; } switch (contentType) { case _Http.ContentType.FORM_URLENCODED: { postdata = ""; let bFirstItem = true; for (const key in options.postdata) { if (options.postdata.hasOwnProperty(key)) { if (bFirstItem) { bFirstItem = false; } else { postdata += "&"; } const encodedKey = encodeURIComponent(key); const encodedValue = encodeURIComponent(options.postdata[key]); postdata += `${encodedKey}=${encodedValue}`; } } break; } default: case _Http.ContentType.JSON: if (contentType == null) { options.headers["Content-Type"] = _Http.ContentType.JSON; } postdata = JSON.stringify(options.postdata); break; } } else { postdata = options.postdata; } } if (options.cache === false) { const timestamp = now(); uri = new URI(url); if (!uri.query) { uri.query = `ts=${timestamp}`; } else { uri.query = `${uri.query}&ts=${timestamp}`; } url = uri.toString(); } if (options.query) { uri = new URI(url); query = extend(uri.getQuery(), options.query); uri.setQuery(query); url = uri.toString(); } const xhr = new XMLHttpRequest(); xhr.open(method, url, options.async); xhr.withCredentials = options.withCredentials !== void 0 ? options.withCredentials : false; xhr.responseType = options.responseType || this._guessResponseType(url); for (const header in options.headers) { if (options.headers.hasOwnProperty(header)) { xhr.setRequestHeader(header, options.headers[header]); } } xhr.onreadystatechange = () => { this._onReadyStateChange(method, url, options, xhr); }; xhr.onerror = () => { this._onError(method, url, options, xhr); errored = true; }; try { xhr.send(postdata); } catch (e) { if (!errored) { options.error(xhr.status, xhr, e); } } return xhr; } _guessResponseType(url) { const uri = new URI(url); const ext = path.getExtension(uri.path).toLowerCase(); if (_Http.binaryExtensions.indexOf(ext) >= 0) { return _Http.ResponseType.ARRAY_BUFFER; } else if (ext === ".json") { return _Http.ResponseType.JSON; } else if (ext === ".xml") { return _Http.ResponseType.DOCUMENT; } return _Http.ResponseType.TEXT; } _isBinaryContentType(contentType) { const binTypes = [ _Http.ContentType.BASIS, _Http.ContentType.BIN, _Http.ContentType.DDS, _Http.ContentType.GLB, _Http.ContentType.MP3, _Http.ContentType.MP4, _Http.ContentType.OGG, _Http.ContentType.OPUS, _Http.ContentType.WAV ]; if (binTypes.indexOf(contentType) >= 0) { return true; } return false; } _isBinaryResponseType(responseType) { return responseType === _Http.ResponseType.ARRAY_BUFFER || responseType === _Http.ResponseType.BLOB || responseType === _Http.ResponseType.JSON; } _onReadyStateChange(method, url, options, xhr) { if (xhr.readyState === 4) { switch (xhr.status) { case 0: { if (xhr.responseURL && xhr.responseURL.startsWith("file:///")) { this._onSuccess(method, url, options, xhr); } else { this._onError(method, url, options, xhr); } break; } case 200: case 201: case 206: case 304: { this._onSuccess(method, url, options, xhr); break; } default: { this._onError(method, url, options, xhr); break; } } } } _onSuccess(method, url, options, xhr) { let response; let contentType; const header = xhr.getResponseHeader("Content-Type"); if (header) { const parts = header.split(";"); contentType = parts[0].trim(); } try { if (this._isBinaryContentType(contentType) || this._isBinaryResponseType(xhr.responseType)) { response = xhr.response; } else if (contentType === _Http.ContentType.JSON || url.split("?")[0].endsWith(".json")) { response = JSON.parse(xhr.responseText); } else if (xhr.responseType === _Http.ResponseType.DOCUMENT || contentType === _Http.ContentType.XML) { response = xhr.responseXML; } else { response = xhr.responseText; } options.callback(null, response); } catch (err) { options.callback(err); } } _onError(method, url, options, xhr) { if (options.retrying) { return; } if (options.retry && options.retries < options.maxRetries) { options.retries++; options.retrying = true; const retryDelay = math.clamp(Math.pow(2, options.retries) * _Http.retryDelay, 0, options.maxRetryDelay || 5e3); console.log(`${method}: ${url} - Error ${xhr.status}. Retrying in ${retryDelay} ms`); setTimeout(() => { options.retrying = false; this.request(method, url, options, options.callback); }, retryDelay); } else { options.callback(xhr.status === 0 ? "Network error" : xhr.status, null); } } }; __publicField(_Http, "ContentType", { AAC: "audio/aac", BASIS: "image/basis", BIN: "application/octet-stream", DDS: "image/dds", FORM_URLENCODED: "application/x-www-form-urlencoded", GIF: "image/gif", GLB: "model/gltf-binary", JPEG: "image/jpeg", JSON: "application/json", MP3: "audio/mpeg", MP4: "audio/mp4", OGG: "audio/ogg", OPUS: 'audio/ogg; codecs="opus"', PNG: "image/png", TEXT: "text/plain", WAV: "audio/x-wav", XML: "application/xml" }); __publicField(_Http, "ResponseType", { TEXT: "text", ARRAY_BUFFER: "arraybuffer", BLOB: "blob", DOCUMENT: "document", JSON: "json" }); __publicField(_Http, "binaryExtensions", [ ".model", ".wav", ".ogg", ".mp3", ".mp4", ".m4a", ".aac", ".dds", ".basis", ".glb", ".opus" ]); __publicField(_Http, "retryDelay", 100); let Http = _Http; const http = new Http(); export { Http, http };