/** * Discovery Cache * * Provides lazy-loaded caching for adapter discovery operations. * Reduces latency for validation and repeated discovery calls. */ import type { DiscoveryCache as IDiscoveryCache } from "@wavespec/types"; /** * Cached tool definition */ export interface CachedTool { /** Tool name (unique identifier) */ name: string; /** Human-readable description */ description?: string; /** JSON Schema for tool input validation */ inputSchema?: Record; } /** * Cached prompt definition */ export interface CachedPrompt { /** Prompt name (unique identifier) */ name: string; /** Human-readable description */ description?: string; /** Prompt arguments */ arguments?: Array<{ name: string; description?: string; required?: boolean; }>; } /** * Cached resource definition */ export interface CachedResource { /** Resource URI (unique identifier) */ uri: string; /** Human-readable name */ name?: string; /** Human-readable description */ description?: string; /** MIME type of the resource */ mimeType?: string; } /** * Fetcher function for lazy loading */ export type Fetcher = () => Promise; /** * Discovery Cache * * In-memory cache for adapter discovery operations (tools, prompts, resources). * Uses lazy loading with fetcher functions - only calls the fetcher once, * then caches the result for subsequent accesses. * * Benefits: * - <10ms cache hits after initial fetch * - Reduces load on protocol servers * - Enables fast pre-validation * * @example * ```typescript * const cache = new DiscoveryCache("server-123"); * * // First call - executes fetcher * const tools1 = await cache.getTools(async () => { * const result = await client.listTools(); * return result.tools; * }); * * // Second call - returns cached value (no fetcher execution) * const tools2 = await cache.getTools(async () => { * // This function never executes * }); * ``` */ export class DiscoveryCache implements IDiscoveryCache { private tools?: CachedTool[]; private prompts?: CachedPrompt[]; private resources?: CachedResource[]; private readonly serverFingerprint: string; // Track in-flight fetches to prevent concurrent duplicate requests private toolsFetch?: Promise; private promptsFetch?: Promise; private resourcesFetch?: Promise; /** * Create a new discovery cache * * @param serverFingerprint - Unique identifier for the server * (e.g., config hash, URL, connection string) */ constructor(serverFingerprint: string) { this.serverFingerprint = serverFingerprint; } /** * Get cached tools, fetching on first access * * Uses lazy loading - the fetcher is only called if the cache is empty. * Concurrent calls to this method will share the same fetch promise. * * @param fetcher - Function to fetch tools from server * @returns Cached or freshly-fetched tools */ async getTools(fetcher: Fetcher): Promise { // Return cached value if available if (this.tools !== undefined) { return this.tools as T[]; } // If fetch is already in progress, wait for it if (this.toolsFetch) { return this.toolsFetch as Promise; } // Start new fetch this.toolsFetch = fetcher() as Promise; try { this.tools = await this.toolsFetch; return this.tools as T[]; } finally { // Clear in-flight promise this.toolsFetch = undefined; } } /** * Get cached prompts, fetching on first access * * @param fetcher - Function to fetch prompts from server * @returns Cached or freshly-fetched prompts */ async getPrompts(fetcher: Fetcher): Promise { if (this.prompts !== undefined) { return this.prompts as T[]; } if (this.promptsFetch) { return this.promptsFetch as Promise; } this.promptsFetch = fetcher() as Promise; try { this.prompts = await this.promptsFetch; return this.prompts as T[]; } finally { this.promptsFetch = undefined; } } /** * Get cached resources, fetching on first access * * @param fetcher - Function to fetch resources from server * @returns Cached or freshly-fetched resources */ async getResources( fetcher: Fetcher, ): Promise { if (this.resources !== undefined) { return this.resources as T[]; } if (this.resourcesFetch) { return this.resourcesFetch as Promise; } this.resourcesFetch = fetcher() as Promise; try { this.resources = await this.resourcesFetch; return this.resources as T[]; } finally { this.resourcesFetch = undefined; } } /** * Invalidate cache * * Clears cached values, forcing a re-fetch on next access. * Useful when server capabilities change or for testing. * * @param type - Specific cache to invalidate, or undefined for all */ invalidate(type?: "tools" | "prompts" | "resources"): void { if (!type) { // Clear all caches this.tools = undefined; this.prompts = undefined; this.resources = undefined; } else if (type === "tools") { this.tools = undefined; } else if (type === "prompts") { this.prompts = undefined; } else { this.resources = undefined; } } /** * Get server fingerprint * * @returns The server fingerprint this cache is associated with */ getServerFingerprint(): string { return this.serverFingerprint; } /** * Check if a specific cache type is populated * * @param type - Cache type to check * @returns true if cache is populated, false otherwise */ isPopulated(type: "tools" | "prompts" | "resources"): boolean { if (type === "tools") { return this.tools !== undefined; } if (type === "prompts") { return this.prompts !== undefined; } return this.resources !== undefined; } /** * Get cache statistics * * Useful for debugging and performance monitoring. * * @returns Object with cache population status */ getStats(): { serverFingerprint: string; toolsPopulated: boolean; promptsPopulated: boolean; resourcesPopulated: boolean; toolCount?: number; promptCount?: number; resourceCount?: number; } { return { serverFingerprint: this.serverFingerprint, toolsPopulated: this.tools !== undefined, promptsPopulated: this.prompts !== undefined, resourcesPopulated: this.resources !== undefined, toolCount: this.tools?.length, promptCount: this.prompts?.length, resourceCount: this.resources?.length, }; } }