feat(package-executor): add 2-minute in-memory cache for execution results

- Cache successful execution results with 2-minute TTL
- Automatic cleanup of expired entries every minute
- Cache hit returns 0ms execution time to indicate cached response
- Add getCacheStats() helper for debugging
- clearCache() now clears both local and remote sandbox cache
This commit is contained in:
Ajax Davis 2025-12-17 11:29:22 +10:00
parent 00718382fc
commit 30e9bb41b7

View file

@ -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<string, CacheEntry>();
/**
* Generate a cache key from execution parameters
*/
function getCacheKey(
packageName: string,
functionName: string,
params: Record<string, unknown>
): 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<void> {
// 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<void> {
}
}
/**
* 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
*/