Caching Strategies

Comprehensive guide to caching in SCORM API integrations.

Table of Contents

Overview

Caching reduces:

  • API calls
  • Response times
  • Server load
  • Costs

Client-Side Caching

Package Metadata Caching

Cache Package Information:

class PackageCache {
  private cache = new Map<string, { data: any; expiresAt: number }>();
  private ttl = 5 * 60 * 1000; // 5 minutes

  async get(packageId: string) {
    const cached = this.cache.get(packageId);
    
    if (cached && cached.expiresAt > Date.now()) {
      return cached.data;
    }
    
    const pkg = await fetchPackage(packageId);
    this.cache.set(packageId, {
      data: pkg,
      expiresAt: Date.now() + this.ttl
    });
    
    return pkg;
  }
  
  invalidate(packageId: string) {
    this.cache.delete(packageId);
  }
}

Session Data Caching

Cache Session State:

class SessionCache {
  private cache = new Map<string, SessionData>();
  private ttl = 30 * 1000; // 30 seconds (sessions change frequently)

  async get(sessionId: string) {
    const cached = this.cache.get(sessionId);
    
    if (cached && cached.expiresAt > Date.now()) {
      return cached.data;
    }
    
    const session = await fetchSession(sessionId);
    this.cache.set(sessionId, {
      data: session,
      expiresAt: Date.now() + this.ttl
    });
    
    return session;
  }
  
  update(sessionId: string, updates: any) {
    const cached = this.cache.get(sessionId);
    if (cached) {
      cached.data = { ...cached.data, ...updates };
      cached.expiresAt = Date.now() + this.ttl;
    }
  }
}

List Caching

Cache Package Lists:

class ListCache {
  private cache = new Map<string, { data: any[]; expiresAt: number }>();
  
  async getPackages(tenantId: string, filters: any) {
    const key = `packages:${tenantId}:${JSON.stringify(filters)}`;
    const cached = this.cache.get(key);
    
    if (cached && cached.expiresAt > Date.now()) {
      return cached.data;
    }
    
    const packages = await fetchPackages(tenantId, filters);
    this.cache.set(key, {
      data: packages,
      expiresAt: Date.now() + 2 * 60 * 1000 // 2 minutes
    });
    
    return packages;
  }
}

Server-Side Caching

Redis Caching

Setup Redis:

import Redis from 'ioredis';

const redis = new Redis(process.env.REDIS_URL);

async function getCachedPackage(packageId: string) {
  const cached = await redis.get(`package:${packageId}`);
  
  if (cached) {
    return JSON.parse(cached);
  }
  
  const pkg = await fetchPackageFromDB(packageId);
  await redis.setex(
    `package:${packageId}`,
    300, // 5 minutes TTL
    JSON.stringify(pkg)
  );
  
  return pkg;
}

In-Memory Caching

Node.js Cache:

import NodeCache from 'node-cache';

const cache = new NodeCache({ stdTTL: 300 }); // 5 minutes default

async function getPackage(packageId: string) {
  const cached = cache.get(`package:${packageId}`);
  if (cached) {
    return cached;
  }
  
  const pkg = await fetchPackage(packageId);
  cache.set(`package:${packageId}`, pkg);
  return pkg;
}

CDN Caching

Static Assets

Cache Package Files:

// Package files served via CDN
const packageUrl = `https://cdn.app.allureconnect.com/packages/${packageId}/index.html`;

// CDN caches with long TTL
// Cache-Control: public, max-age=31536000

API Responses

Cache API Responses:

// Cache-Control headers
response.headers.set('Cache-Control', 'public, max-age=300'); // 5 minutes
response.headers.set('ETag', generateETag(data));

// Client sends If-None-Match
if (request.headers.get('If-None-Match') === etag) {
  return new Response(null, { status: 304 }); // Not Modified
}

Cache Invalidation

Manual Invalidation

Invalidate on Updates:

async function updatePackage(packageId: string, updates: any) {
  // Update in database
  await db.packages.update({
    where: { id: packageId },
    data: updates
  });
  
  // Invalidate cache
  cache.delete(`package:${packageId}`);
  cache.delete(`packages:list:${tenantId}`);
  
  // Invalidate CDN (if using)
  await invalidateCDN(`/packages/${packageId}/*`);
}

Time-Based Invalidation

TTL-Based:

// Cache expires after TTL
const cache = new Map<string, { data: any; expiresAt: number }>();

function isExpired(key: string): boolean {
  const cached = cache.get(key);
  return !cached || cached.expiresAt < Date.now();
}

Event-Based Invalidation

Webhook-Based:

// Invalidate cache on webhook events
webhook.on('package.processing.completed', (event) => {
  cache.delete(`package:${event.data.package_id}`);
  cache.delete(`packages:list:${event.tenant_id}`);
});

Best Practices

1. Choose Appropriate TTLs

const TTLs = {
  packageMetadata: 5 * 60 * 1000,      // 5 minutes
  sessionData: 30 * 1000,              // 30 seconds
  packageList: 2 * 60 * 1000,         // 2 minutes
  staticAssets: 365 * 24 * 60 * 60 * 1000 // 1 year
};

2. Cache Keys

Use Descriptive Keys:

// Good keys
`package:${packageId}`
`packages:list:${tenantId}:page:${page}:limit:${limit}`
`session:${sessionId}`

// Bad keys
`pkg:${id}`
`list`
`data`

3. Handle Cache Misses

async function getWithFallback(key: string, fetchFn: () => Promise<any>) {
  // Try cache first
  const cached = await cache.get(key);
  if (cached) {
    return cached;
  }
  
  // Fetch on miss
  const data = await fetchFn();
  
  // Cache result
  await cache.set(key, data);
  
  return data;
}

4. Monitor Cache Performance

class CacheMetrics {
  hits = 0;
  misses = 0;
  
  get hitRate() {
    const total = this.hits + this.misses;
    return total > 0 ? this.hits / total : 0;
  }
}

const metrics = new CacheMetrics();

async function get(key: string) {
  const cached = await cache.get(key);
  if (cached) {
    metrics.hits++;
    return cached;
  }
  metrics.misses++;
  return null;
}

Related Documentation


Last Updated: 2025-01-15