playcanvas
Version:
Open-source WebGL/WebGPU 3D engine for the web
471 lines (470 loc) • 19.4 kB
JavaScript
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
};