Skip to content

Commit

Permalink
feat(core): urbex now supports XHR requests for browser environments
Browse files Browse the repository at this point in the history
  • Loading branch information
Michael committed Nov 24, 2022
1 parent cfe80d5 commit 77f0913
Showing 1 changed file with 88 additions and 12 deletions.
100 changes: 88 additions & 12 deletions lib/core/api/xhr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,37 @@ import type {
ResponseTypes
} from "../../types";

import { UrbexError, TimeoutError } from "../error";
import { resolveRequest } from "./resolve-request";
import { UrbexError, TimeoutError, NetworkError } from "../error";
import { createEmptyScheme, uppercase, forEach, isUndefined, merge } from "../../utils";

interface BindableEventListener {
event: string;
listener: XMLEventListener;
}

type BrowserResponseTypes = "arraybuffer" | "blob" | "document" | "json" | "text";
type XMLProgressEvent = ProgressEvent<EventTarget>;

type XMLEventListener = (this: XMLHttpRequest, ev: XMLProgressEvent) => void;
type XMLEventListeners = BindableEventListener[];

const BROWSER_RESPONSE_TYPES = ["arraybuffer", "blob", "document", "json", "text"];

export class BrowserRequest implements UrbexRequestApi {
public send(config: InternalConfiguration): DispatchedAPIRequest {
return new Promise((resolve, reject) => {
return new Promise((_resolve, _reject) => {
const request = new XMLHttpRequest();

function manageListeners(
listeners: XMLEventListeners,
method: "addEventListener" | "removeEventListener"
) {
for (const { event, listener } of listeners) {
request[method](event, listener);
}
}

request.open(uppercase(config.method), config.url.href, true);

if (BROWSER_RESPONSE_TYPES.includes(config.responseType)) {
Expand All @@ -28,33 +47,87 @@ export class BrowserRequest implements UrbexRequestApi {
config.headers.delete("Content-Type");
}

forEach(config.headers.get(), (key, value) => {
request.setRequestHeader(key, value);
});
forEach(config.headers.get(), request.setRequestHeader.bind(request));

if (config.timeout) {
request.timeout = config.timeout;
}

request.ontimeout = function () {};
function resolve(response: any): void {
return resolveRequest.call({ config, request }, _resolve, _reject, response);
}

function createErrorInstance<T extends typeof UrbexError>(
instance: T
): InstanceType<T> {
return UrbexError.createErrorInstance.call({ config, request }, instance);
}

function onTimeout(this: XMLHttpRequest, ev: XMLProgressEvent): void {
const timeoutError = createErrorInstance(TimeoutError);
timeoutError.timeout = config.timeout;
_reject(timeoutError);

request.onabort = function () {};
manageListeners(listeners, "removeEventListener");
}

request.onerror = function () {};
function onAbort(this: XMLHttpRequest, ev: XMLProgressEvent): void {
const abortError = createErrorInstance(UrbexError);
abortError.message = "The request was aborted.";
_reject(abortError);

// https://plnkr.co/edit/ycQbBr0vr7ceUP2p6PHy?p=preview&preview
manageListeners(listeners, "removeEventListener");
}

function onError(this: XMLHttpRequest, ev: XMLProgressEvent): void {
// https://stackoverflow.com/questions/45067892/xmlhttprequest-onerror-handler-use-case

const networkError = createErrorInstance(NetworkError);
_reject(networkError);

manageListeners(listeners, "removeEventListener");
}

function onLoad(this: XMLHttpRequest, ev: XMLProgressEvent): void {
// Uncaught DOMException: XMLHttpRequest.responseText
// getter: responseText is only available if responseType is '' or 'text'.
// thats why the below function exists

function getResponse() {
if (request.responseType === "document") {
return request.responseXML;
}

if (!request.responseType || request.responseType === "text") {
return request.responseText;
}

return request.response;
}

request.onload = function () {
resolve({
data: request.response || request.responseText,
data: getResponse(),
request: request,
response: {
status: request.status,
statusText: request.statusText,
headers: request.getAllResponseHeaders()
}
});
};

manageListeners(listeners, "removeEventListener");
}

const listeners: XMLEventListeners = [
{ event: "timeout", listener: onTimeout },
{ event: "abort", listener: onAbort },
{ event: "error", listener: onError },
{ event: "load", listener: onLoad }
];

manageListeners(listeners, "addEventListener");

// https://plnkr.co/edit/ycQbBr0vr7ceUP2p6PHy?p=preview&preview

request.onreadystatechange = function () {};

Expand All @@ -63,4 +136,7 @@ export class BrowserRequest implements UrbexRequestApi {
}
}

// this is here because when building for the browser, the http api
// is replaced with the xhr api. And so it doesn't throw errors
// this is here for safety
export const DECODERS = createEmptyScheme(["br", "gzip", "deflate", "compress"]);

0 comments on commit 77f0913

Please sign in to comment.