diff --git a/packages/package-executor/src/executor.ts b/packages/package-executor/src/executor.ts index c980950..556ba00 100644 --- a/packages/package-executor/src/executor.ts +++ b/packages/package-executor/src/executor.ts @@ -6,6 +6,70 @@ import type { ExecutionResult, ExecutorOptions } from './types.js'; const DEFAULT_TIMEOUT = 10000; // 10 seconds +const CACHE_TTL_MS = 2 * 60 * 1000; // 2 minutes + +// In-memory cache for execution results +interface CacheEntry { + result: ExecutionResult; + expiresAt: number; +} + +const executionCache = new Map(); + +/** + * Generate a cache key from execution parameters + */ +function getCacheKey( + packageName: string, + functionName: string, + params: Record +): string { + const paramsKey = JSON.stringify(params, Object.keys(params).sort()); + return `${packageName}::${functionName}::${paramsKey}`; +} + +/** + * Get cached result if still valid + */ +function getCachedResult(cacheKey: string): ExecutionResult | null { + const entry = executionCache.get(cacheKey); + if (!entry) return null; + + if (Date.now() > entry.expiresAt) { + executionCache.delete(cacheKey); + return null; + } + + return entry.result; +} + +/** + * Store result in cache + */ +function setCachedResult(cacheKey: string, result: ExecutionResult): void { + // Only cache successful results + if (!result.success) return; + + executionCache.set(cacheKey, { + result, + expiresAt: Date.now() + CACHE_TTL_MS, + }); +} + +/** + * Periodically clean up expired cache entries + */ +function cleanupCache(): void { + const now = Date.now(); + for (const [key, entry] of executionCache.entries()) { + if (now > entry.expiresAt) { + executionCache.delete(key); + } + } +} + +// Run cleanup every minute +setInterval(cleanupCache, 60 * 1000).unref(); // Ensure URL has protocol function getSandboxUrl(): string { @@ -31,6 +95,16 @@ export async function executePackage( const startTime = Date.now(); const timeout = options.timeout || DEFAULT_TIMEOUT; + // Check cache first + const cacheKey = getCacheKey(packageName, functionName, params); + const cachedResult = getCachedResult(cacheKey); + if (cachedResult) { + return { + ...cachedResult, + executionTimeMs: 0, // Indicate cache hit with 0ms execution time + }; + } + try { // Call the remote sandbox service const controller = new AbortController(); @@ -71,12 +145,17 @@ export async function executePackage( executionTimeMs?: number; }; - return { + const executionResult: ExecutionResult = { success: result.success, output: result.output, error: result.error, executionTimeMs: result.executionTimeMs || executionTimeMs, }; + + // Cache successful results + setCachedResult(cacheKey, executionResult); + + return executionResult; } catch (error) { const executionTimeMs = Date.now() - startTime; @@ -97,9 +176,12 @@ export async function executePackage( } /** - * Clear the package cache on the remote sandbox + * Clear the package cache on the remote sandbox and local execution cache */ export async function clearCache(): Promise { + // Clear local execution cache + executionCache.clear(); + try { const response = await fetch(`${SANDBOX_URL}/cache/clear`, { method: 'POST', @@ -114,6 +196,16 @@ export async function clearCache(): Promise { } } +/** + * Get current cache statistics + */ +export function getCacheStats(): { size: number; ttlMs: number } { + return { + size: executionCache.size, + ttlMs: CACHE_TTL_MS, + }; +} + /** * Check if the sandbox service is healthy */