everything-dev
Version:
A consolidated product package for building Module Federation apps with oRPC APIs.
1 lines • 8.2 kB
Source Map (JSON)
{"version":3,"file":"http-client.mjs","names":[],"sources":["../src/http-client.ts"],"sourcesContent":["import { Data, Duration, Effect, Schedule } from \"effect\";\n\n// --- Tagged errors ---\n\nexport class FetchNetworkError extends Data.TaggedError(\"FetchNetworkError\")<{\n url: string;\n cause: unknown;\n}> {}\n\nexport class FetchTimeoutError extends Data.TaggedError(\"FetchTimeoutError\")<{\n url: string;\n}> {}\n\nexport class FetchHttpError extends Data.TaggedError(\"FetchHttpError\")<{\n url: string;\n status: number;\n statusText: string;\n}> {}\n\nexport type FetchError = FetchNetworkError | FetchTimeoutError | FetchHttpError;\n\n// --- Options ---\n\nexport interface FetchOptions {\n method?: string;\n headers?: Record<string, string>;\n body?: BodyInit;\n redirect?: RequestRedirect;\n timeout?: Duration.DurationInput;\n}\n\nexport interface FetchWithRetryOptions extends FetchOptions {\n retries?: number;\n}\n\n// --- Defaults ---\n\nconst DEFAULT_TIMEOUT = \"10 seconds\";\nconst DEFAULT_RETRIES = 3;\n\nconst EXPONENTIAL_BASE = Duration.seconds(1);\nconst EXPONENTIAL_CAP = Duration.seconds(15);\n\n// --- Helpers ---\n\nconst isRetryable = (error: FetchError): boolean => {\n if (error instanceof FetchNetworkError) return true;\n if (error instanceof FetchTimeoutError) return true;\n if (error instanceof FetchHttpError) return error.status >= 500;\n return false;\n};\n\n// --- Low-level: just timeout + error classification, returns Response regardless of HTTP status ---\n\nconst fetchRawEff = (\n url: string,\n options?: FetchOptions,\n): Effect.Effect<Response, FetchNetworkError | FetchTimeoutError> =>\n Effect.tryPromise({\n try: async () => {\n const timeoutMs = Duration.toMillis(Duration.decode(options?.timeout ?? DEFAULT_TIMEOUT));\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), timeoutMs);\n try {\n return await fetch(url, {\n method: options?.method ?? \"GET\",\n headers: options?.headers,\n body: options?.body,\n redirect: options?.redirect,\n signal: controller.signal,\n });\n } catch (error) {\n if (error instanceof Error && error.name === \"AbortError\") {\n throw new FetchTimeoutError({ url });\n }\n throw new FetchNetworkError({ url, cause: error });\n } finally {\n clearTimeout(timer);\n }\n },\n catch: (error) => {\n if (error instanceof FetchNetworkError || error instanceof FetchTimeoutError) {\n return error;\n }\n return new FetchNetworkError({ url, cause: error });\n },\n });\n\n// --- Adds HTTP status checking: non-2xx becomes FetchHttpError ---\n\nconst fetchEff = (url: string, options?: FetchOptions): Effect.Effect<Response, FetchError> =>\n fetchRawEff(url, options).pipe(\n Effect.flatMap((response) => {\n if (response.ok) return Effect.succeed(response);\n return Effect.fail(\n new FetchHttpError({ url, status: response.status, statusText: response.statusText }),\n );\n }),\n );\n\n// --- With retry ---\n\nconst retrySchedule = Schedule.exponential(EXPONENTIAL_BASE).pipe(\n Schedule.upTo(EXPONENTIAL_CAP),\n Schedule.intersect(Schedule.recurs(DEFAULT_RETRIES)),\n);\n\nexport const fetchWithRetryEff = (\n url: string,\n options?: FetchWithRetryOptions,\n): Effect.Effect<Response, FetchError> => {\n const retries = options?.retries ?? DEFAULT_RETRIES;\n\n if (retries <= 0) return fetchEff(url, options);\n\n const schedule =\n options?.retries !== undefined\n ? Schedule.exponential(EXPONENTIAL_BASE).pipe(\n Schedule.upTo(EXPONENTIAL_CAP),\n Schedule.intersect(Schedule.recurs(retries)),\n )\n : retrySchedule;\n\n return fetchEff(url, options).pipe(\n Effect.retry({\n schedule,\n while: isRetryable,\n }),\n );\n};\n\n// --- In-memory GET cache (per-process, prevents duplicate round trips) ---\n\nconst getCache = new Map<string, { data: unknown; expiresAt: number }>();\nconst GET_CACHE_TTL_MS = 30_000;\n\nfunction isCacheable(_url: string, options?: FetchWithRetryOptions): boolean {\n if (options?.method && options.method !== \"GET\") return false;\n if (options?.body) return false;\n return true;\n}\n\n// --- Promise wrappers (bridge for non-Effect code) ---\n\nexport const fetchResponse = (url: string, options?: FetchOptions): Promise<Response> =>\n Effect.runPromise(fetchRawEff(url, options));\n\nexport const fetchJsonOrNull = async <T>(\n url: string,\n options?: FetchWithRetryOptions,\n): Promise<T | null> => {\n if (isCacheable(url, options)) {\n const cached = getCache.get(url);\n if (cached && cached.expiresAt > Date.now()) {\n return cached.data as T;\n }\n }\n\n const retries = options?.retries ?? DEFAULT_RETRIES;\n const eff =\n retries > 0 ? fetchWithRetryEff(url, { ...options, retries }) : fetchEff(url, options);\n\n try {\n const res = await Effect.runPromise(eff);\n const data = (await res.json()) as T;\n if (isCacheable(url, options)) {\n getCache.set(url, { data, expiresAt: Date.now() + GET_CACHE_TTL_MS });\n }\n return data;\n } catch (error) {\n const msg =\n error instanceof FetchNetworkError\n ? `[http] Network error: ${error.url} — ${String(error.cause)}`\n : error instanceof FetchTimeoutError\n ? `[http] Timeout: ${error.url}`\n : error instanceof FetchHttpError\n ? `[http] HTTP ${error.status}: ${error.url}`\n : `[http] Unknown error while fetching ${url}`;\n console.error(msg);\n return null;\n }\n};\n"],"mappings":"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"}