UNPKG

@happy-ts/fetch-t

Version:

Abortable fetch wrapper with the ability to specify the return type.

151 lines (146 loc) 4.04 kB
import { Err, Ok } from 'happy-rusty'; import invariant from 'tiny-invariant'; const ABORT_ERROR = "AbortError"; const TIMEOUT_ERROR = "TimeoutError"; class FetchError extends Error { /** * The name of the error. */ name = "FetchError"; /** * The status code of the response. */ status = 0; constructor(message, status) { super(message); this.status = status; } } function fetchT(url, init) { if (typeof url !== "string") { invariant(url instanceof URL, () => `Url must be a string or URL object but received ${url}.`); } const { // default not abort able abortable = false, responseType, timeout, onProgress, onChunk, ...rest } = init ?? {}; const shouldWaitTimeout = timeout != null; let cancelTimer; if (shouldWaitTimeout) { invariant(typeof timeout === "number" && timeout > 0, () => `Timeout must be a number greater than 0 but received ${timeout}.`); } let controller; if (abortable || shouldWaitTimeout) { controller = new AbortController(); rest.signal = controller.signal; } const response = fetch(url, rest).then(async (res) => { cancelTimer?.(); if (!res.ok) { await res.body?.cancel(); return Err(new FetchError(res.statusText, res.status)); } if (res.body) { const shouldNotifyProgress = typeof onProgress === "function"; const shouldNotifyChunk = typeof onChunk === "function"; if (shouldNotifyProgress || shouldNotifyChunk) { const [stream1, stream2] = res.body.tee(); const reader = stream1.getReader(); let totalByteLength = null; let completedByteLength = 0; if (shouldNotifyProgress) { const contentLength = res.headers.get("content-length") ?? res.headers.get("Content-Length"); if (contentLength == null) { onProgress(Err(new Error("No content-length in response headers."))); } else { totalByteLength = parseInt(contentLength, 10); } } reader.read().then(function notify({ done, value }) { if (done) { return; } if (shouldNotifyChunk) { onChunk(value); } if (shouldNotifyProgress && totalByteLength != null) { completedByteLength += value.byteLength; onProgress(Ok({ totalByteLength, completedByteLength })); } reader.read().then(notify); }); res = new Response(stream2, { headers: res.headers, status: res.status, statusText: res.statusText }); } } switch (responseType) { case "arraybuffer": { return Ok(await res.arrayBuffer()); } case "blob": { return Ok(await res.blob()); } case "json": { try { return Ok(await res.json()); } catch { return Err(new Error("Response is invalid json while responseType is json")); } } case "text": { return Ok(await res.text()); } default: { return Ok(res); } } }).catch((err) => { cancelTimer?.(); return Err(err); }); if (shouldWaitTimeout) { const timer = setTimeout(() => { if (!controller.signal.aborted) { const error = new Error(); error.name = TIMEOUT_ERROR; controller.abort(error); } }, timeout); cancelTimer = () => { if (timer) { clearTimeout(timer); } cancelTimer = null; }; } if (abortable) { return { // eslint-disable-next-line @typescript-eslint/no-explicit-any abort(reason) { cancelTimer?.(); controller.abort(reason); }, get aborted() { return controller.signal.aborted; }, get response() { return response; } }; } else { return response; } } export { ABORT_ERROR, FetchError, TIMEOUT_ERROR, fetchT }; //# sourceMappingURL=main.mjs.map