UNPKG

@react-gnome/core

Version:

## Getting Started

207 lines (206 loc) 7.16 kB
// 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 }; });