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 | 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, 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 = {}; 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 }); } }, );