UNPKG

everything-dev

Version:

A consolidated product package for building Module Federation apps with oRPC APIs.

100 lines (98 loc) 3.78 kB
import { Data, Duration, Effect, Schedule } from "effect"; //#region src/http-client.ts var FetchNetworkError = class extends Data.TaggedError("FetchNetworkError") {}; var FetchTimeoutError = class extends Data.TaggedError("FetchTimeoutError") {}; var FetchHttpError = class extends Data.TaggedError("FetchHttpError") {}; const DEFAULT_TIMEOUT = "10 seconds"; const DEFAULT_RETRIES = 3; const EXPONENTIAL_BASE = Duration.seconds(1); const EXPONENTIAL_CAP = Duration.seconds(15); const isRetryable = (error) => { if (error instanceof FetchNetworkError) return true; if (error instanceof FetchTimeoutError) return true; if (error instanceof FetchHttpError) return error.status >= 500; return false; }; const fetchRawEff = (url, options) => Effect.tryPromise({ try: async () => { const timeoutMs = Duration.toMillis(Duration.decode(options?.timeout ?? DEFAULT_TIMEOUT)); const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), timeoutMs); try { return await fetch(url, { method: options?.method ?? "GET", headers: options?.headers, body: options?.body, redirect: options?.redirect, signal: controller.signal }); } catch (error) { if (error instanceof Error && error.name === "AbortError") throw new FetchTimeoutError({ url }); throw new FetchNetworkError({ url, cause: error }); } finally { clearTimeout(timer); } }, catch: (error) => { if (error instanceof FetchNetworkError || error instanceof FetchTimeoutError) return error; return new FetchNetworkError({ url, cause: error }); } }); const fetchEff = (url, options) => fetchRawEff(url, options).pipe(Effect.flatMap((response) => { if (response.ok) return Effect.succeed(response); return Effect.fail(new FetchHttpError({ url, status: response.status, statusText: response.statusText })); })); const retrySchedule = Schedule.exponential(EXPONENTIAL_BASE).pipe(Schedule.upTo(EXPONENTIAL_CAP), Schedule.intersect(Schedule.recurs(DEFAULT_RETRIES))); const fetchWithRetryEff = (url, options) => { const retries = options?.retries ?? DEFAULT_RETRIES; if (retries <= 0) return fetchEff(url, options); const schedule = options?.retries !== void 0 ? Schedule.exponential(EXPONENTIAL_BASE).pipe(Schedule.upTo(EXPONENTIAL_CAP), Schedule.intersect(Schedule.recurs(retries))) : retrySchedule; return fetchEff(url, options).pipe(Effect.retry({ schedule, while: isRetryable })); }; const getCache = /* @__PURE__ */ new Map(); const GET_CACHE_TTL_MS = 3e4; function isCacheable(_url, options) { if (options?.method && options.method !== "GET") return false; if (options?.body) return false; return true; } const fetchResponse = (url, options) => Effect.runPromise(fetchRawEff(url, options)); const fetchJsonOrNull = async (url, options) => { if (isCacheable(url, options)) { const cached = getCache.get(url); if (cached && cached.expiresAt > Date.now()) return cached.data; } const retries = options?.retries ?? DEFAULT_RETRIES; const eff = retries > 0 ? fetchWithRetryEff(url, { ...options, retries }) : fetchEff(url, options); try { const data = await (await Effect.runPromise(eff)).json(); if (isCacheable(url, options)) getCache.set(url, { data, expiresAt: Date.now() + GET_CACHE_TTL_MS }); return data; } catch (error) { const msg = error instanceof FetchNetworkError ? `[http] Network error: ${error.url}${String(error.cause)}` : error instanceof FetchTimeoutError ? `[http] Timeout: ${error.url}` : error instanceof FetchHttpError ? `[http] HTTP ${error.status}: ${error.url}` : `[http] Unknown error while fetching ${url}`; console.error(msg); return null; } }; //#endregion export { fetchJsonOrNull, fetchResponse }; //# sourceMappingURL=http-client.mjs.map