@happy-ts/fetch-t
Version:
Abortable fetch wrapper with the ability to specify the return type.
156 lines (150 loc) • 4.24 kB
JavaScript
;
var happyRusty = require('happy-rusty');
var invariant = require('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 happyRusty.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(happyRusty.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(happyRusty.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 happyRusty.Ok(await res.arrayBuffer());
}
case "blob": {
return happyRusty.Ok(await res.blob());
}
case "json": {
try {
return happyRusty.Ok(await res.json());
} catch {
return happyRusty.Err(new Error("Response is invalid json while responseType is json"));
}
}
case "text": {
return happyRusty.Ok(await res.text());
}
default: {
return happyRusty.Ok(res);
}
}
}).catch((err) => {
cancelTimer?.();
return happyRusty.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;
}
}
exports.ABORT_ERROR = ABORT_ERROR;
exports.FetchError = FetchError;
exports.TIMEOUT_ERROR = TIMEOUT_ERROR;
exports.fetchT = fetchT;
//# sourceMappingURL=main.cjs.map