Caching
Per-method TTL caching with pluggable adapters
Opt-In Caching
Caching is off by default. Riot responses are either immutable (match bodies, which you usually store yourself) or time-sensitive (live games, ranked entries), so the library does not guess for you. Pass a cache adapter to enable it:
import { createClient, MemoryCache } from '@wardbox/whisper/core';
const client = createClient({
apiKey: 'RGAPI-your-key-here',
cache: new MemoryCache(),
});With a cache, GET requests are stored keyed by route, path, and query parameters for cacheTtl seconds (default 300). Cache keys include a hash of your API key to prevent cross-key cache pollution when rotating keys.
MemoryCache is bounded by entry count (default 1000, FIFO eviction once full, expired entries swept first). The bound is entries, not bytes, so size it for the largest bodies you cache:
// match-v5 bodies are ~1.3MB each; keep at most ~65MB resident
const client = createClient({
apiKey: 'RGAPI-your-key-here',
cache: new MemoryCache({ maxEntries: 50 }),
});TTL Configuration
Configure TTLs per API path pattern using the cacheTtl option. Rather than a separate byMethod map, patterns are top-level keys on the config object. Pattern matching checks if the request path contains the pattern string:
import { createClient } from '@wardbox/whisper/core';
const client = createClient({
apiKey: 'RGAPI-your-key-here',
cacheTtl: {
default: 60, // 60 seconds for everything else
'/lol/summoner/v4': 300, // 5 minutes for summoner data
'/lol/match/v5': 30, // 30 seconds for match data
'/lol/spectator/v5': 0, // Never cache live game data
'/lol/league/v4': 120, // 2 minutes for league data
},
});A TTL of 0 means "do not cache" -- the request always hits the API. This is the correct default for live game data like spectator endpoints, where stale data is actively harmful.
Bypassing the Cache for One Call
Pass { cache: false } to client.request to skip the cached entry and fetch from Riot. The fresh response is stored, so later reads see it:
const summoner = await client.request<LolSummoner>(
'na1',
`/lol/summoner/v4/summoners/by-puuid/${puuid}`,
'summoner-v4.getByPuuid',
{ cache: false },
);Custom Cache Adapter
Replace the built-in memory cache with any storage backend by implementing the CacheAdapter interface:
import type { CacheAdapter } from '@wardbox/whisper/core';
const redisCache: CacheAdapter = {
async get<T>(key: string): Promise<T | undefined> {
const data = await redis.get(key);
return data ? JSON.parse(data) : undefined;
},
async set<T>(key: string, value: T, ttlSeconds: number): Promise<void> {
await redis.set(key, JSON.stringify(value), 'EX', ttlSeconds);
},
async delete(key: string): Promise<void> {
await redis.del(key);
},
async has(key: string): Promise<boolean> {
return (await redis.exists(key)) === 1;
},
};Pass it to the client:
import { createClient } from '@wardbox/whisper/core';
const client = createClient({
apiKey: 'RGAPI-your-key-here',
cache: redisCache,
});API-Key-Aware Cache Keys
Cache keys include a short hash derived from your API key. This prevents a subtle bug: if you rotate API keys and the old key's cached responses are served to the new key, Riot may flag the mismatch.
The key itself is never stored in the cache -- only a djb2 hash prefix. This works in all runtimes (no crypto module needed) and is deterministic across requests.
Cache key format: {key-hash}:{route}:{path}?{sorted-params}
Example: k8f2a:na1:/lol/summoner/v4/summoners/by-puuid/xyzCache Behavior
- Nothing is cached unless you pass a
cacheadapter. - Only GET requests are cached. POST, PUT, and DELETE always hit the API.
- The cache is checked before the middleware pipeline runs -- cached responses skip middleware entirely for performance.
MemoryCachedrops expired entries on read, and sweeps them (then evicts oldest, FIFO) whenmaxEntriesis reached.