Data Cache
Caching utilities for data loading.
Import
import {
MemoryCache,
getCache,
setCache,
cached,
generateCacheKey,
cacheKey,
buildCacheControl,
parseCacheControl,
Cached,
createDataCacheAdapter
} from '@ereo/data'MemoryCache
In-memory cache implementation with tag-based invalidation support.
Constructor
new MemoryCache()The MemoryCache class takes no constructor parameters.
CacheStorage Interface Methods
All methods are async and return Promises:
class MemoryCache implements CacheStorage {
// Get full cache entry with metadata
get<T>(key: string): Promise<CacheEntry<T> | null>
// Set a cache entry with full metadata
set<T>(key: string, entry: CacheEntry<T>): Promise<void>
// Delete a cache entry
delete(key: string): Promise<boolean>
// Delete all entries with a specific tag
deleteByTag(tag: string): Promise<void>
// Clear all entries
clear(): Promise<void>
// Get all keys
keys(): Promise<string[]>
// Get cache statistics
getStats(): { size: number; tags: number }
}CacheAdapter-Compatible Methods
For easier integration with @ereo/core:
class MemoryCache {
// Get just the value (not full entry)
getValue<T>(key: string): Promise<T | undefined>
// Set value with options (ttl in seconds, tags)
setValue<T>(key: string, value: T, options?: { ttl?: number; tags?: string[] }): Promise<void>
// Check if key exists
has(key: string): Promise<boolean>
// Invalidate by tag
invalidateTag(tag: string): Promise<void>
invalidateTags(tags: string[]): Promise<void>
// Get keys by tag
getByTag(tag: string): Promise<string[]>
// Get CacheAdapter wrapper
asCacheAdapter(): CacheAdapter & TaggedCache
}CacheEntry Interface
interface CacheEntry<T = unknown> {
value: T
timestamp: number
maxAge: number
staleWhileRevalidate?: number
tags: string[]
}Example
const cache = new MemoryCache()
// Set a value with full entry metadata
await cache.set('user:123', {
value: { name: 'Alice' },
timestamp: Date.now(),
maxAge: 3600,
tags: ['users', 'user-123']
})
// Get the full entry (includes metadata)
const entry = await cache.get('user:123')
if (entry) {
console.log(entry.value) // { name: 'Alice' }
}
// Or use simplified getValue for just the value
const user = await cache.getValue('user:123')
// Set with simplified interface
await cache.setValue('session:abc', { userId: 123 }, { ttl: 1800, tags: ['sessions'] })
// Check existence
if (await cache.has('user:123')) {
console.log('User is cached')
}
// Delete single entry
await cache.delete('user:123')
// Delete all entries with a tag
await cache.deleteByTag('users')
// Clear all
await cache.clear()
// Get all keys
const keys = await cache.keys()
// Get stats
const stats = cache.getStats()
console.log(`Cache has ${stats.size} entries and ${stats.tags} tags`)
// Get CacheAdapter for use with @ereo/core
const adapter = cache.asCacheAdapter()getCache / setCache
Global cache instance management functions.
Signatures
// Get (or create) the global cache instance
function getCache(): CacheStorage
// Set a custom cache storage implementation
function setCache(storage: CacheStorage): voidCacheStorage Interface
interface CacheStorage {
get<T>(key: string): Promise<CacheEntry<T> | null>
set<T>(key: string, entry: CacheEntry<T>): Promise<void>
delete(key: string): Promise<boolean | void>
deleteByTag(tag: string): Promise<void>
clear(): Promise<void>
keys(): Promise<string[]>
}Example
// Get the global cache instance
const cache = getCache()
// Use the cache
await cache.set('user:123', {
value: { name: 'Alice' },
timestamp: Date.now(),
maxAge: 3600,
tags: ['users']
})
const entry = await cache.get('user:123')
// Use a custom cache storage (e.g., Redis adapter)
import { RedisCache } from './my-redis-cache'
setCache(new RedisCache({ url: process.env.REDIS_URL }))cached
Wraps a function with caching and stale-while-revalidate support.
Signature
function cached<T>(
key: string,
fn: () => Promise<T>,
options: CacheOptions
): Promise<T>CacheOptions
interface CacheOptions {
maxAge?: number // TTL in seconds (default: 60)
staleWhileRevalidate?: number // Additional seconds to serve stale while refreshing
tags?: string[] // Tags for invalidation
private?: boolean // Mark as private (for Cache-Control header)
}Example
export const loader = createLoader(async ({ params }) => {
// Cache the database call
const post = await cached(
`post:${params.id}`,
() => db.posts.find(params.id),
{ maxAge: 3600, tags: ['posts', `post-${params.id}`] }
)
return { post }
})With Conditional Caching
const post = await cached(
`post:${params.id}`,
async () => {
const post = await db.posts.find(params.id)
// Don't cache drafts - just return without caching
if (post.status === 'draft') {
return post // Will still be cached; handle draft logic elsewhere
}
return post
},
{ maxAge: 3600, tags: ['posts'] }
)Note: The
cachedfunction always caches the result. For conditional caching, consider usinggetCache()andsetCache()directly to control when values are stored.
cacheKey
Generates a cache key from a prefix and parts, joined with :.
Signature
function cacheKey(prefix: string, ...parts: (string | number)[]): stringExample
const key = cacheKey('posts', userId, 'page', page)
// 'posts:123:page:1'generateCacheKey
Generates a cache key from a Request object using the HTTP method and URL.
Signature
function generateCacheKey(request: Request): stringExample
const key = generateCacheKey(request)
// 'GET:/posts?page=2'buildCacheControl
Builds a Cache-Control header string from CacheOptions.
Signature
function buildCacheControl(options: CacheOptions): stringOptions
Uses the CacheOptions interface from @ereo/core:
interface CacheOptions {
maxAge?: number // Seconds to cache
staleWhileRevalidate?: number // Seconds to serve stale while revalidating
tags?: string[] // Cache tags (not included in header output)
private?: boolean // Use private instead of public directive
}Note: For advanced Cache-Control directives like
s-maxage,no-cache,no-store,must-revalidate, orimmutable, set theCache-Controlheader manually viacontext.responseHeaders.
Example
const header = buildCacheControl({
maxAge: 3600,
staleWhileRevalidate: 86400
})
// 'public, max-age=3600, stale-while-revalidate=86400'
const privateHeader = buildCacheControl({
maxAge: 60,
private: true
})
// 'private, max-age=60'parseCacheControl
Parses a Cache-Control header string back into CacheOptions.
Signature
function parseCacheControl(header: string): CacheOptionsExample
const options = parseCacheControl('public, max-age=3600, stale-while-revalidate=86400')
// {
// public: true,
// maxAge: 3600,
// staleWhileRevalidate: 86400
// }Cached Decorator
Decorator for caching class methods.
Example
class PostService {
@Cached({ maxAge: 3600, tags: ['posts'] })
async getPost(id: string) {
return await db.posts.find(id)
}
@Cached({ maxAge: 300 })
async getPopularPosts() {
return await db.posts.findPopular()
}
}createDataCacheAdapter
Creates a CacheAdapter from MemoryCache.
Signature
function createDataCacheAdapter(cache: MemoryCache): CacheAdapterExample
const memCache = new MemoryCache()
const adapter = createDataCacheAdapter(memCache)
// Use as CacheAdapter
await adapter.set('key', value, { ttl: 3600 })
const value = await adapter.get('key')Caching in Loaders
Basic Caching
export const loader = createLoader(async ({ params }) => {
const post = await cached(
cacheKey('post', params.id),
() => db.posts.find(params.id),
{ maxAge: 3600, tags: ['posts'] }
)
return { post }
})Conditional Caching
export const loader = createLoader(async ({ params, context }) => {
const user = context.get('user')
// Different cache strategies based on auth
const post = await cached(
cacheKey('post', params.id, user ? 'auth' : 'anon'),
() => db.posts.find(params.id),
{
maxAge: user ? 60 : 3600,
tags: ['posts', `post-${params.id}`]
}
)
return { post }
})Cache with Fallback
export const loader = createLoader(async ({ params }) => {
const cache = getCache()
const entry = await cache.get(`post:${params.id}`)
let post = entry?.value
if (!post) {
post = await db.posts.find(params.id)
if (post) {
await cache.set(`post:${params.id}`, {
value: post,
timestamp: Date.now(),
maxAge: 3600,
tags: ['posts', `post-${params.id}`]
})
}
}
if (!post) {
throw new Response('Not Found', { status: 404 })
}
return { post }
})Cache Patterns
Stale-While-Revalidate
The cached function has built-in SWR support:
// Use staleWhileRevalidate option for automatic background refresh
const post = await cached(
`post:${params.id}`,
() => db.posts.find(params.id),
{
maxAge: 3600, // Fresh for 1 hour
staleWhileRevalidate: 86400, // Serve stale for up to 24 more hours while refreshing
tags: ['posts']
}
)Or implement custom SWR logic:
async function getWithSWR<T>(
key: string,
fetcher: () => Promise<T>,
maxAge: number,
swr: number
): Promise<T> {
const cache = getCache()
const entry = await cache.get<T>(key)
const now = Date.now()
if (entry) {
const age = (now - entry.timestamp) / 1000
// Fresh: return cached value
if (age < maxAge) {
return entry.value
}
// Stale but within SWR window: return cached and refresh in background
if (age < maxAge + swr) {
// Fire-and-forget background refresh
fetcher().then(async value => {
await cache.set(key, {
value,
timestamp: Date.now(),
maxAge,
staleWhileRevalidate: swr,
tags: []
})
})
return entry.value
}
}
// Expired or missing: fetch fresh
const value = await fetcher()
await cache.set(key, {
value,
timestamp: now,
maxAge,
staleWhileRevalidate: swr,
tags: []
})
return value
}Cache Warming
async function warmCache() {
const cache = getCache()
const popularPosts = await db.posts.findPopular(10)
for (const post of popularPosts) {
await cache.set(`post:${post.id}`, {
value: post,
timestamp: Date.now(),
maxAge: 3600,
tags: ['posts', `post-${post.id}`]
})
}
}Cache Busting
async function bustCache(pattern: RegExp) {
const cache = getCache()
const keys = await cache.keys()
for (const key of keys) {
if (pattern.test(key)) {
await cache.delete(key)
}
}
}
// Bust all post caches
await bustCache(/^post:/)
// Or use tag-based invalidation (preferred)
await cache.deleteByTag('posts')