import { SsrDataFormatError } from "../others/errors"; import ssrConfig from "../ssr-config"; import { MINUTE } from "../utils/date"; import createNetworkCache from "./cache"; import { SsrHttpClientError, SsrHttpNotFoundError, SsrHttpRateLimitError, SsrHttpResponseError, SsrHttpServerError, SsrHttpUnauthenticatedError, SsrHttpUnauthorizedError, SsrHttpUnprocessableEntityError, SsrNetworkError, } from "./errors"; import { parseRateLimitHeaders } from "./utils"; const networkCache = createNetworkCache(); const checkResponse = (response) => { if (response.ok) { // response.status >= 200 && response.status < 300 return response; } switch (true) { case response.status === 401: throw new SsrHttpUnauthenticatedError(response); case response.status === 403: throw new SsrHttpUnauthorizedError(response); case response.status === 404: throw new SsrHttpNotFoundError(response); case response.status === 422: throw new SsrHttpUnprocessableEntityError(response); case response.status === 429: throw new SsrHttpRateLimitError(response); case response.status >= 400 && response.status < 500: throw new SsrHttpClientError(response); case response.status >= 500: throw new SsrHttpServerError(response); default: throw new SsrHttpResponseError(response); } }; const getOptionsWithCacheKey = (url, options, cacheType = null) => { if (options && options.cacheKey) { if (!options.cacheTtl) options.cacheTtl = MINUTE; return options; } if (options && options.method && options.method.toLowerCase() !== "get") { delete options.cacheKey; delete options.cacheTtl; return options; } const newOptions = options ? { ...options } : {}; if (!newOptions || !newOptions.hasOwnProperty("cacheTtl")) { newOptions.cacheTtl = MINUTE; } return { ...newOptions, cacheKey: `${cacheType ? cacheType + ":" : ""}${url}`, }; }; const setCacheIfNeeded = (response, cacheKey, cacheTtl) => { if (cacheKey && cacheTtl) networkCache.set(cacheKey, response, cacheTtl); return { ...response, cached: false }; }; export async function fetchUrl(url, options = {}, cors = true) { try { const response = await fetch(url, { ...options, headers: { "x-requested-with": ssrConfig.name, }, ...(cors ? { mode: "cors" } : null), }); return checkResponse(response); } catch (err) { if (err instanceof TypeError) throw new SsrNetworkError("Network error"); throw err; } } export async function fetchJson( url, { cacheTtl = null, maxAge = null, ...restOptions } = {} ) { const options = getOptionsWithCacheKey( url, { cacheTtl, maxAge, ...restOptions }, "json" ); const { cacheKey: fetchCacheKey, cacheTtl: fetchCacheTtl, maxAge: fetchMaxAge, ...fetchOptions } = getOptionsWithCacheKey(url, options, "json"); if (fetchCacheKey && fetchCacheTtl) { const cachedResponse = networkCache.get(fetchCacheKey, fetchMaxAge); if (cachedResponse !== undefined) return { ...cachedResponse, cached: true }; } return fetchUrl(url, fetchOptions) .then(async (response) => { const body = await response.json(); return setCacheIfNeeded( { headers: response.headers, rateLimit: parseRateLimitHeaders(response), body, }, fetchCacheKey, fetchCacheTtl ); }) .catch((err) => { throw err instanceof SyntaxError ? new SsrDataFormatError("JSON parse error", err) : err; }); } export async function fetchHtml( url, { cacheTtl = null, maxAge = null, ...restOptions } = {} ) { const options = getOptionsWithCacheKey( url, { cacheTtl, maxAge, ...restOptions }, "json" ); const { cacheKey: fetchCacheKey, cacheTtl: fetchCacheTtl, maxAge: fetchMaxAge, ...fetchOptions } = getOptionsWithCacheKey(url, options, "html"); if (fetchCacheKey && fetchCacheTtl) { const cachedResponse = networkCache.get(fetchCacheKey, fetchMaxAge); if (cachedResponse !== undefined) return { ...cachedResponse, cached: true }; } return fetchUrl(url, fetchOptions).then(async (response) => { const body = await response.text(); return setCacheIfNeeded( { headers: response.headers, rateLimit: parseRateLimitHeaders(response), body: new DOMParser().parseFromString(body, "text/html"), }, fetchCacheKey, fetchCacheTtl ); }); }