S3 Link Responses
TTL is calculated from the iRacing API’s presigned URL expires field
The iRacing Data Client SDK supports opt-in caching via pluggable stores powered by @http-client-toolkit/core. When a CacheStore is provided, resolved GET results are cached automatically. For iRacing’s usual presigned S3 responses, TTL is derived from the presigned URL’s expiry.
Pass a CacheStore implementation via the stores option:
import { IRacingDataClient } from 'iracing-data-client';
const iracing = new IRacingDataClient({ auth: { /* ... */ }, stores: { cache: myCacheStore, },});
// First call — network request, response is cachedconst cars1 = await iracing.car.get();
// Second call within cache period — returned from storeconst cars2 = await iracing.car.get();When caching is enabled:
Cache-Control: max-age from the API expires field; direct responses use normal HTTP cache headers, or the toolkit fallback TTL when no cache headers are presentCache keys are derived from the request URL and parameters, so different parameters produce different cache entries:
await iracing.member.get({ custIds: [123456] }); // Cache key 1await iracing.member.get({ custIds: [789012] }); // Cache key 2await iracing.member.get({ custIds: [123456] }); // Hits cache key 1The SDK does not hard-code per-endpoint cache durations. Cache lifetime is determined by the response metadata that reaches @http-client-toolkit/core:
S3 Link Responses
TTL is calculated from the iRacing API’s presigned URL expires field
Direct HTTP Responses
Standard HTTP cache headers such as Cache-Control, Expires, and Last-Modified are respected
No Cache Headers
The toolkit uses its fallback TTL for cacheable responses. In @http-client-toolkit/core@4.2.0, that fallback is 3600 seconds.
If you need endpoint-specific freshness rules, layer an application cache on top of the SDK or implement them inside your chosen store.
Install a store package separately. Published toolkit stores include @http-client-toolkit/store-memory, @http-client-toolkit/store-sqlite, and @http-client-toolkit/store-dynamodb.
import { IRacingDataClient } from 'iracing-data-client';import { InMemoryCacheStore } from '@http-client-toolkit/store-memory';
const iracing = new IRacingDataClient({ auth: { /* ... */ }, stores: { cache: new InMemoryCacheStore({ maxItems: 1000, maxMemoryBytes: 50_000_000, }), },});See the http-client-toolkit repository for the latest store package list and options.
Prefer a published store package where possible. If you implement your own, use the CacheStore contract from @http-client-toolkit/core. The value is toolkit-managed cache data, not a raw Response, so store and return it unchanged.
interface CacheStore<T = unknown> { get(hash: string): Promise<T | undefined>; set(hash: string, value: T, ttlSeconds: number): Promise<void>; delete(hash: string): Promise<void>; clear(scope?: string): Promise<void>; setWithTags( hash: string, value: T, ttlSeconds: number, tags: string[] ): Promise<void>; invalidateByTag(tag: string): Promise<number>; invalidateByTags(tags: string[]): Promise<number>;}Simple in-memory example:
import type { CacheStore } from '@http-client-toolkit/core';
class SimpleCacheStore<T = unknown> implements CacheStore<T> { private entries = new Map<string, { value: T; expiresAt: number; tags: Set<string>; }>(); private tagIndex = new Map<string, Set<string>>();
async get(hash: string): Promise<T | undefined> { const entry = this.entries.get(hash); if (!entry) return undefined; if (entry.expiresAt <= Date.now()) { await this.delete(hash); return undefined; } return entry.value; }
async set(hash: string, value: T, ttlSeconds: number): Promise<void> { await this.setWithTags(hash, value, ttlSeconds, []); }
async setWithTags( hash: string, value: T, ttlSeconds: number, tags: string[] ): Promise<void> { await this.delete(hash);
const tagSet = new Set(tags); this.entries.set(hash, { value, expiresAt: Date.now() + ttlSeconds * 1000, tags: tagSet, });
for (const tag of tagSet) { const hashes = this.tagIndex.get(tag) ?? new Set<string>(); hashes.add(hash); this.tagIndex.set(tag, hashes); } }
async delete(hash: string): Promise<void> { const entry = this.entries.get(hash); if (!entry) return;
this.entries.delete(hash); for (const tag of entry.tags) { const hashes = this.tagIndex.get(tag); hashes?.delete(hash); if (hashes?.size === 0) { this.tagIndex.delete(tag); } } }
async clear(scope?: string): Promise<void> { for (const hash of [...this.entries.keys()]) { if (!scope || hash.startsWith(scope)) { await this.delete(hash); } } }
async invalidateByTag(tag: string): Promise<number> { const hashes = [...(this.tagIndex.get(tag) ?? [])]; await Promise.all(hashes.map((hash) => this.delete(hash))); return hashes.length; }
async invalidateByTags(tags: string[]): Promise<number> { const hashes = new Set<string>(); for (const tag of tags) { for (const hash of this.tagIndex.get(tag) ?? []) { hashes.add(hash); } }
await Promise.all([...hashes].map((hash) => this.delete(hash))); return hashes.size; }}
const iracing = new IRacingDataClient({ auth: { /* ... */ }, stores: { cache: new SimpleCacheStore(), },});If you need caching logic beyond what the store provides (e.g. custom TTLs per endpoint, conditional caching), you can layer your own cache on top:
class CachedIRacingService { private cache = new Map<string, { data: unknown; expires: number }>();
constructor(private iracing: IRacingDataClient) {}
async getMemberInfo( custId: number, cacheDuration = 5 * 60 * 1000 // 5 minutes ) { const cacheKey = `member:${custId}`; const cached = this.cache.get(cacheKey);
if (cached && cached.expires > Date.now()) { return cached.data; }
const data = await this.iracing.member.get({ custIds: [custId] });
this.cache.set(cacheKey, { data, expires: Date.now() + cacheDuration, });
return data; }
clearCache() { this.cache.clear(); }}Warm up the cache on application start:
async function preloadCache(iracing: IRacingDataClient) { await Promise.all([ iracing.car.get(), iracing.track.get(), iracing.series.get(), iracing.constants.categories(), iracing.constants.divisions(), ]);}
await preloadCache(iracing);Combine requests to reduce cache entries and API calls:
// Instead of fetching members one at a timeconst ids = [123, 456, 789];
// Single batched requestconst members = await iracing.member.get({ custIds: ids });Use Pluggable Stores
Pass a CacheStore via stores.cache for automatic, TTL-aware caching with no extra code
Cache Static Data
Aggressively cache data that rarely changes (cars, tracks, series)
Monitor Performance
Track cache hit rates to validate your caching strategy
Don't Over-Cache
Avoid caching real-time data that needs to be fresh
stores option