@react-gnome/core
Version:
## Getting Started
485 lines (484 loc) • 14.7 kB
JavaScript
// src/polyfills/xml-http-request/soup3.ts
import GLib from "gi://GLib?version=2.0";
import Soup from "gi://Soup?version=3.0";
import { registerPolyfills } from "../shared/polyfill-global.mjs";
registerPolyfills("XMLHttpRequest")(() => {
let RequestEvents;
((RequestEvents2) => {
RequestEvents2["READY_STATE_CHANGE"] = "readystatechange";
RequestEvents2["ABORT"] = "abort";
RequestEvents2["ERROR"] = "error";
RequestEvents2["LOAD"] = "load";
RequestEvents2["LOAD_END"] = "loadend";
RequestEvents2["LOAD_START"] = "loadstart";
RequestEvents2["PROGRESS"] = "progress";
RequestEvents2["TIMEOUT"] = "timeout";
})(RequestEvents || (RequestEvents = {}));
let ReadyState;
((ReadyState2) => {
ReadyState2[ReadyState2["UNSENT"] = 0] = "UNSENT";
ReadyState2[ReadyState2["OPENED"] = 1] = "OPENED";
ReadyState2[ReadyState2["HEADERS_RECEIVED"] = 2] = "HEADERS_RECEIVED";
ReadyState2[ReadyState2["LOADING"] = 3] = "LOADING";
ReadyState2[ReadyState2["DONE"] = 4] = "DONE";
})(ReadyState || (ReadyState = {}));
function typedArrayToString(array) {
const decoder = new TextDecoder();
return decoder.decode(array);
}
class ProgressEvent {
constructor(type, target) {
this.type = type;
this.target = target;
}
loaded = 0;
total = 1;
lengthComputable = false;
}
class EventController {
listeners = /* @__PURE__ */ new Map();
constructor(request) {
this.add("abort" /* ABORT */, (e) => request.onabort?.(e));
this.add("error" /* ERROR */, (e) => request.onerror?.(e));
this.add("load" /* LOAD */, (e) => request.onload?.(e));
this.add("loadend" /* LOAD_END */, (e) => request.onloadend?.(e));
this.add("loadstart" /* LOAD_START */, (e) => request.onloadstart?.(e));
this.add("progress" /* PROGRESS */, (e) => request.onprogress?.(e));
this.add("timeout" /* TIMEOUT */, (e) => request.ontimeout?.(e));
this.add(
"readystatechange" /* READY_STATE_CHANGE */,
(e) => request.onreadystatechange?.(e)
);
}
add(event, listener) {
if (!this.listeners.has(event)) {
this.listeners.set(event, []);
}
this.listeners.get(event).push(listener);
}
remove(event, listener) {
if (!this.listeners.has(event)) {
return;
}
const listeners = this.listeners.get(event);
const index = listeners.indexOf(listener);
if (index !== -1) {
listeners.splice(index, 1);
}
}
emit(event, ev) {
if (!this.listeners.has(event)) {
return;
}
this.listeners.get(event).forEach(async (listener) => {
try {
if (typeof listener === "function") await listener(ev);
else await listener.handleEvent(ev);
} catch (e) {
console.error(e);
}
});
}
clear() {
this.listeners.clear();
}
}
class HeadersList {
headers = /* @__PURE__ */ new Map();
replace(headersString) {
this.headers.clear();
const headers = headersString.split("\r\n");
for (const header of headers) {
const [key, value] = header.split(": ");
this.headers.set(key, value);
}
}
set(name, value) {
this.headers.set(name, value);
}
get(name) {
return this.headers.get(name);
}
getAll() {
let result = "";
this.headers.forEach((value, key) => {
result += `${key}: ${value}\r
`;
});
return result;
}
forEach(callback) {
this.headers.forEach(callback);
}
freeze() {
Object.freeze(this);
Object.freeze(this.headers);
}
clear() {
this.headers.clear();
}
}
class ContentType {
constructor(type) {
this.type = type;
this.content = type.split(";").map((e) => e.trim());
}
content;
is(type) {
return this.content[0] === type;
}
isOfType(type) {
const [mainType] = this.content[0].split("/");
return mainType === type;
}
toString() {
return this.type;
}
toType() {
if (this.isOfType("text")) {
return "text";
} else if (this.is("application/json")) {
return "json";
} else if (this.is("application/octet-stream")) {
return "arraybuffer";
} else if (this.isOfType("image") || this.isOfType("audio") || this.isOfType("video")) {
return "blob";
}
return "";
}
}
class XMLHttpRequest {
// #region enum
DONE = 4 /* DONE */;
HEADERS_RECEIVED = 2 /* HEADERS_RECEIVED */;
LOADING = 3 /* LOADING */;
OPENED = 1 /* OPENED */;
UNSENT = 0 /* UNSENT */;
static DONE = 4 /* DONE */;
static HEADERS_RECEIVED = 2 /* HEADERS_RECEIVED */;
static LOADING = 3 /* LOADING */;
static OPENED = 1 /* OPENED */;
static UNSENT = 0 /* UNSENT */;
// #endregion
// #region unused
withCredentials;
// #endregion
_requestConfig = {
method: "GET",
url: "",
async: true,
username: null,
password: null
};
_currentReadyState = 0 /* UNSENT */;
_responseBuffer = null;
_responseType = null;
_responseURL = null;
_contentType = null;
_eventController = new EventController(this);
_requestHeaders = new HeadersList();
_responseHeaders = new HeadersList();
_status = null;
_statusText = null;
_searchParams = null;
_abortCallback = null;
_body = null;
get readyState() {
return this._currentReadyState;
}
get response() {
if (this._currentReadyState === 4 /* DONE */) {
return this._parseResponseData();
}
return null;
}
get responseText() {
const decoder = new TextDecoder();
if (this._responseBuffer) {
return decoder.decode(this._responseBuffer);
}
return null;
}
get responseXML() {
return null;
}
get responseType() {
if (this._responseType === null) {
return this._contentType?.toType() ?? "";
}
return this._responseType;
}
set responseType(type) {
this._responseType = type;
}
get responseURL() {
return this._responseURL ?? this._getFullUrl();
}
get status() {
return this._status ?? 0;
}
get statusText() {
return this._statusText ?? "";
}
timeout = 6e4;
// #region handlers
onabort = (ev) => {
};
onerror = (ev) => {
};
onload = (ev) => {
};
onloadend = (ev) => {
};
onloadstart = (ev) => {
};
onprogress = (ev) => {
};
ontimeout = (ev) => {
};
// #endregion
_parseResponseData() {
switch (this.responseType) {
case "json":
return JSON.parse(this.responseText);
case "arraybuffer":
return this._responseBuffer.slice();
case "blob":
return new Blob([this._responseBuffer]);
case "text":
case "":
return this.responseText;
}
}
_loadRequestBody(body) {
if (body) {
if (typeof body === "string") {
this._body = body;
return;
}
if (body instanceof ArrayBuffer || "buffer" in body && body.buffer instanceof ArrayBuffer) {
this._body = typedArrayToString(body);
return;
}
if (body instanceof FormData) {
const bodyData = {};
body.forEach((value, key) => {
bodyData[key] = value;
});
this._body = JSON.stringify(bodyData);
return;
}
if (body instanceof URLSearchParams) {
this._searchParams = body;
return;
}
this._body = JSON.stringify(body);
}
}
_setReadyState(readyState) {
if (this._currentReadyState >= readyState) return;
this._currentReadyState = readyState;
this._eventController.emit(
"readystatechange" /* READY_STATE_CHANGE */,
new ProgressEvent("readystatechange" /* READY_STATE_CHANGE */, this)
);
}
_getFullUrl() {
const { url, username, password } = this._requestConfig;
const urlObj = new URL(url);
if (username) {
urlObj.username = username;
}
if (password) {
urlObj.password = password;
}
if (this._searchParams) {
for (const [key, value] of this._searchParams) {
urlObj.searchParams.set(key, value);
}
}
return urlObj.toString();
}
_getTimeout() {
if (this.timeout <= 0) return 60 * 60;
return Math.max(1, Math.floor(this.timeout / 1e3));
}
_getSoupMessage() {
const method = this._requestConfig.method;
const fullUrl = this._getFullUrl();
const uri = GLib.Uri.parse(fullUrl, GLib.UriFlags.NONE);
const message = new Soup.Message({
method,
uri
});
if (!message) {
throw new Error(`Unable to create a message for ${method} ${fullUrl}`);
}
this._requestHeaders.forEach((value, key) => {
message.get_request_headers().append(key, value);
});
if (this._body) {
const contentType = this._requestHeaders.get("Content-Type");
const bytes = new TextEncoder().encode(this._body);
message.set_request_body_from_bytes(
contentType ?? "application/json",
new GLib.Bytes(bytes)
);
}
return message;
}
_finishRequest(status_code) {
const ok = status_code >= 200 && status_code < 300;
this._setReadyState(4 /* DONE */);
this._eventController.emit(
"loadend" /* LOAD_END */,
new ProgressEvent("loadend" /* LOAD_END */, this)
);
if (ok) {
this._eventController.emit(
"load" /* LOAD */,
new ProgressEvent("load" /* LOAD */, this)
);
} else if (status_code === 7) {
this._eventController.emit(
"timeout" /* TIMEOUT */,
new ProgressEvent("timeout" /* TIMEOUT */, this)
);
} else if (status_code === 1) {
this._eventController.emit(
"abort" /* ABORT */,
new ProgressEvent("abort" /* ABORT */, this)
);
} else {
this._eventController.emit(
"error" /* ERROR */,
new ProgressEvent("error" /* ERROR */, this)
);
}
}
async _sendAsync() {
try {
const session = new Soup.Session();
session.timeout = this._getTimeout();
const message = this._getSoupMessage();
this._abortCallback = () => session.abort();
const inputStream = await new Promise((resolve, reject) => {
session.send_async(message, 0, null, (_, result) => {
try {
const stream = session.send_finish(result);
resolve(stream);
} catch (e) {
reject(e);
}
});
});
const responseBuffer = await new Promise((resolve, reject) => {
const chunks = [];
const readChunk = () => {
inputStream.read_bytes_async(4096, 0, null, (_, result) => {
try {
const bytes = inputStream.read_bytes_finish(result);
if (bytes.get_size() === 0) {
resolve(new Uint8Array(Buffer.concat(chunks)));
return;
}
chunks.push(new Uint8Array(bytes.toArray()));
readChunk();
} catch (e) {
reject(e);
}
});
};
readChunk();
});
const { status_code, reason_phrase } = message;
const headers = message.get_response_headers();
this._responseHeaders.clear();
headers.foreach((name, value) => {
this._responseHeaders.set(name, value);
});
this._setReadyState(2 /* HEADERS_RECEIVED */);
const [contentTypeStr] = headers.get_content_type();
this._contentType = contentTypeStr ? new ContentType(contentTypeStr) : null;
this._status = status_code;
this._statusText = reason_phrase;
this._responseBuffer = responseBuffer;
this._responseURL = message.get_uri().to_string();
this._finishRequest(status_code);
} catch (e) {
this._eventController.emit(
"error" /* ERROR */,
new ProgressEvent("error" /* ERROR */, this)
);
}
}
_sendSync() {
const session = new Soup.Session();
session.timeout = this._getTimeout();
const message = this._getSoupMessage();
this._setReadyState(3 /* LOADING */);
this._eventController.emit(
"loadstart" /* LOAD_START */,
new ProgressEvent("loadstart" /* LOAD_START */, this)
);
const inputStream = session.send(message, null);
const bytes = inputStream.read_bytes(4096, null);
const responseBuffer = new Uint8Array(bytes.toArray());
const { status_code, reason_phrase } = message;
const headers = message.get_response_headers();
this._responseHeaders.clear();
headers.foreach((name, value) => {
this._responseHeaders.set(name, value);
});
this._setReadyState(2 /* HEADERS_RECEIVED */);
const [contentTypeStr] = headers.get_content_type();
this._contentType = contentTypeStr ? new ContentType(contentTypeStr) : null;
this._status = status_code;
this._statusText = reason_phrase;
this._responseBuffer = responseBuffer;
this._responseURL = message.get_uri().to_string();
this._finishRequest(status_code);
}
abort() {
this._abortCallback?.();
}
getAllResponseHeaders() {
return this._responseHeaders.getAll();
}
getResponseHeader(header) {
return this._responseHeaders.get(header) ?? null;
}
setRequestHeader(name, value) {
this._requestHeaders.set(name, value);
}
open(method, url, async, username, password) {
this._requestConfig = {
method,
url: url.toString(),
async: async ?? true,
username,
password
};
Object.freeze(this._requestConfig);
this._requestHeaders.freeze();
this._setReadyState(1 /* OPENED */);
}
overrideMimeType(mime) {
}
send(body) {
this._loadRequestBody(body);
if (this._requestConfig.async) {
this._sendAsync();
} else {
this._sendSync();
}
}
addEventListener(type, listener) {
this._eventController.add(type, listener);
}
removeEventListener(type, listener) {
this._eventController.remove(type, listener);
}
}
return { XMLHttpRequest };
});