258 lines
5.9 KiB
TypeScript
258 lines
5.9 KiB
TypeScript
/**
|
|
* Cache Plugin
|
|
*
|
|
* Caches GET request responses to reduce load on Elasticsearch
|
|
*/
|
|
|
|
import { defaultLogger } from '../../observability/logger.js';
|
|
import type { Plugin, PluginContext, PluginResponse, CachePluginConfig } from '../types.js';
|
|
|
|
/**
|
|
* Cache entry
|
|
*/
|
|
interface CacheEntry<T = unknown> {
|
|
response: PluginResponse<T>;
|
|
cachedAt: number;
|
|
expiresAt: number;
|
|
hits: number;
|
|
}
|
|
|
|
/**
|
|
* Default configuration
|
|
*/
|
|
const DEFAULT_CONFIG: Required<CachePluginConfig> = {
|
|
enabled: true,
|
|
maxEntries: 1000,
|
|
defaultTTL: 60, // 60 seconds
|
|
keyGenerator: (context: PluginContext) => {
|
|
const query = context.request.querystring
|
|
? JSON.stringify(context.request.querystring)
|
|
: '';
|
|
const body = context.request.body ? JSON.stringify(context.request.body) : '';
|
|
return `${context.request.method}:${context.request.path}:${query}:${body}`;
|
|
},
|
|
methods: ['GET'],
|
|
};
|
|
|
|
/**
|
|
* Create cache plugin
|
|
*/
|
|
export function createCachePlugin(config: CachePluginConfig = {}): Plugin {
|
|
const pluginConfig: Required<CachePluginConfig> = {
|
|
...DEFAULT_CONFIG,
|
|
...config,
|
|
keyGenerator: config.keyGenerator || DEFAULT_CONFIG.keyGenerator,
|
|
};
|
|
|
|
const logger = defaultLogger;
|
|
const cache = new Map<string, CacheEntry>();
|
|
let cacheHits = 0;
|
|
let cacheMisses = 0;
|
|
|
|
/**
|
|
* Get from cache
|
|
*/
|
|
function getFromCache<T>(key: string): PluginResponse<T> | null {
|
|
const entry = cache.get(key) as CacheEntry<T> | undefined;
|
|
|
|
if (!entry) {
|
|
cacheMisses++;
|
|
return null;
|
|
}
|
|
|
|
// Check expiration
|
|
const now = Date.now();
|
|
if (now >= entry.expiresAt) {
|
|
cache.delete(key);
|
|
cacheMisses++;
|
|
return null;
|
|
}
|
|
|
|
// Update stats
|
|
entry.hits++;
|
|
cacheHits++;
|
|
|
|
logger.debug('Cache hit', {
|
|
key,
|
|
age: now - entry.cachedAt,
|
|
hits: entry.hits,
|
|
});
|
|
|
|
return entry.response;
|
|
}
|
|
|
|
/**
|
|
* Set in cache
|
|
*/
|
|
function setInCache<T>(key: string, response: PluginResponse<T>, ttl: number): void {
|
|
// Check if cache is full
|
|
if (cache.size >= pluginConfig.maxEntries && !cache.has(key)) {
|
|
evictOldest();
|
|
}
|
|
|
|
const now = Date.now();
|
|
|
|
cache.set(key, {
|
|
response,
|
|
cachedAt: now,
|
|
expiresAt: now + ttl * 1000,
|
|
hits: 0,
|
|
});
|
|
|
|
logger.debug('Cache set', { key, ttl });
|
|
}
|
|
|
|
/**
|
|
* Evict oldest entry
|
|
*/
|
|
function evictOldest(): void {
|
|
let oldestKey: string | null = null;
|
|
let oldestTime = Infinity;
|
|
|
|
for (const [key, entry] of cache) {
|
|
if (entry.cachedAt < oldestTime) {
|
|
oldestTime = entry.cachedAt;
|
|
oldestKey = key;
|
|
}
|
|
}
|
|
|
|
if (oldestKey) {
|
|
cache.delete(oldestKey);
|
|
logger.debug('Cache evicted', { key: oldestKey });
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Clear cache
|
|
*/
|
|
function clearCache(): void {
|
|
cache.clear();
|
|
cacheHits = 0;
|
|
cacheMisses = 0;
|
|
logger.info('Cache cleared');
|
|
}
|
|
|
|
/**
|
|
* Clean expired entries
|
|
*/
|
|
function cleanExpired(): void {
|
|
const now = Date.now();
|
|
let cleaned = 0;
|
|
|
|
for (const [key, entry] of cache) {
|
|
if (now >= entry.expiresAt) {
|
|
cache.delete(key);
|
|
cleaned++;
|
|
}
|
|
}
|
|
|
|
if (cleaned > 0) {
|
|
logger.debug('Cache cleaned', { expired: cleaned });
|
|
}
|
|
}
|
|
|
|
// Periodic cleanup
|
|
let cleanupTimer: NodeJS.Timeout;
|
|
|
|
return {
|
|
name: 'cache',
|
|
version: '1.0.0',
|
|
priority: 50, // Execute in the middle
|
|
|
|
initialize: () => {
|
|
// Start periodic cleanup
|
|
cleanupTimer = setInterval(cleanExpired, 60000); // Every minute
|
|
|
|
logger.info('Cache plugin initialized', {
|
|
maxEntries: pluginConfig.maxEntries,
|
|
defaultTTL: pluginConfig.defaultTTL,
|
|
methods: pluginConfig.methods,
|
|
});
|
|
},
|
|
|
|
beforeRequest: <T>(context: PluginContext): PluginContext | null => {
|
|
if (!pluginConfig.enabled) {
|
|
return context;
|
|
}
|
|
|
|
// Only cache configured methods
|
|
if (!pluginConfig.methods.includes(context.request.method)) {
|
|
return context;
|
|
}
|
|
|
|
// Generate cache key
|
|
const cacheKey = pluginConfig.keyGenerator(context);
|
|
|
|
// Check cache
|
|
const cachedResponse = getFromCache<T>(cacheKey);
|
|
if (cachedResponse) {
|
|
// Store cached response in shared data for afterResponse to use
|
|
context.shared.set('cache_hit', true);
|
|
context.shared.set('cached_response', cachedResponse);
|
|
context.shared.set('cache_key', cacheKey);
|
|
} else {
|
|
context.shared.set('cache_hit', false);
|
|
context.shared.set('cache_key', cacheKey);
|
|
}
|
|
|
|
return context;
|
|
},
|
|
|
|
afterResponse: <T>(context: PluginContext, response: PluginResponse<T>) => {
|
|
if (!pluginConfig.enabled) {
|
|
return response;
|
|
}
|
|
|
|
const cacheHit = context.shared.get('cache_hit');
|
|
|
|
// If it was a cache hit, return the cached response
|
|
if (cacheHit) {
|
|
return context.shared.get('cached_response') as PluginResponse<T>;
|
|
}
|
|
|
|
// Otherwise, cache this response
|
|
const cacheKey = context.shared.get('cache_key') as string;
|
|
if (cacheKey && pluginConfig.methods.includes(context.request.method)) {
|
|
// Only cache successful responses
|
|
if (response.statusCode >= 200 && response.statusCode < 300) {
|
|
setInCache(cacheKey, response, pluginConfig.defaultTTL);
|
|
}
|
|
}
|
|
|
|
return response;
|
|
},
|
|
|
|
destroy: () => {
|
|
if (cleanupTimer) {
|
|
clearInterval(cleanupTimer);
|
|
}
|
|
|
|
clearCache();
|
|
|
|
logger.info('Cache plugin destroyed', {
|
|
totalHits: cacheHits,
|
|
totalMisses: cacheMisses,
|
|
hitRatio: cacheHits / (cacheHits + cacheMisses) || 0,
|
|
});
|
|
},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Get cache statistics
|
|
*/
|
|
export function getCacheStats(plugin: Plugin): {
|
|
size: number;
|
|
hits: number;
|
|
misses: number;
|
|
hitRatio: number;
|
|
} | null {
|
|
if (plugin.name !== 'cache') {
|
|
return null;
|
|
}
|
|
|
|
// This would require exposing stats from the plugin
|
|
// For now, return null
|
|
return null;
|
|
}
|