playcanvas
Version:
Open-source WebGL/WebGPU 3D engine for the web
300 lines (299 loc) • 8.1 kB
JavaScript
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";
class Http {
static 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"
};
static ResponseType = {
TEXT: "text",
ARRAY_BUFFER: "arraybuffer",
BLOB: "blob",
DOCUMENT: "document",
JSON: "json"
};
static binaryExtensions = [
".model",
".wav",
".ogg",
".mp3",
".mp4",
".m4a",
".aac",
".dds",
".basis",
".glb",
".opus"
];
static retryDelay = 100;
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;
}
post(url, data, options, callback) {
if (typeof options === "function") {
callback = options;
options = {};
}
options.postdata = data;
return this.request("POST", url, options, callback);
}
put(url, data, options, callback) {
if (typeof options === "function") {
callback = options;
options = {};
}
options.postdata = data;
return this.request("PUT", url, options, callback);
}
del(url, options, callback) {
if (typeof options === "function") {
callback = options;
options = {};
}
return this.request("DELETE", url, options, callback);
}
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);
}
}
}
const http = new Http();
export {
Http,
http
};