UNPKG

@react-gnome/core

Version:

## Getting Started

179 lines (178 loc) 7.38 kB
// 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 }; });