Learning Objectives

  • Implement Promise memoization
  • Build intelligent caching strategies
  • Handle cache invalidation
  • Optimize async performance

Why Memoize Promises?

Memoization prevents:

Basic Promise Memoization

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);

Memoization with TTL (Time To Live)

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
);

LRU (Least Recently Used) Cache

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
);

Preventing Duplicate In-Flight Requests

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

Advanced Cache with Invalidation

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-/);

Cached API Client

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');

Stale-While-Revalidate Pattern

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);

Cache Warming

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() }
]);

Best Practices

  1. Cache GET requests only - Don't cache mutations
  2. Set appropriate TTL - Balance freshness vs performance
  3. Implement cache invalidation - Clear stale data
  4. Use LRU eviction - Prevent unlimited growth
  5. Deduplicate in-flight requests - Prevent duplicate calls
  6. Consider stale-while-revalidate - Best UX
  7. Warm critical caches - Preload important data
  8. Monitor cache hit rates - Optimize strategy

Key Takeaways

  • ✅ Memoization prevents duplicate requests
  • ✅ Use TTL for automatic expiration
  • LRU cache prevents unlimited growth
  • Deduplicate in-flight requests
  • Stale-while-revalidate for best UX
  • ✅ Implement cache invalidation strategies

Next Steps

Now that you've mastered advanced patterns, let's explore real-world applications!