@hyper-fetch/core
Version:
Cache, Queue and Persist your requests no matter if you are online or offline!
175 lines (150 loc) • 5.75 kB
text/typescript
import { Adapter } from "../adapter/adapter";
import { HttpMethods } from "constants/http.constants";
import type { HttpMethodsType, HttpStatusType } from "types";
import type { QueryParamsType } from "adapter";
import type { HttpAdapterExtraType, FetchAdapterOptionsType } from "./http-adapter.types";
import { parseErrorResponse, parseResponse, stringifyQueryParams } from "./http-adapter.utils";
import { defaultTimeout, xhrExtra } from "./http-adapter.constants";
const defaultExtra = xhrExtra;
export const getAdapter = () =>
new Adapter<
FetchAdapterOptionsType,
HttpMethodsType,
HttpStatusType,
HttpAdapterExtraType,
QueryParamsType | string | null,
undefined,
string
>({
name: "http",
defaultMethod: HttpMethods.GET,
defaultExtra,
systemErrorStatus: 0 as number,
systemErrorExtra: defaultExtra,
})
.setQueryParamsMapper(stringifyQueryParams)
.setFetcher(
async ({
request,
adapterOptions,
headers,
payload,
onError,
onResponseEnd,
onTimeoutError,
onRequestEnd,
createAbortListener,
getAbortController,
onResponseProgress,
onResponseStart,
onBeforeRequest,
onRequestStart,
onRequestProgress,
onSuccess,
}) => {
const { method, client, endpoint, queryParams } = request;
const queryString = queryParams ? stringifyQueryParams(queryParams) : "";
const fullUrl = `${client.url}${endpoint}${queryString}`;
const controller = getAbortController();
const { timeout: timeoutMs = defaultTimeout, streaming = false, ...restOptions } = adapterOptions || {};
let timedOut = false;
let timeoutId: ReturnType<typeof setTimeout> | undefined;
// Abort listener handles user-initiated aborts (e.g. request.abort())
// For fetch, onAbort is a no-op since the signal is already passed to fetch()
const unmountListener = createAbortListener({
status: 0,
extra: defaultExtra,
});
if (timeoutMs > 0 && controller) {
timeoutId = setTimeout(() => {
timedOut = true;
unmountListener();
controller.abort();
}, timeoutMs);
}
onBeforeRequest();
onRequestStart();
try {
const init: RequestInit = {
method,
headers: headers as Record<string, string>,
signal: controller?.signal,
...restOptions,
};
if (payload && method !== "GET" && method !== "HEAD") {
init.body = payload as BodyInit;
if (typeof payload === "string") {
const total = new Blob([payload]).size;
onRequestProgress({ total, loaded: total } as ProgressEvent);
}
}
onRequestEnd();
const response = await fetch(fullUrl, init);
if (timeoutId) clearTimeout(timeoutId);
onResponseStart();
const responseHeaders: Record<string, string> = {};
response.headers.forEach((value, key) => {
responseHeaders[key] = value;
});
const extra: HttpAdapterExtraType = { headers: responseHeaders };
const { status } = response;
if (streaming && response.body) {
onResponseEnd();
unmountListener();
if (response.ok) {
onSuccess({ data: response.body as any, status, extra });
} else {
const errorBody = await response.text();
const error = parseErrorResponse(errorBody);
onError({ error, status, extra });
}
} else {
let body: string;
if (response.body && typeof response.body.getReader === "function") {
const reader = response.body.getReader();
const contentLength = parseInt(response.headers.get("content-length") || "0", 10);
let receivedLength = 0;
const chunks: Uint8Array[] = [];
// eslint-disable-next-line no-constant-condition
while (true) {
// eslint-disable-next-line no-await-in-loop
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
receivedLength += value.length;
onResponseProgress({ total: contentLength || receivedLength, loaded: receivedLength } as ProgressEvent);
}
const allChunks = new Uint8Array(receivedLength);
let position = 0;
// eslint-disable-next-line no-restricted-syntax
for (const chunk of chunks) {
allChunks.set(chunk, position);
position += chunk.length;
}
body = new TextDecoder().decode(allChunks);
} else {
body = await response.text();
}
onResponseEnd();
unmountListener();
if (response.ok) {
const data = parseResponse(body);
onSuccess({ data, status, extra });
} else {
const error = parseErrorResponse(body);
onError({ error, status, extra });
}
}
} catch (err: any) {
if (timeoutId) clearTimeout(timeoutId);
unmountListener();
if (controller?.signal?.aborted) {
if (timedOut) {
onTimeoutError({ status: 0, extra: defaultExtra });
}
return;
}
onError({ error: err, status: 0, extra: defaultExtra });
}
},
);