SPB Git

Repository Public

gecko.ts 2.5 KB 40 lines typescript

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
Files main
Menu

Theme