UNPKG

everything-dev

Version:

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

132 lines (130 loc) • 5.25 kB
let effect = require("effect"); //#region src/utils/http-client.ts var FetchNetworkError = class extends effect.Data.TaggedError("FetchNetworkError") {}; var FetchTimeoutError = class extends effect.Data.TaggedError("FetchTimeoutError") {}; var FetchHttpError = class extends effect.Data.TaggedError("FetchHttpError") {}; const DEFAULT_TIMEOUT = "10 seconds"; const DEFAULT_RETRIES = 3; const EXPONENTIAL_BASE = effect.Duration.seconds(1); const EXPONENTIAL_CAP = effect.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 makeHttpFetch = (base) => { const urlOf = (input) => typeof input === "string" ? input : input instanceof URL ? input.href : input.url; const attempt = (input, init) => effect.Effect.tryPromise({ try: (signal) => base(input, { ...init, signal: init?.signal ? AbortSignal.any([init.signal, signal]) : signal }), catch: (cause) => new FetchNetworkError({ url: urlOf(input), cause }) }); const get = (url, options) => { const timeoutMs = effect.Duration.toMillis(options?.timeout ?? DEFAULT_TIMEOUT); const retries = options?.retries ?? DEFAULT_RETRIES; const once = attempt(url, { method: options?.method ?? "GET", headers: options?.headers, body: options?.body, redirect: options?.redirect }).pipe(effect.Effect.timeoutOrElse({ duration: timeoutMs, orElse: () => effect.Effect.fail(new FetchTimeoutError({ url })) }), effect.Effect.filterOrFail((response) => response.ok, (response) => new FetchHttpError({ url, status: response.status, statusText: response.statusText }))); if (retries <= 0) return once; return once.pipe(effect.Effect.retry({ schedule: effect.Schedule.exponential(EXPONENTIAL_BASE).pipe(effect.Schedule.upTo({ duration: EXPONENTIAL_CAP, times: retries }), effect.Schedule.jittered), while: isRetryable })); }; return { attempt, get }; }; const defaultHttpFetch = makeHttpFetch((url, init) => globalThis.fetch(url, init)); /** * The HTTP seam for boot-path consumers (bundle resolution, federation * loading, orchestrator deadlines): one resilience policy — interruption- * backed deadlines, capped exponential jittered retry, typed errors — with * the base fetch injectable so the bundle fetch interceptor can point it at * the pre-patch `fetch` and not re-enter itself. */ var HttpFetch = class HttpFetch extends effect.Context.Service()("everything-dev/http-client/HttpFetch") { static layerWith(options) { return effect.Layer.succeed(HttpFetch, makeHttpFetch(options.fetchImpl)); } static layer = effect.Layer.succeed(HttpFetch, defaultHttpFetch); }; const getCache = /* @__PURE__ */ new Map(); const GET_CACHE_TTL_MS = 3e4; const GET_CACHE_MAX_ENTRIES = 128; function setCached(url, data) { const now = Date.now(); for (const [key, entry] of getCache) if (entry.expiresAt <= now) getCache.delete(key); if (getCache.size >= GET_CACHE_MAX_ENTRIES) for (const key of getCache.keys()) { getCache.delete(key); if (getCache.size < GET_CACHE_MAX_ENTRIES) break; } getCache.set(url, { data, expiresAt: now + GET_CACHE_TTL_MS }); } function isCacheable(_url, options) { if (options?.method && options.method !== "GET") return false; if (options?.body) return false; return true; } /** One raw fetch attempt with a deadline: returns the Response regardless of * status; rejects on network failure or deadline expiry. */ const fetchRaw = (url, init, timeout = DEFAULT_TIMEOUT) => effect.Effect.runPromise(defaultHttpFetch.attempt(url, init).pipe(effect.Effect.timeoutOrElse({ duration: timeout, orElse: () => effect.Effect.fail(new FetchTimeoutError({ url })) }))); const fetchResponse = (url, options) => effect.Effect.runPromise(defaultHttpFetch.attempt(url, { method: options?.method, headers: options?.headers, body: options?.body, redirect: options?.redirect }).pipe(effect.Effect.timeoutOrElse({ duration: options?.timeout ?? DEFAULT_TIMEOUT, orElse: () => effect.Effect.fail(new FetchTimeoutError({ url })) }))); const fetchJsonOrNull = async (url, options) => { if (isCacheable(url, options)) { const cached = getCache.get(url); if (cached && cached.expiresAt > Date.now()) return cached.data; } try { const data = await (await effect.Effect.runPromise(defaultHttpFetch.get(url, options))).json(); if (isCacheable(url, options)) setCached(url, data); 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 exports.FetchHttpError = FetchHttpError; exports.FetchNetworkError = FetchNetworkError; exports.FetchTimeoutError = FetchTimeoutError; exports.HttpFetch = HttpFetch; exports.fetchJsonOrNull = fetchJsonOrNull; exports.fetchRaw = fetchRaw; exports.fetchResponse = fetchResponse; //# sourceMappingURL=http-client.cjs.map