An ETag revalidation cache stores each response alongside its ETag — a short version stamp the server assigns to the current content — and on the next read revalidates instead of blindly re-downloading: it asks the server whether the copy it already holds is still current, quoting that stamp. When nothing has changed the server replies 304 Not Modified with no body and you reuse the cached copy; when it has changed the server sends a fresh 200 with new data and a new ETag. It is how browsers and HTTP caches avoid re-fetching a resource that has not moved.
Implement etagRevalidationCache(fetcher). It returns get(url), which resolves the resource's data. Under the hood get calls fetcher(url, { etag }), passing the stored ETag (or undefined on the very first read), and turns the 200 or 304 answer into the right cached value.
type Response<T> =
| { status: 304 } // Not Modified — no body
| { status: 200; data: T; etag: string }; // fresh body + its version stamp
type Fetcher<T> = (
url: string,
opts: { etag: string | undefined }, // the stored ETag, if any
) => Promise<Response<T>>;
function etagRevalidationCache<T>(
fetcher: Fetcher<T>,
): (url: string) => Promise<T>; // get(url) -> resolves the data
const get = etagRevalidationCache(fetcher);
await get('/doc'); // cold: fetcher('/doc', { etag: undefined })
// -> { status: 200, data, etag: 'v1' }
// stores { data, etag: 'v1' }, resolves data
await get('/doc'); // revalidate: fetcher('/doc', { etag: 'v1' })
// -> { status: 304 }
// resolves the SAME cached data, nothing re-downloaded
// the document has changed on the server since 'v1'
await get('/doc'); // fetcher('/doc', { etag: 'v1' })
// -> { status: 200, data: next, etag: 'v2' }
// replaces the cache, resolves the new data;
// the next read now sends 'v2'
{ etag }; only the very first read for a url sends { etag: undefined }.{ status: 304 } response has no body; resolve the data already in the cache and leave the stored ETag untouched.{ status: 200 } response carries fresh data and a new etag; overwrite both in the cache and resolve the new data.get(url) calls for the same url must share one fetch; a second call that arrives while one is running gets the same promise.