The Kit/Utils

SWR Cache

Generic in-memory stale-while-revalidate cache (TTL + inflight de-duplication) that wraps any async fetcher: serve fresh, serve stale + refresh in the background, await cold, serve last-good on error. Decoupled from WC26's ESPN service.

swr-cache.ts
// Generic in-memory stale-while-revalidate cache: TTL + inflight de-duplication.
// Extracted + decoupled from WC26's ESPN data service (the real caching layer when the
// upstream sends `Cache-Control: max-age=1` and no ETag, so there's no 304 path).
// Dependency-free; wraps any async fetcher.
//   fresh  -> serve cached
//   stale  -> serve stale immediately + refresh in the background
//   cold   -> await the first fetch
//   error  -> serve the last good value if we have one

export interface SwrEntry<T> {
  data: T;
  at: number;
  inflight?: Promise<T>;
}

export interface SwrCache<T> {
  /** SWR read: fresh -> cached; stale -> stale + bg refresh; cold -> await. */
  get(key: string): Promise<T>;
  /** Always fetch (warm the cache + dedupe concurrent calls) — e.g. a live poller. */
  fresh(key: string): Promise<T>;
  /** Drop a key (or the whole cache). */
  invalidate(key?: string): void;
}

export function createSwrCache<T>(fetcher: (key: string) => Promise<T>, ttlMs: number): SwrCache<T> {
  const cache = new Map<string, SwrEntry<T>>();
  const load = (key: string) =>
    fetcher(key).then((data) => {
      cache.set(key, { data, at: Date.now() });
      return data;
    });

  return {
    async get(key) {
      const now = Date.now();
      const e = cache.get(key);
      if (e && now - e.at < ttlMs) return e.data; // fresh
      if (e && e.inflight) return e.data; // already revalidating -> serve stale
      const revalidate = load(key)
        .catch((err) => {
          if (e) return e.data; // serve stale on error
          throw err;
        })
        .finally(() => {
          const cur = cache.get(key);
          if (cur) cur.inflight = undefined;
        });
      if (e) {
        e.inflight = revalidate;
        return e.data; // stale-while-revalidate
      }
      return revalidate; // cold
    },

    async fresh(key) {
      const e = cache.get(key);
      if (e?.inflight) return e.inflight; // dedupe concurrent forced fetches
      const p = load(key).finally(() => {
        const cur = cache.get(key);
        if (cur) cur.inflight = undefined;
      });
      cache.set(key, { ...(e ?? { data: undefined as unknown as T, at: 0 }), inflight: p });
      return p;
    },

    invalidate(key) {
      if (key) cache.delete(key);
      else cache.clear();
    },
  };
}
← The whole kit