{"version":3,"file":"http-client.cjs","names":["Data","Duration","Effect","Schedule"],"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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"}