BREAKING CHANGE(core): Refactor to v3: introduce modular core/domain architecture, plugin system, observability and strict TypeScript configuration; remove legacy classes
This commit is contained in:
257
ts/core/plugins/built-in/cache-plugin.ts
Normal file
257
ts/core/plugins/built-in/cache-plugin.ts
Normal file
@@ -0,0 +1,257 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
Reference in New Issue
Block a user