@react-gnome/core
Version:
## Getting Started
179 lines (178 loc) • 7.38 kB
JavaScript
// src/polyfills/fetch/soup2.ts
import Soup from "gi://Soup?version=2.4";
import { registerPolyfills } from "../shared/polyfill-global.mjs";
registerPolyfills("fetch")(() => {
class AbortError extends Error {
code = 20;
name = "AbortError";
constructor(msg) {
super(msg ?? "The operation was aborted");
}
}
const defaultHeaders = {
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
"Accept-Language": "en-US,en;q=0.9"
};
async function fetch(url, options = {}) {
if (url == null) throw new TypeError("URL must be specified.");
if (typeof url === "object") {
options = url;
if (!options.url) throw new TypeError("URL must be specified.");
url = options.url;
}
if (options.signal?.aborted) throw options.signal.reason ?? new AbortError();
const method = options.method || "GET";
const message = Soup.Message.new(method, url);
if (!message) throw new TypeError(`Invalid URL: ${url}`);
const httpSession = new Soup.SessionAsync();
httpSession.user_agent = defaultHeaders["User-Agent"];
for (const [key, value] of Object.entries(defaultHeaders)) {
message.request_headers.append(key, value);
}
const headers = options.headers || {};
for (const [key, value] of Object.entries(headers)) {
message.request_headers.append(key, String(value) || null);
}
if (options.body) {
if (options.body instanceof URLSearchParams) {
message.set_request("application/x-www-form-urlencoded", Soup.MemoryUse.COPY, new TextEncoder().encode(options.body.toString()));
} else if (options.body instanceof FormData) {
const multipartBoundary = `----WebKitFormBoundary${Math.random().toString(36).slice(2)}`;
const parts = [];
const encoder = new TextEncoder();
const contentType = `multipart/form-data; boundary=${multipartBoundary}`;
for (const [key, value] of options.body.entries()) {
parts.push(encoder.encode(`\r
--${multipartBoundary}\r
`));
if (value instanceof Blob || value instanceof File) {
const blobValue = value;
const filename = value.name || "blob";
parts.push(encoder.encode(`Content-Disposition: form-data; name="${key}"; filename="${filename}"\r
`));
parts.push(encoder.encode(`Content-Type: ${blobValue.type || "application/octet-stream"}\r
\r
`));
const buffer = await blobValue.arrayBuffer();
parts.push(new Uint8Array(buffer));
} else {
parts.push(encoder.encode(`Content-Disposition: form-data; name="${key}"\r
\r
`));
parts.push(encoder.encode(String(value)));
}
}
parts.push(encoder.encode(`\r
--${multipartBoundary}--\r
`));
const totalLength = parts.reduce((sum, part) => sum + part.length, 0);
const body = new Uint8Array(totalLength);
let offset = 0;
for (const part of parts) {
body.set(part, offset);
offset += part.length;
}
message.set_request(contentType, Soup.MemoryUse.COPY, body);
} else if (options.body instanceof ArrayBuffer || options.body && ArrayBuffer.isView(options.body)) {
const body = options.body instanceof ArrayBuffer ? new Uint8Array(options.body) : new Uint8Array(options.body.buffer);
message.set_request("application/octet-stream", Soup.MemoryUse.COPY, body);
} else if (typeof options.body === "string") {
const contentType = options.headers instanceof Headers ? options.headers.get("content-type") : options.headers?.["content-type"];
message.set_request(contentType || "text/plain", Soup.MemoryUse.COPY, new TextEncoder().encode(options.body));
} else {
throw new TypeError("Unsupported body type");
}
}
const redirectMode = options.redirect || "follow";
if (redirectMode === "error" || redirectMode === "manual") {
message.set_flags(Soup.MessageFlags.NO_REDIRECT);
}
let wasAborted = false;
if (options.signal) {
options.signal.addEventListener("abort", () => {
httpSession.abort();
wasAborted = true;
});
}
let redirectCount = 0;
const responseBuffer = await new Promise((resolve, reject) => {
httpSession.queue_message(message, (_, msg) => {
if (!msg?.response_body?.data) {
reject(new TypeError("Failed to fetch"));
return;
}
const status = msg.status_code;
if ((status === 301 || status === 302 || status === 303 || status === 307 || status === 308) && redirectMode !== "error") {
const location = msg.response_headers.get_one("Location");
if (location) {
if (redirectMode === "manual") {
resolve(new TextEncoder().encode(JSON.stringify({ type: "opaqueredirect", url: location })));
return;
}
redirectCount++;
if (redirectCount > 20) {
reject(new TypeError("Maximum redirect count exceeded"));
return;
}
if (status === 303 && method !== "GET") {
msg.method = "GET";
msg.request_body.truncate();
}
}
}
resolve(msg.response_body_data.toArray());
});
});
const { status_code, reason_phrase } = message;
const ok = status_code >= 200 && status_code < 300;
const responseHeaders = new Headers();
message.response_headers.foreach((name, value) => {
responseHeaders.append(name, value);
});
let bodyUsed = false;
const response = {
status: status_code,
statusText: reason_phrase,
ok,
type: status_code === 0 ? "opaqueredirect" : "basic",
url: message.uri?.to_string(true) || url.toString(),
redirected: message.uri?.to_string(true) !== url.toString(),
headers: responseHeaders,
bodyUsed: false,
async json() {
if (bodyUsed) throw new TypeError("Body has already been consumed.");
bodyUsed = true;
this.bodyUsed = true;
return JSON.parse(new TextDecoder().decode(responseBuffer));
},
async text() {
if (bodyUsed) throw new TypeError("Body has already been consumed.");
bodyUsed = true;
this.bodyUsed = true;
return new TextDecoder().decode(responseBuffer);
},
async arrayBuffer() {
if (bodyUsed) throw new TypeError("Body has already been consumed.");
bodyUsed = true;
this.bodyUsed = true;
return responseBuffer.buffer;
},
clone() {
if (bodyUsed) throw new TypeError("Body has already been consumed.");
return {
...this,
bodyUsed: false,
json: this.json.bind({ ...this, bodyUsed: false }),
text: this.text.bind({ ...this, bodyUsed: false }),
arrayBuffer: this.arrayBuffer.bind({ ...this, bodyUsed: false }),
clone: this.clone.bind({ ...this, bodyUsed: false }),
headers: new Headers(this.headers)
};
}
};
if (wasAborted) throw options.signal?.reason ?? new AbortError();
return response;
}
return { fetch };
});