/**
* Price Cache Module
*
* In-memory LRU cache for domain pricing data with TTL support.
* Reduces load on registrar websites and improves response times.
*/
export interface JokerPricingResult {
domain: string;
available: boolean;
pricing?: {
registration_1yr?: number;
renewal_1yr?: number;
transfer?: number;
currency: string; // e.g., "USD", "EUR"
};
special_offers?: {
description: string;
discount_price?: number;
}[];
source_url: string;
last_updated: string; // ISO timestamp
error?: string;
}
interface CachedEntry {
data: JokerPricingResult;
expiresAt: number; // Unix timestamp in milliseconds
}
/**
* Simple in-memory LRU cache with TTL
*/
export class PriceCache {
private cache: Map<string, CachedEntry>;
private maxSize: number = 1000; // Maximum number of cached entries
private ttlMs: number;
constructor() {
// Load TTL from environment variable (default: 6 hours)
const ttlHours = parseInt(process.env.MCP_DOMAIN_PRICING_CACHE_TTL_HOURS || '6', 10);
this.ttlMs = ttlHours * 60 * 60 * 1000;
this.cache = new Map();
}
/**
* Generate cache key for domain and registrar
*/
private getCacheKey(domain: string, registrar: string = 'joker'): string {
return `${domain.toLowerCase()}:${registrar}`;
}
/**
* Get cached pricing data if available and not expired
*/
get(domain: string, registrar: string = 'joker'): JokerPricingResult | null {
const key = this.getCacheKey(domain, registrar);
const entry = this.cache.get(key);
if (!entry) {
return null; // Cache miss
}
// Check if entry has expired
if (Date.now() > entry.expiresAt) {
this.cache.delete(key); // Remove expired entry
return null;
}
return entry.data;
}
/**
* Store pricing data in cache with TTL
*/
set(domain: string, data: JokerPricingResult, registrar: string = 'joker'): void {
const key = this.getCacheKey(domain, registrar);
const entry: CachedEntry = {
data,
expiresAt: Date.now() + this.ttlMs
};
this.cache.set(key, entry);
// Enforce max size using simple LRU strategy
// When cache is full, delete the oldest entry (first in Map)
if (this.cache.size > this.maxSize) {
const firstKey = this.cache.keys().next().value;
if (firstKey) {
this.cache.delete(firstKey);
}
}
}
/**
* Clear all cached entries
*/
clear(): void {
this.cache.clear();
}
/**
* Prune expired entries from cache
* Call this periodically to free up memory
*/
prune(): void {
const now = Date.now();
const keysToDelete: string[] = [];
for (const [key, entry] of this.cache.entries()) {
if (now > entry.expiresAt) {
keysToDelete.push(key);
}
}
for (const key of keysToDelete) {
this.cache.delete(key);
}
}
/**
* Get cache statistics
*/
getStats(): {
size: number;
maxSize: number;
ttlHours: number;
} {
return {
size: this.cache.size,
maxSize: this.maxSize,
ttlHours: this.ttlMs / (60 * 60 * 1000)
};
}
}
/**
* Singleton cache instance
* Shared across the entire MCP server
*/
export const priceCache = new PriceCache();
// Prune expired entries every hour
setInterval(() => {
priceCache.prune();
}, 60 * 60 * 1000);