@react-gnome/core
Version:
## Getting Started
207 lines (206 loc) • 7.16 kB
JavaScript
// src/polyfills/fetch/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("fetch")(() => {
class AbortError extends Error {
code = 20;
constructor(msg) {
super(msg ?? "signal is aborted without reason");
this.name = "AbortError";
}
}
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) {
const abortReason = options.signal.reason;
throw abortReason ?? new AbortError();
}
const method = options.method || "GET";
const uri = GLib.Uri.parse(url.toString(), GLib.UriFlags.NONE);
const message = new Soup.Message({
method,
uri
});
if (!message) throw new TypeError(`Invalid URL: ${url}`);
const session = new Soup.Session();
session.user_agent = defaultHeaders["User-Agent"];
if (options.redirect === "error") {
message.flags |= Soup.MessageFlags.NO_REDIRECT;
}
let wasAborted = false;
if (options.signal) {
options.signal.addEventListener("abort", () => {
session.abort();
wasAborted = true;
});
}
for (const [key, value] of Object.entries(defaultHeaders)) {
message.get_request_headers().append(key, value);
}
const headers = options.headers || {};
if (headers instanceof Headers) {
headers.forEach((value, key) => {
message.get_request_headers().append(key, value || null);
});
} else {
for (const [key, value] of Object.entries(headers)) {
message.get_request_headers().append(key, String(value) || null);
}
}
if (typeof options.body === "string") {
const bytes = new TextEncoder().encode(options.body);
const contentType = options.headers instanceof Headers ? options.headers.get("content-type") : options.headers?.["content-type"];
message.set_request_body_from_bytes(
contentType || "text/plain",
new GLib.Bytes(bytes)
);
} else if (options.body instanceof FormData) {
const multipartBoundary = `----WebKitFormBoundary${Math.random().toString(36).slice(2)}`;
const parts = [];
const encoder = new TextEncoder();
let formContentType = `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 formBody = new Uint8Array(totalLength);
let offset = 0;
for (const part of parts) {
formBody.set(part, offset);
offset += part.length;
}
message.set_request_body_from_bytes(
formContentType,
new GLib.Bytes(formBody)
);
} else if (options.body instanceof ArrayBuffer || options.body && ArrayBuffer.isView(options.body)) {
const bodyArray = options.body instanceof ArrayBuffer ? new Uint8Array(options.body) : new Uint8Array(options.body.buffer);
message.set_request_body_from_bytes(
"application/octet-stream",
new GLib.Bytes(bodyArray)
);
}
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(new TypeError("Failed to fetch"));
}
});
});
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(new TypeError("Failed to read response body"));
}
});
};
readChunk();
});
const { status_code, reason_phrase } = message;
const ok = status_code >= 200 && status_code < 300;
const responseHeaders = new Headers();
message.get_response_headers().foreach((name, value) => {
responseHeaders.append(name, value);
});
let bodyUsed = false;
const response = {
status: status_code,
statusText: reason_phrase,
ok,
type: "basic",
url: url.toString(),
redirected: false,
headers: responseHeaders,
bodyUsed: false,
async json() {
if (bodyUsed) {
throw new TypeError("Body has already been consumed.");
}
bodyUsed = true;
this.bodyUsed = true;
const decoder = new TextDecoder();
const responseBody = decoder.decode(responseBuffer);
return JSON.parse(responseBody);
},
async text() {
if (bodyUsed) {
throw new TypeError("Body has already been consumed.");
}
bodyUsed = true;
this.bodyUsed = true;
const decoder = new TextDecoder();
const responseBody = decoder.decode(responseBuffer);
return responseBody;
},
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 };
}
};
if (wasAborted) {
const abortReason = options.signal?.reason;
throw abortReason ?? new AbortError();
}
return response;
}
return { fetch };
});