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