everything-dev
Version:
A consolidated product package for building Module Federation apps with oRPC APIs.
100 lines (98 loc) • 3.78 kB
JavaScript
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