31. Mini project: a resilient fetch client (timeout, retry, cache, dedup)
class ResilientClient {
#cache = new Map();
#inFlight = new Map(); // dedupe: identical concurrent requests share one promise
async get(url, { timeoutMs = 5000, retries = 2, cacheMs = 30000 } = {}) {
const cached = this.#cache.get(url);
if (cached && Date.now() - cached.time < cacheMs) return cached.data;
if (this.#inFlight.has(url)) return this.#inFlight.get(url); // join the existing request
const promise = this.#fetchWithRetry(url, timeoutMs, retries)
.then((data) => {
this.#cache.set(url, { data, time: Date.now() });
this.#inFlight.delete(url);
return data;
})
.catch((err) => { this.#inFlight.delete(url); throw err; });
this.#inFlight.set(url, promise);
return promise;
}
async #fetchWithRetry(url, timeoutMs, retries) {
for (let attempt = 0; attempt <= retries; attempt++) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
try {
const res = await fetch(url, { signal: controller.signal });
clearTimeout(timer);
if (!res.ok && res.status >= 500 && attempt < retries) {
await new Promise(r => setTimeout(r, 2 ** attempt * 200)); // exponential backoff
continue;
}
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
} catch (err) {
clearTimeout(timer);
if (attempt === retries) throw err;
}
}
}
}
This single class demonstrates request deduplication (multiple UI components
asking for the same data at once share one network call), TTL caching,
per-request timeouts via AbortController, and retry-with-backoff limited to
retriable (5xx) failures — the exact pattern behind libraries like SWR and
React Query, minus the React-specific rendering integration.