UNPKG

@alwatr/fetch

Version:

Enhanced fetch API with cache strategy, retry pattern, timeout, helper methods and enhanced types.

225 lines (221 loc) 8.32 kB
/* @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