aboutsummaryrefslogtreecommitdiff
path: root/src/js/thirdparty/undici.js
diff options
context:
space:
mode:
Diffstat (limited to 'src/js/thirdparty/undici.js')
-rw-r--r--src/js/thirdparty/undici.js331
1 files changed, 331 insertions, 0 deletions
diff --git a/src/js/thirdparty/undici.js b/src/js/thirdparty/undici.js
new file mode 100644
index 000000000..cf3968b58
--- /dev/null
+++ b/src/js/thirdparty/undici.js
@@ -0,0 +1,331 @@
+// const { Object } = import.meta.primordials;
+const { EventEmitter } = import.meta.require("events");
+const {
+ Readable,
+ [Symbol.for("::bunternal::")]: { _ReadableFromWeb },
+} = import.meta.require("node:stream");
+
+const ObjectCreate = Object.create;
+const kEmptyObject = ObjectCreate(null);
+
+export var fetch = Bun.fetch;
+export var Response = globalThis.Response;
+export var Headers = globalThis.Headers;
+export var Request = globalThis.Request;
+export var URLSearchParams = globalThis.URLSearchParams;
+export var URL = globalThis.URL;
+export class File extends Blob {}
+export class FileReader extends EventTarget {
+ constructor() {
+ throw new Error("Not implemented yet!");
+ }
+}
+
+export var FormData = globalThis.FormData;
+function notImplemented() {
+ throw new Error("Not implemented in bun");
+}
+
+/**
+ * An object representing a URL.
+ * @typedef {Object} UrlObject
+ * @property {string | number} [port]
+ * @property {string} [path]
+ * @property {string} [pathname]
+ * @property {string} [hostname]
+ * @property {string} [origin]
+ * @property {string} [protocol]
+ * @property {string} [search]
+ */
+
+/**
+ * @typedef {import('http').IncomingHttpHeaders} IncomingHttpHeaders
+ * @typedef {'GET' | 'HEAD' | 'POST' | 'PUT' | 'DELETE' | 'CONNECT' | 'OPTIONS' | 'TRACE' | 'PATCH'} HttpMethod
+ * @typedef {import('stream').Readable} Readable
+ * @typedef {import('events').EventEmitter} EventEmitter
+ */
+
+class BodyReadable extends _ReadableFromWeb {
+ #response;
+ #bodyUsed;
+
+ constructor(response, options = {}) {
+ var { body } = response;
+ if (!body) throw new Error("Response body is null");
+ super(options, body);
+
+ this.#response = response;
+ this.#bodyUsed = response.bodyUsed;
+ }
+
+ get bodyUsed() {
+ // return this.#response.bodyUsed;
+ return this.#bodyUsed;
+ }
+
+ #consume() {
+ if (this.#bodyUsed) throw new TypeError("unusable");
+ this.#bodyUsed = true;
+ }
+
+ async arrayBuffer() {
+ this.#consume();
+ return await this.#response.arrayBuffer();
+ }
+
+ async blob() {
+ this.#consume();
+ return await this.#response.blob();
+ }
+
+ async formData() {
+ this.#consume();
+ return await this.#response.formData();
+ }
+
+ async json() {
+ this.#consume();
+ return await this.#response.json();
+ }
+
+ async text() {
+ this.#consume();
+ return await this.#response.text();
+ }
+}
+
+// NOT IMPLEMENTED
+// * idempotent?: boolean;
+// * onInfo?: (info: { statusCode: number, headers: Object<string, string | string[]> }) => void;
+// * opaque?: *;
+// * responseHeader: 'raw' | null;
+// * headersTimeout?: number | null;
+// * bodyTimeout?: number | null;
+// * upgrade?: boolean | string | null;
+// * blocking?: boolean;
+
+/**
+ * Performs an HTTP request.
+ * @param {string | URL | UrlObject} url
+ * @param {{
+ * dispatcher: Dispatcher;
+ * method: HttpMethod;
+ * signal?: AbortSignal | EventEmitter | null;
+ * maxRedirections?: number;
+ * body?: string | Buffer | Uint8Array | Readable | null | FormData;
+ * headers?: IncomingHttpHeaders | string[] | null;
+ * query?: Record<string, any>;
+ * reset?: boolean;
+ * throwOnError?: boolean;
+ * }} [options]
+ * @returns {{
+ * statusCode: number;
+ * headers: IncomingHttpHeaders;
+ * body: ResponseBody;
+ * trailers: Object<string, string>;
+ * opaque: *;
+ * context: Object<string, *>;
+ * }}
+ */
+export async function request(
+ url,
+ options = {
+ method: "GET",
+ signal: null,
+ headers: null,
+ query: null,
+ // idempotent: false, // GET and HEAD requests are idempotent by default
+ // blocking = false,
+ // upgrade = false,
+ // headersTimeout: 30000,
+ // bodyTimeout: 30000,
+ reset: false,
+ throwOnError: false,
+ body: null,
+ // dispatcher,
+ },
+) {
+ let {
+ method = "GET",
+ headers: inputHeaders,
+ query,
+ signal,
+ // idempotent, // GET and HEAD requests are idempotent by default
+ // blocking = false,
+ // upgrade = false,
+ // headersTimeout = 30000,
+ // bodyTimeout = 30000,
+ reset = false,
+ throwOnError = false,
+ body: inputBody,
+ maxRedirections,
+ // dispatcher,
+ } = options;
+
+ // TODO: More validations
+
+ if (typeof url === "string") {
+ if (query) url = new URL(url);
+ } else if (typeof url === "object" && url !== null) {
+ if (!(url instanceof URL)) {
+ // TODO: Parse undici UrlObject
+ throw new Error("not implemented");
+ }
+ } else throw new TypeError("url must be a string, URL, or UrlObject");
+
+ if (typeof url === "string" && query) url = new URL(url);
+ if (typeof url === "object" && url !== null && query) if (query) url.search = new URLSearchParams(query).toString();
+
+ method = method && typeof method === "string" ? method.toUpperCase() : null;
+ // idempotent = idempotent === undefined ? method === "GET" || method === "HEAD" : idempotent;
+
+ if (inputBody && (method === "GET" || method === "HEAD")) {
+ throw new Error("Body not allowed for GET or HEAD requests");
+ }
+
+ if (inputBody && inputBody.read && inputBody instanceof Readable) {
+ // TODO: Streaming via ReadableStream?
+ let data = "";
+ inputBody.setEncoding("utf8");
+ for await (const chunk of stream) {
+ data += chunk;
+ }
+ inputBody = new TextEncoder().encode(data);
+ }
+
+ if (maxRedirections !== undefined && Number.isNaN(maxRedirections)) {
+ throw new Error("maxRedirections must be a number if defined");
+ }
+
+ if (signal && !(signal instanceof AbortSignal)) {
+ // TODO: Add support for event emitter signal
+ throw new Error("signal must be an instance of AbortSignal");
+ }
+
+ let resp;
+ /** @type {Response} */
+ const {
+ status: statusCode,
+ headers,
+ trailers,
+ } = (resp = await fetch(url, {
+ signal,
+ mode: "cors",
+ method,
+ headers: inputHeaders || kEmptyObject,
+ body: inputBody,
+ redirect: maxRedirections === "undefined" || maxRedirections > 0 ? "follow" : "manual",
+ keepalive: !reset,
+ }));
+
+ // Throw if received 4xx or 5xx response indicating HTTP error
+ if (throwOnError && statusCode >= 400 && statusCode < 600) {
+ throw new Error(`Request failed with status code ${statusCode}`);
+ }
+
+ const body = resp.body ? new BodyReadable(resp) : null;
+
+ return { statusCode, headers: headers.toJSON(), body, trailers, opaque: kEmptyObject, context: kEmptyObject };
+}
+
+export function stream() {
+ throw new Error("Not implemented in bun");
+}
+export function pipeline() {
+ throw new Error("Not implemented in bun");
+}
+export function connect() {
+ throw new Error("Not implemented in bun");
+}
+export function upgrade() {
+ throw new Error("Not implemented in bun");
+}
+
+export class MockClient {
+ constructor() {
+ throw new Error("Not implemented in bun");
+ }
+}
+export class MockPool {
+ constructor() {
+ throw new Error("Not implemented in bun");
+ }
+}
+export class MockAgent {
+ constructor() {
+ throw new Error("Not implemented in bun");
+ }
+}
+
+export function mockErrors() {
+ throw new Error("Not implemented in bun");
+}
+
+export function Undici() {
+ throw new Error("Not implemented in bun");
+}
+
+class Dispatcher extends EventEmitter {}
+class Agent extends Dispatcher {}
+class Pool extends Dispatcher {
+ request() {
+ throw new Error("Not implemented in bun");
+ }
+}
+class BalancedPool extends Dispatcher {}
+class Client extends Dispatcher {
+ request() {
+ throw new Error("Not implemented in bun");
+ }
+}
+
+Undici.Dispatcher = Dispatcher;
+Undici.Pool = Pool;
+Undici.BalancedPool = BalancedPool;
+Undici.Client = Client;
+Undici.Agent = Agent;
+
+Undici.buildConnector =
+ Undici.errors =
+ Undici.setGlobalDispatcher =
+ Undici.getGlobalDispatcher =
+ Undici.request =
+ Undici.stream =
+ Undici.pipeline =
+ Undici.connect =
+ Undici.upgrade =
+ Undici.MockClient =
+ Undici.MockPool =
+ Undici.MockAgent =
+ Undici.mockErrors =
+ notImplemented;
+
+Undici.fetch = fetch;
+
+export default {
+ fetch,
+ Response,
+ Headers,
+ Request,
+ URLSearchParams,
+ URL,
+ File,
+ FileReader,
+ FormData,
+ request,
+ stream,
+ pipeline,
+ connect,
+ upgrade,
+ MockClient,
+ MockPool,
+ MockAgent,
+ mockErrors,
+ Dispatcher,
+ Pool,
+ BalancedPool,
+ Client,
+ Agent,
+ Undici,
+ [Symbol.for("CommonJS")]: 0,
+};