Repository Public
1// CoinGecko reads (public coin ids only — never an address or a balance), with a cache, stale-on-error and — since v10 — a2// short retry after a failure.3//4// Before v10 the answer of a call was cached for its TTL whatever it was: on a process that had no earlier value, ONE refused5// request (the free tier answers 429 to a burst) was kept as "no price" for four minutes. Home then showed « Prix désactivés »6// and no daily change although prices were enabled, and concurrent requests each made their own call, which is what the free7// tier refuses. Now: concurrent callers share one request; a refused request is retried once after a pause; a failure is8// remembered for RETRY_MS only (the stale value, when there is one, is served meanwhile).9export type GeckoFetch = (url: string, init: { headers: Record<string, string> }) => Promise<{ ok: boolean; status: number; json(): Promise<unknown> }>;10export interface GeckoOpts { now?: () => number; retryMs?: number; pauseMs?: number; base?: string }1112export function createGecko(fetchJson: GeckoFetch, o: GeckoOpts = {}): <T>(key: string, ttlMs: number, path: string) => Promise<T | null> {13 const now = o.now ?? Date.now, RETRY_MS = o.retryMs ?? 15_000, PAUSE_MS = o.pauseMs ?? 1200, BASE = o.base ?? 'https://api.coingecko.com/api/v3';14 const fresh = new Map<string, { at: number; v: unknown }>(), stale = new Map<string, unknown>(), failedAt = new Map<string, number>(), inflight = new Map<string, Promise<unknown>>();15 return async function gecko<T>(key: string, ttlMs: number, path: string): Promise<T | null> {16 const c = fresh.get(key);17 if (c && now() - c.at < ttlMs) return c.v as T;18 const f = failedAt.get(key);19 if (f !== undefined && now() - f < RETRY_MS) return (stale.get(key) as T | undefined) ?? null;20 const running = inflight.get(key);21 if (running) return running as Promise<T | null>;22 const once = async (): Promise<T> => {23 const r = await fetchJson(BASE + path, { headers: { accept: 'application/json' } });24 if (!r.ok) throw new Error(String(r.status));25 return (await r.json()) as T;26 };27 const p = (async (): Promise<T | null> => {28 try {29 let j: T;30 try { j = await once(); } catch { await new Promise((r) => setTimeout(r, PAUSE_MS)); j = await once(); }31 stale.set(key, j); fresh.set(key, { at: now(), v: j }); failedAt.delete(key);32 return j;33 } catch { failedAt.set(key, now()); return (stale.get(key) as T | undefined) ?? null; }34 finally { inflight.delete(key); }35 })();36 inflight.set(key, p);37 return p;38 };39}40