@alwatr/fetch
Version:
Enhanced fetch API with cache strategy, retry pattern, timeout, helper methods and enhanced types.
225 lines (221 loc) • 8.32 kB
JavaScript
/* @alwatr/fetch v5.6.2 */
// src/main.ts
import { HttpStatusCodes as HttpStatusCodes2 } from "@alwatr/http-primer";
// src/core.ts
import { delay } from "@alwatr/delay";
import { getGlobalThis } from "@alwatr/global-this";
import { HttpStatusCodes, MimeTypes } from "@alwatr/http-primer";
import { createLogger } from "@alwatr/logger";
import { packageTracer } from "@alwatr/package-tracer";
import { parseDuration } from "@alwatr/parse-duration";
__dev_mode__: packageTracer.add("@alwatr/fetch", "5.6.2");
var logger_ = /* @__PURE__ */ createLogger("@alwatr/fetch");
var globalThis_ = /* @__PURE__ */ getGlobalThis();
var cacheStorage_;
var cacheSupported = /* @__PURE__ */ Object.hasOwn(globalThis_, "caches");
var duplicateRequestStorage_ = {};
function processOptions_(options) {
options.method ?? (options.method = "GET");
options.window ?? (options.window = null);
options.timeout ?? (options.timeout = 8e3);
options.retry ?? (options.retry = 3);
options.retryDelay ?? (options.retryDelay = 1e3);
options.cacheStrategy ?? (options.cacheStrategy = "network_only");
options.removeDuplicate ?? (options.removeDuplicate = "never");
options.headers ?? (options.headers = {});
if (options.cacheStrategy !== "network_only" && cacheSupported !== true) {
logger_.incident?.("fetch", "fetch_cache_strategy_unsupported", {
cacheSupported
});
options.cacheStrategy = "network_only";
}
if (options.removeDuplicate === "auto") {
options.removeDuplicate = cacheSupported ? "until_load" : "always";
}
if (options.url.lastIndexOf("?") === -1 && options.queryParams != null) {
const queryParams = options.queryParams;
const queryArray = Object.keys(queryParams).map((key) => `${key}=${String(queryParams[key])}`);
if (queryArray.length > 0) {
options.url += "?" + queryArray.join("&");
}
}
if (options.bodyJson !== void 0) {
options.body = JSON.stringify(options.bodyJson);
options.headers["content-type"] = MimeTypes.JSON;
}
if (options.bearerToken !== void 0) {
options.headers.authorization = `Bearer ${options.bearerToken}`;
} else if (options.alwatrAuth !== void 0) {
options.headers.authorization = `Alwatr ${options.alwatrAuth.userId}:${options.alwatrAuth.userToken}`;
}
return options;
}
async function handleCacheStrategy_(options) {
if (options.cacheStrategy === "network_only") {
return handleRemoveDuplicate_(options);
}
logger_.logMethod?.("_handleCacheStrategy");
if (cacheStorage_ == null && options.cacheStorageName == null) {
cacheStorage_ = await caches.open("fetch_cache");
}
const cacheStorage = options.cacheStorageName != null ? await caches.open(options.cacheStorageName) : cacheStorage_;
const request = new Request(options.url, options);
switch (options.cacheStrategy) {
case "cache_first": {
const cachedResponse = await cacheStorage.match(request);
if (cachedResponse != null) {
return cachedResponse;
}
const response = await handleRemoveDuplicate_(options);
if (response.ok) {
cacheStorage.put(request, response.clone());
}
return response;
}
case "cache_only": {
const cachedResponse = await cacheStorage.match(request);
if (cachedResponse == null) {
logger_.accident("_handleCacheStrategy", "fetch_cache_not_found", { url: request.url });
throw new Error("fetch_cache_not_found");
}
return cachedResponse;
}
case "network_first": {
try {
const networkResponse = await handleRemoveDuplicate_(options);
if (networkResponse.ok) {
cacheStorage.put(request, networkResponse.clone());
}
return networkResponse;
} catch (err) {
const cachedResponse = await cacheStorage.match(request);
if (cachedResponse != null) {
return cachedResponse;
}
throw err;
}
}
case "update_cache": {
const networkResponse = await handleRemoveDuplicate_(options);
if (networkResponse.ok) {
cacheStorage.put(request, networkResponse.clone());
}
return networkResponse;
}
case "stale_while_revalidate": {
const cachedResponse = await cacheStorage.match(request);
const fetchedResponsePromise = handleRemoveDuplicate_(options).then((networkResponse) => {
if (networkResponse.ok) {
cacheStorage.put(request, networkResponse.clone());
if (typeof options.revalidateCallback === "function") {
setTimeout(options.revalidateCallback, 0, networkResponse.clone());
}
}
return networkResponse;
});
return cachedResponse ?? fetchedResponsePromise;
}
default: {
return handleRemoveDuplicate_(options);
}
}
}
async function handleRemoveDuplicate_(options) {
if (options.removeDuplicate === "never") return handleRetryPattern_(options);
logger_.logMethod?.("handleRemoveDuplicate_");
const cacheKey = options.method + " " + options.url;
duplicateRequestStorage_[cacheKey] ?? (duplicateRequestStorage_[cacheKey] = handleRetryPattern_(options));
try {
const response = await duplicateRequestStorage_[cacheKey];
if (duplicateRequestStorage_[cacheKey] != null) {
if (response.ok !== true || options.removeDuplicate === "until_load") {
delete duplicateRequestStorage_[cacheKey];
}
}
return response.clone();
} catch (err) {
delete duplicateRequestStorage_[cacheKey];
throw err;
}
}
async function handleRetryPattern_(options) {
if (!(options.retry > 1)) return handleTimeout_(options);
logger_.logMethod?.("_handleRetryPattern");
options.retry--;
const externalAbortSignal = options.signal;
try {
const response = await handleTimeout_(options);
if (response.status < HttpStatusCodes.Error_Server_500_Internal_Server_Error) {
return response;
}
throw new Error("fetch_server_error");
} catch (err) {
logger_.accident("fetch", "fetch_failed_retry", err);
if (globalThis_.navigator?.onLine === false) {
logger_.accident("handleRetryPattern_", "offline", "Skip retry because offline");
throw err;
}
await delay.by(options.retryDelay);
options.signal = externalAbortSignal;
return handleRetryPattern_(options);
}
}
function handleTimeout_(options) {
if (options.timeout === 0) {
return globalThis_.fetch(options.url, options);
}
logger_.logMethod?.("handleTimeout_");
return new Promise((resolved, reject) => {
const abortController = typeof AbortController === "function" ? new AbortController() : null;
const externalAbortSignal = options.signal;
options.signal = abortController?.signal;
if (abortController !== null && externalAbortSignal != null) {
externalAbortSignal.addEventListener("abort", () => abortController.abort(), { once: true });
}
const timeoutId = setTimeout(() => {
reject(new Error("fetch_timeout"));
abortController?.abort("fetch_timeout");
}, parseDuration(options.timeout));
globalThis_.fetch(options.url, options).then((response) => resolved(response)).catch((reason) => reject(reason)).finally(() => {
delete options.signal;
clearTimeout(timeoutId);
});
});
}
// src/main.ts
async function fetchJson(options) {
let response;
let responseText;
let responseJson;
try {
response = await fetch(options);
responseText = await response.text();
responseJson = JSON.parse(responseText);
if (responseJson.ok === false) {
throw new Error(`fetch_response_nok`);
}
return responseJson;
} catch (error) {
const responseError = {
...responseJson,
ok: false,
statusCode: response?.status ?? HttpStatusCodes2.Error_Server_500_Internal_Server_Error,
errorCode: responseJson?.errorCode ?? error.message,
errorMessage: responseJson?.errorMessage ?? error.message
// responseText,
};
logger_.accident("fetchJson", "fetch_json_failed", { responseError, error, responseText });
return responseError;
}
}
function fetch(options) {
options = processOptions_(options);
logger_.logMethodArgs?.("fetch", { options });
return handleCacheStrategy_(options);
}
export {
cacheSupported,
fetch,
fetchJson
};
//# sourceMappingURL=main.mjs.map