Last active
March 7, 2025 15:22
-
-
Save wch/5485051a0d79ffc024d2e7b1c92e7fe8 to your computer and use it in GitHub Desktop.
XMLHttpRequest shim
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// =================================================================== | |
// XMLHttpRequestShim - shim for XMLHttpRequest | |
// This assumes the XMLHttpRequest types from lib.dom.d.ts are available | |
// | |
// Based on https://github.com/apple502j/xhr-shim | |
// =================================================================== | |
const sHeaders = Symbol("headers"); | |
const sRespHeaders = Symbol("response headers"); | |
const sAbortController = Symbol("AbortController"); | |
const sMethod = Symbol("method"); | |
const sURL = Symbol("URL"); | |
const sMIME = Symbol("MIME"); | |
const sDispatch = Symbol("dispatch"); | |
const sErrored = Symbol("errored"); | |
const sTimeout = Symbol("timeout"); | |
const sTimedOut = Symbol("timedOut"); | |
const sIsResponseText = Symbol("isResponseText"); | |
// type XMLHttpRequestResponseType = "" | "text" | "arraybuffer" | "blob" | "json"; | |
// type XMLHttpRequestEventMap = { | |
// abort: Event; | |
// error: Event; | |
// load: Event; | |
// loadend: Event; | |
// loadstart: Event; | |
// progress: Event; | |
// timeout: Event; | |
// }; | |
class XMLHttpRequestShim extends EventTarget implements XMLHttpRequest { | |
/* eslint-disable @typescript-eslint/no-explicit-any */ | |
/* eslint-disable @typescript-eslint/naming-convention */ | |
// Public properties | |
public readyState: number; | |
public response: any; | |
public responseType: XMLHttpRequestResponseType; | |
public responseURL: string; | |
public status: number; | |
public statusText: string; | |
public timeout: number; | |
public withCredentials: boolean; | |
// Symbol-based private properties | |
private [sHeaders]: Record<string, string>; | |
private [sRespHeaders]: Record<string, string>; | |
private [sAbortController]: AbortController; | |
private [sMethod]: string; | |
private [sURL]: string; | |
private [sMIME]: string; | |
private [sErrored]: boolean; | |
private [sTimeout]: number | NodeJS.Timeout; | |
private [sTimedOut]: boolean; | |
private [sIsResponseText]: boolean; | |
// Event handlers | |
public onabort: ((this: XMLHttpRequest, ev: Event) => any) | null; | |
public onerror: ((this: XMLHttpRequest, ev: Event) => any) | null; | |
public onload: ((this: XMLHttpRequest, ev: Event) => any) | null; | |
public onloadend: ((this: XMLHttpRequest, ev: Event) => any) | null; | |
public onloadstart: ((this: XMLHttpRequest, ev: Event) => any) | null; | |
public onprogress: ((this: XMLHttpRequest, ev: Event) => any) | null; | |
public ontimeout: ((this: XMLHttpRequest, ev: Event) => any) | null; | |
public onreadystatechange: ((this: XMLHttpRequest, ev: Event) => any) | null; | |
readonly UNSENT = 0; | |
readonly OPENED = 1; | |
readonly HEADERS_RECEIVED = 2; | |
readonly LOADING = 3; | |
readonly DONE = 4; | |
static readonly UNSENT = 0; | |
static readonly OPENED = 1; | |
static readonly HEADERS_RECEIVED = 2; | |
static readonly LOADING = 3; | |
static readonly DONE = 4; | |
constructor() { | |
super(); | |
this.readyState = XMLHttpRequestShim.UNSENT; | |
this.response = null; | |
this.responseType = ""; | |
this.responseURL = ""; | |
this.status = 0; | |
this.statusText = ""; | |
this.timeout = 0; | |
this.withCredentials = false; | |
this[sHeaders] = Object.create(null); | |
this[sHeaders].accept = "*/*"; | |
this[sRespHeaders] = Object.create(null); | |
this[sAbortController] = new AbortController(); | |
this[sMethod] = ""; | |
this[sURL] = ""; | |
this[sMIME] = ""; | |
this[sErrored] = false; | |
this[sTimeout] = 0; | |
this[sTimedOut] = false; | |
this[sIsResponseText] = true; | |
// Initialize event handlers | |
this.onabort = null; | |
this.onerror = null; | |
this.onload = null; | |
this.onloadend = null; | |
this.onloadstart = null; | |
this.onprogress = null; | |
this.ontimeout = null; | |
this.onreadystatechange = null; | |
} | |
get responseText(): string { | |
if (this[sErrored]) { | |
throw new DOMException( | |
"An error occurred while loading the resource", | |
"InvalidStateError" | |
); | |
} | |
if (this.readyState < this.HEADERS_RECEIVED) return ""; | |
if (this[sIsResponseText]) return this.response as string; | |
throw new DOMException( | |
"Response type not set to text", | |
"InvalidStateError" | |
); | |
} | |
get responseXML(): never { | |
throw new Error("XML not supported"); | |
} | |
private [sDispatch](evt: Event): void { | |
const attr = `on${evt.type}` as keyof XMLHttpRequestShim; | |
const handler = this[attr] as | |
| ((this: XMLHttpRequestShim, ev: Event) => any) | |
| null; | |
if (typeof handler === "function") { | |
this.addEventListener(evt.type, handler.bind(this), { | |
once: true, | |
}); | |
} | |
this.dispatchEvent(evt); | |
} | |
abort(): void { | |
this[sAbortController].abort(); | |
this.status = 0; | |
this.readyState = XMLHttpRequestShim.UNSENT; | |
} | |
open(method: string, url: string): void { | |
this.status = 0; | |
this[sMethod] = method; | |
this[sURL] = url; | |
this.readyState = XMLHttpRequestShim.OPENED; | |
} | |
setRequestHeader(header: string, value: string): void { | |
header = String(header).toLowerCase(); | |
if (typeof this[sHeaders][header] === "undefined") { | |
this[sHeaders][header] = String(value); | |
} else { | |
this[sHeaders][header] += `, ${value}`; | |
} | |
} | |
overrideMimeType(mimeType: string): void { | |
this[sMIME] = String(mimeType); | |
} | |
getAllResponseHeaders(): string { | |
if ( | |
this[sErrored] || | |
this.readyState < XMLHttpRequestShim.HEADERS_RECEIVED | |
) { | |
return ""; | |
} | |
return Object.entries(this[sRespHeaders]) | |
.map(([header, value]) => `${header}: ${value}`) | |
.join("\r\n"); | |
} | |
getResponseHeader(headerName: string): string | null { | |
const value = this[sRespHeaders][String(headerName).toLowerCase()]; | |
return typeof value === "string" ? value : null; | |
} | |
send(body?: Document | XMLHttpRequestBodyInit | null): void { | |
if (this.timeout > 0) { | |
this[sTimeout] = setTimeout(() => { | |
this[sTimedOut] = true; | |
this[sAbortController].abort(); | |
}, this.timeout); | |
} | |
const responseType = this.responseType || "text"; | |
this[sIsResponseText] = responseType === "text"; | |
fetch(this[sURL], { | |
method: this[sMethod] || "GET", | |
signal: this[sAbortController].signal, | |
headers: this[sHeaders], | |
credentials: this.withCredentials ? "include" : "same-origin", | |
body: body as BodyInit, | |
}) | |
.finally(() => { | |
this.readyState = XMLHttpRequestShim.DONE; | |
clearTimeout(this[sTimeout] as NodeJS.Timeout); | |
this[sDispatch](new CustomEvent("loadstart")); | |
}) | |
.then( | |
async (resp) => { | |
this.responseURL = resp.url; | |
this.status = resp.status; | |
this.statusText = resp.statusText; | |
const finalMIME = | |
this[sMIME] || this[sRespHeaders]["content-type"] || "text/plain"; | |
Object.assign(this[sRespHeaders], resp.headers); | |
switch (responseType) { | |
case "text": | |
this.response = await resp.text(); | |
break; | |
case "blob": | |
this.response = new Blob([await resp.arrayBuffer()], { | |
type: finalMIME, | |
}); | |
break; | |
case "arraybuffer": | |
this.response = await resp.arrayBuffer(); | |
break; | |
case "json": | |
this.response = await resp.json(); | |
break; | |
} | |
this[sDispatch](new CustomEvent("load")); | |
}, | |
(err) => { | |
let eventName = "abort"; | |
if (err.name !== "AbortError") { | |
this[sErrored] = true; | |
eventName = "error"; | |
} else if (this[sTimedOut]) { | |
eventName = "timeout"; | |
} | |
this[sDispatch](new CustomEvent(eventName)); | |
} | |
) | |
.finally(() => this[sDispatch](new CustomEvent("loadend"))); | |
} | |
get upload(): XMLHttpRequestUpload { | |
return new XMLHttpRequestUpload(); | |
} | |
} | |
// interface XMLHttpRequestBodyInit { | |
// [key: string]: any; | |
// } | |
// declare global { | |
// // Make XMLHttpRequestShim available globally as XMLHttpRequest | |
// interface Window { | |
// XMLHttpRequest: typeof XMLHttpRequestShim; | |
// } | |
// } | |
// Export the class for direct imports | |
export { XMLHttpRequestShim }; | |
// Make it globally available | |
(globalThis || self).XMLHttpRequest = XMLHttpRequestShim; | |
// // I think this is code for mjs and cjs support, but I'm not sure | |
// if (typeof module === "object" && module.exports) { | |
// module.exports = XMLHttpRequestShim; | |
// } else { | |
// (globalThis || self).XMLHttpRequest = XMLHttpRequestShim; | |
// } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment