JavaScript Promises Tutorial - Section 5: Advanced Patterns
Memoization prevents:
function memoize(fn) {
const cache = new Map();
return function(...args) {
const key = JSON.stringify(args);
if (cache.has(key)) {
console.log('Cache hit');
return cache.get(key);
}
console.log('Cache miss');
const promise = fn(...args);
cache.set(key, promise);
return promise;
};
}
// Usage
const fetchUser = memoize(async (id) => {
const response = await fetch(`/api/users/${id}`);
return await response.json();
});
// First call: fetches from API
await fetchUser(1);
// Second call: returns cached Promise
await fetchUser(1);
function memoizeWithTTL(fn, ttl = 60000) {
const cache = new Map();
return function(...args) {
const key = JSON.stringify(args);
const cached = cache.get(key);
if (cached && Date.now() - cached.timestamp < ttl) {
console.log('Cache hit');
return cached.promise;
}
console.log('Cache miss or expired');
const promise = fn(...args);
cache.set(key, {
promise,
timestamp: Date.now()
});
return promise;
};
}
// Usage: Cache for 5 minutes
const fetchUser = memoizeWithTTL(
async (id) => {
const response = await fetch(`/api/users/${id}`);
return await response.json();
},
300000
);
class LRUCache {
constructor(maxSize = 100) {
this.maxSize = maxSize;
this.cache = new Map();
}
get(key) {
if (!this.cache.has(key)) {
return undefined;
}
// Move to end (most recently used)
const value = this.cache.get(key);
this.cache.delete(key);
this.cache.set(key, value);
return value;
}
set(key, value) {
// Remove if exists
if (this.cache.has(key)) {
this.cache.delete(key);
}
// Remove oldest if at capacity
if (this.cache.size >= this.maxSize) {
const firstKey = this.cache.keys().next().value;
this.cache.delete(firstKey);
}
this.cache.set(key, value);
}
clear() {
this.cache.clear();
}
}
function memoizeWithLRU(fn, maxSize = 100) {
const cache = new LRUCache(maxSize);
return function(...args) {
const key = JSON.stringify(args);
const cached = cache.get(key);
if (cached) {
return cached;
}
const promise = fn(...args);
cache.set(key, promise);
return promise;
};
}
// Usage
const fetchUser = memoizeWithLRU(
async (id) => {
const response = await fetch(`/api/users/${id}`);
return await response.json();
},
50 // Max 50 cached users
);
class RequestDeduplicator {
constructor() {
this.pending = new Map();
}
async execute(key, fn) {
// Return existing promise if in flight
if (this.pending.has(key)) {
console.log('Reusing in-flight request');
return await this.pending.get(key);
}
// Create new promise
const promise = fn().finally(() => {
this.pending.delete(key);
});
this.pending.set(key, promise);
return await promise;
}
}
// Usage
const deduplicator = new RequestDeduplicator();
async function fetchUser(id) {
return await deduplicator.execute(
`user-${id}`,
async () => {
const response = await fetch(`/api/users/${id}`);
return await response.json();
}
);
}
// Multiple simultaneous calls share same request
Promise.all([
fetchUser(1),
fetchUser(1),
fetchUser(1)
]); // Only 1 actual API call
class SmartCache {
constructor(options = {}) {
this.cache = new Map();
this.ttl = options.ttl || 60000;
this.maxSize = options.maxSize || 100;
}
get(key) {
const entry = this.cache.get(key);
if (!entry) {
return null;
}
// Check if expired
if (Date.now() - entry.timestamp > this.ttl) {
this.cache.delete(key);
return null;
}
// Update access time
entry.lastAccess = Date.now();
return entry.value;
}
set(key, value) {
// Evict if at capacity
if (this.cache.size >= this.maxSize) {
this.evictLRU();
}
this.cache.set(key, {
value,
timestamp: Date.now(),
lastAccess: Date.now()
});
}
evictLRU() {
let oldestKey = null;
let oldestTime = Infinity;
for (const [key, entry] of this.cache.entries()) {
if (entry.lastAccess < oldestTime) {
oldestTime = entry.lastAccess;
oldestKey = key;
}
}
if (oldestKey) {
this.cache.delete(oldestKey);
}
}
invalidate(pattern) {
if (typeof pattern === 'string') {
this.cache.delete(pattern);
} else if (pattern instanceof RegExp) {
for (const key of this.cache.keys()) {
if (pattern.test(key)) {
this.cache.delete(key);
}
}
}
}
clear() {
this.cache.clear();
}
}
// Usage
const cache = new SmartCache({ ttl: 300000, maxSize: 50 });
async function fetchUser(id) {
const key = `user-${id}`;
const cached = cache.get(key);
if (cached) {
return cached;
}
const response = await fetch(`/api/users/${id}`);
const data = await response.json();
cache.set(key, data);
return data;
}
// Invalidate specific user
cache.invalidate('user-1');
// Invalidate all users
cache.invalidate(/^user-/);
class CachedAPIClient {
constructor(baseURL, cacheOptions = {}) {
this.baseURL = baseURL;
this.cache = new SmartCache(cacheOptions);
this.deduplicator = new RequestDeduplicator();
}
async request(endpoint, options = {}) {
const method = options.method || 'GET';
const cacheKey = `${method}-${endpoint}`;
// Only cache GET requests
if (method !== 'GET') {
return await this.fetchData(endpoint, options);
}
// Check cache
const cached = this.cache.get(cacheKey);
if (cached) {
console.log('Cache hit:', endpoint);
return cached;
}
// Deduplicate in-flight requests
return await this.deduplicator.execute(
cacheKey,
async () => {
const data = await this.fetchData(endpoint, options);
this.cache.set(cacheKey, data);
return data;
}
);
}
async fetchData(endpoint, options) {
const url = `${this.baseURL}${endpoint}`;
const response = await fetch(url, options);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return await response.json();
}
invalidate(pattern) {
this.cache.invalidate(pattern);
}
clearCache() {
this.cache.clear();
}
}
// Usage
const api = new CachedAPIClient('https://api.example.com', {
ttl: 300000, // 5 minutes
maxSize: 100
});
// First call: fetches from API
const user1 = await api.request('/users/1');
// Second call: returns from cache
const user2 = await api.request('/users/1');
// Invalidate after update
await api.request('/users/1', { method: 'PUT', body: data });
api.invalidate('GET-/users/1');
class StaleWhileRevalidate {
constructor(ttl = 60000, staleTime = 300000) {
this.cache = new Map();
this.ttl = ttl;
this.staleTime = staleTime;
}
async fetch(key, fetchFn) {
const cached = this.cache.get(key);
const now = Date.now();
if (cached) {
const age = now - cached.timestamp;
// Fresh: return immediately
if (age < this.ttl) {
return cached.data;
}
// Stale but acceptable: return and revalidate in background
if (age < this.staleTime) {
this.revalidate(key, fetchFn);
return cached.data;
}
}
// No cache or too stale: fetch fresh
const data = await fetchFn();
this.cache.set(key, { data, timestamp: now });
return data;
}
async revalidate(key, fetchFn) {
try {
const data = await fetchFn();
this.cache.set(key, { data, timestamp: Date.now() });
} catch (error) {
console.error('Revalidation failed:', error);
}
}
}
// Usage
const swr = new StaleWhileRevalidate(60000, 300000);
async function getUser(id) {
return await swr.fetch(
`user-${id}`,
async () => {
const response = await fetch(`/api/users/${id}`);
return await response.json();
}
);
}
// Returns cached data immediately, revalidates in background
const user = await getUser(1);
class CacheWarmer {
constructor(cache) {
this.cache = cache;
}
async warmUp(entries) {
console.log(`Warming cache with ${entries.length} entries...`);
await Promise.all(
entries.map(async ({ key, fetchFn }) => {
try {
const data = await fetchFn();
this.cache.set(key, data);
} catch (error) {
console.error(`Failed to warm ${key}:`, error);
}
})
);
console.log('Cache warmed');
}
}
// Usage
const cache = new SmartCache();
const warmer = new CacheWarmer(cache);
// Warm cache on app start
await warmer.warmUp([
{ key: 'user-1', fetchFn: () => fetchUser(1) },
{ key: 'user-2', fetchFn: () => fetchUser(2) },
{ key: 'config', fetchFn: () => fetchConfig() }
]);
Now that you've mastered advanced patterns, let's explore real-world applications!