templui-mcp-server
Version:
A Model Context Protocol (MCP) server for TemplUI components, providing AI assistants with access to component source code, documentation, demos, and metadata.
220 lines • 7.51 kB
JavaScript
import fs from 'fs';
import path from 'path';
import os from 'os';
import { logDebug, logError, logInfo } from './logger.js';
export class Cache {
cacheDir;
memoryCache = new Map();
defaultTTL; // Time to live in milliseconds
constructor(cacheDir, defaultTTL = 30 * 60 * 1000) {
this.cacheDir = cacheDir || path.join(os.homedir(), '.templui-mcp', 'cache');
this.defaultTTL = defaultTTL;
this.ensureCacheDir();
}
/**
* Set reduced TTL for dynamic data
*/
setReducedTTL(key) {
// Use shorter TTL for dynamic/live data that should be refreshed more frequently
if (key.includes('dynamic') || key.includes('list-components') || key.includes('repository-info')) {
return 15 * 60 * 1000; // 15 minutes for dynamic data
}
return this.defaultTTL; // 30 minutes for regular data
}
/**
* Get cached data
*/
async get(key) {
try {
// Check memory cache first
const memoryCached = this.memoryCache.get(key);
if (memoryCached && memoryCached.expiresAt > Date.now()) {
logDebug(`Cache hit (memory): ${key}`);
return memoryCached.data;
}
// Check disk cache
const filePath = this.getFilePath(key);
if (fs.existsSync(filePath)) {
const fileContent = fs.readFileSync(filePath, 'utf-8');
const cacheEntry = JSON.parse(fileContent);
if (cacheEntry.expiresAt > Date.now()) {
// Update memory cache
this.memoryCache.set(key, cacheEntry);
logDebug(`Cache hit (disk): ${key}`);
return cacheEntry.data;
}
else {
// Expired, remove file
fs.unlinkSync(filePath);
logDebug(`Cache expired, removed: ${key}`);
}
}
logDebug(`Cache miss: ${key}`);
return null;
}
catch (error) {
logError(`Cache get error for key ${key}`, error);
return null;
}
}
/**
* Set cached data
*/
async set(key, data, ttl) {
try {
// Use reduced TTL for dynamic data if no explicit TTL provided
const effectiveTTL = ttl || this.setReducedTTL(key);
const expiresAt = Date.now() + effectiveTTL;
const cacheEntry = {
data,
timestamp: Date.now(),
expiresAt
};
// Update memory cache
this.memoryCache.set(key, cacheEntry);
// Update disk cache
const filePath = this.getFilePath(key);
fs.writeFileSync(filePath, JSON.stringify(cacheEntry), 'utf-8');
const ttlMinutes = Math.round(effectiveTTL / 60000);
logDebug(`Cache set: ${key} (expires in ${ttlMinutes}min: ${new Date(expiresAt).toISOString()})`);
}
catch (error) {
logError(`Cache set error for key ${key}`, error);
}
}
/**
* Check if key exists and is not expired
*/
async has(key) {
const data = await this.get(key);
return data !== null;
}
/**
* Delete cached data
*/
async delete(key) {
try {
// Remove from memory cache
this.memoryCache.delete(key);
// Remove from disk cache
const filePath = this.getFilePath(key);
if (fs.existsSync(filePath)) {
fs.unlinkSync(filePath);
}
logDebug(`Cache deleted: ${key}`);
}
catch (error) {
logError(`Cache delete error for key ${key}`, error);
}
}
/**
* Clear all cached data
*/
async clear() {
try {
// Clear memory cache
this.memoryCache.clear();
// Clear disk cache
if (fs.existsSync(this.cacheDir)) {
const files = fs.readdirSync(this.cacheDir);
for (const file of files) {
fs.unlinkSync(path.join(this.cacheDir, file));
}
}
logInfo('Cache cleared');
}
catch (error) {
logError('Cache clear error', error);
}
}
/**
* Clean up expired entries
*/
async cleanup() {
try {
const now = Date.now();
// Clean memory cache
for (const [key, entry] of this.memoryCache.entries()) {
if (entry.expiresAt <= now) {
this.memoryCache.delete(key);
}
}
// Clean disk cache
if (fs.existsSync(this.cacheDir)) {
const files = fs.readdirSync(this.cacheDir);
let cleanedCount = 0;
for (const file of files) {
try {
const filePath = path.join(this.cacheDir, file);
const fileContent = fs.readFileSync(filePath, 'utf-8');
const cacheEntry = JSON.parse(fileContent);
if (cacheEntry.expiresAt <= now) {
fs.unlinkSync(filePath);
cleanedCount++;
}
}
catch (error) {
// Invalid cache file, remove it
fs.unlinkSync(path.join(this.cacheDir, file));
cleanedCount++;
}
}
if (cleanedCount > 0) {
logInfo(`Cache cleanup: removed ${cleanedCount} expired entries`);
}
}
}
catch (error) {
logError('Cache cleanup error', error);
}
}
/**
* Get cache statistics
*/
async getStats() {
try {
const memoryEntries = this.memoryCache.size;
let diskEntries = 0;
let totalSize = 0;
if (fs.existsSync(this.cacheDir)) {
const files = fs.readdirSync(this.cacheDir);
diskEntries = files.length;
for (const file of files) {
const filePath = path.join(this.cacheDir, file);
const stats = fs.statSync(filePath);
totalSize += stats.size;
}
}
return { memoryEntries, diskEntries, totalSize };
}
catch (error) {
logError('Cache stats error', error);
return { memoryEntries: 0, diskEntries: 0, totalSize: 0 };
}
}
/**
* Ensure cache directory exists
*/
ensureCacheDir() {
try {
if (!fs.existsSync(this.cacheDir)) {
fs.mkdirSync(this.cacheDir, { recursive: true });
logInfo(`Created cache directory: ${this.cacheDir}`);
}
}
catch (error) {
logError('Failed to create cache directory', error);
}
}
/**
* Get file path for cache key
*/
getFilePath(key) {
// Sanitize key for filename
const sanitizedKey = key.replace(/[^a-zA-Z0-9-_]/g, '_');
return path.join(this.cacheDir, `${sanitizedKey}.json`);
}
}
// Export singleton instance
export const cache = new Cache();
//# sourceMappingURL=cache.js.map