import * as fs from 'fs'; import * as path from 'path'; import type { OpenAPISpec, ParsedSpec } from './types.js'; /** Default maximum number of cached specs */ export const DEFAULT_CACHE_MAX_SIZE = 10; /** Default cache TTL in minutes */ export const DEFAULT_CACHE_TTL_MINUTES = 15; export interface CacheEntry { spec: OpenAPISpec; metadata: ParsedSpec; dereferenced: boolean; timestamp: number; } export class SpecCache { private cache: Map; private maxSize: number; private ttlMs: number; constructor(maxSize = DEFAULT_CACHE_MAX_SIZE, ttlMinutes = DEFAULT_CACHE_TTL_MINUTES) { this.cache = new Map(); this.maxSize = maxSize; this.ttlMs = ttlMinutes * 60 * 1000; } get(key: string): CacheEntry | undefined { const entry = this.cache.get(key); if (!entry) return undefined; // Check TTL expiration if (Date.now() - entry.timestamp > this.ttlMs) { this.cache.delete(key); return undefined; } // Move to end for LRU (delete and re-add) this.cache.delete(key); this.cache.set(key, entry); return entry; } set(key: string, entry: Omit): void { // Evict oldest entry if at capacity if (this.cache.size >= this.maxSize) { const firstKey = this.cache.keys().next().value; if (firstKey) { this.cache.delete(firstKey); } } this.cache.set(key, { ...entry, timestamp: Date.now() }); } invalidate(key?: string): void { if (key) { this.cache.delete(key); } else { this.cache.clear(); } } get size(): number { return this.cache.size; } } /** * Generate cache key for a spec path. * - Remote URLs: normalized URL * - Local files: absolute path + mtime for change detection */ export function getCacheKey(specPath: string): string { // Remote URL if (specPath.startsWith('http://') || specPath.startsWith('https://')) { return normalizeUrl(specPath); } // Local file: include mtime for change detection try { const resolved = path.resolve(specPath); const stat = fs.statSync(resolved); return `file:${resolved}:${stat.mtimeMs}`; } catch { // File doesn't exist or can't be read - use path only return `file:${path.resolve(specPath)}`; } } /** * Normalize URL for consistent cache keys. * - Removes trailing slashes * - Lowercases protocol and host */ function normalizeUrl(url: string): string { try { const parsed = new URL(url); // Lowercase protocol and host let normalized = `${parsed.protocol}//${parsed.host.toLowerCase()}`; // Add pathname (remove trailing slash unless it's the root) const pathname = parsed.pathname.replace(/\/+$/, '') || '/'; normalized += pathname; // Add search params if present (sorted for consistency) if (parsed.search) { const params = new URLSearchParams(parsed.search); params.sort(); normalized += `?${params.toString()}`; } return normalized; } catch { // Invalid URL, use as-is return url; } } // Global cache instance export const specCache = new SpecCache();