@houmak/minerva-mcp-server
Version:
Minerva Model Context Protocol (MCP) Server for Microsoft 365 and Azure integrations
256 lines (255 loc) • 7.99 kB
JavaScript
import { createClient } from 'redis';
import { logger } from '../logger.js';
export class RedisCache {
client;
config;
isConnected = false;
constructor(config) {
this.config = {
ttl: 3600, // 1 hour default
maxRetries: 3,
retryDelay: 1000,
...config,
};
this.client = createClient({
socket: {
host: this.config.host,
port: this.config.port,
reconnectStrategy: (retries) => {
if (retries > this.config.maxRetries) {
logger.error('Redis connection failed after max retries');
return false;
}
return this.config.retryDelay;
},
},
password: this.config.password,
database: this.config.db || 0,
});
this.setupEventHandlers();
}
setupEventHandlers() {
this.client.on('connect', () => {
logger.info('Redis client connected');
this.isConnected = true;
});
this.client.on('ready', () => {
logger.info('Redis client ready');
});
this.client.on('error', (error) => {
logger.error('Redis client error:', error);
this.isConnected = false;
});
this.client.on('end', () => {
logger.info('Redis client disconnected');
this.isConnected = false;
});
this.client.on('reconnecting', () => {
logger.info('Redis client reconnecting...');
});
}
async connect() {
try {
await this.client.connect();
}
catch (error) {
logger.error('Failed to connect to Redis:', error);
throw error;
}
}
async disconnect() {
try {
await this.client.disconnect();
}
catch (error) {
logger.error('Failed to disconnect from Redis:', error);
}
}
async get(key) {
if (!this.isConnected) {
logger.warn('Redis not connected, skipping cache get');
return null;
}
try {
const value = await this.client.get(key);
if (!value) {
return null;
}
const entry = JSON.parse(value);
// Check if entry is expired
if (Date.now() - entry.timestamp > entry.ttl * 1000) {
await this.delete(key);
return null;
}
logger.debug(`Cache hit for key: ${key}`);
return entry.data;
}
catch (error) {
logger.error(`Error getting cache key ${key}:`, error);
return null;
}
}
async set(key, data, ttl) {
if (!this.isConnected) {
logger.warn('Redis not connected, skipping cache set');
return;
}
try {
const entry = {
data,
timestamp: Date.now(),
ttl: ttl || this.config.ttl,
};
await this.client.setEx(key, entry.ttl, JSON.stringify(entry));
logger.debug(`Cache set for key: ${key} with TTL: ${entry.ttl}s`);
}
catch (error) {
logger.error(`Error setting cache key ${key}:`, error);
}
}
async delete(key) {
if (!this.isConnected) {
return;
}
try {
await this.client.del(key);
logger.debug(`Cache deleted for key: ${key}`);
}
catch (error) {
logger.error(`Error deleting cache key ${key}:`, error);
}
}
async clear() {
if (!this.isConnected) {
return;
}
try {
await this.client.flushDb();
logger.info('Cache cleared');
}
catch (error) {
logger.error('Error clearing cache:', error);
}
}
async exists(key) {
if (!this.isConnected) {
return false;
}
try {
const result = await this.client.exists(key);
return result === 1;
}
catch (error) {
logger.error(`Error checking cache key ${key}:`, error);
return false;
}
}
async getKeys(pattern) {
if (!this.isConnected) {
return [];
}
try {
return await this.client.keys(pattern);
}
catch (error) {
logger.error(`Error getting cache keys with pattern ${pattern}:`, error);
return [];
}
}
async getStats() {
if (!this.isConnected) {
return {
connected: false,
keys: 0,
memory: {},
info: {},
};
}
try {
const [keys, memory, info] = await Promise.all([
this.client.dbSize(),
this.client.memoryUsage(''),
this.client.info(),
]);
return {
connected: true,
keys,
memory,
info: this.parseRedisInfo(info),
};
}
catch (error) {
logger.error('Error getting Redis stats:', error);
return {
connected: false,
keys: 0,
memory: {},
info: {},
};
}
}
parseRedisInfo(info) {
const lines = info.split('\r\n');
const result = {};
for (const line of lines) {
if (line.includes(':')) {
const [key, value] = line.split(':');
result[key] = value;
}
}
return result;
}
isHealthy() {
return this.isConnected;
}
// Cache-specific methods for Minerva
async cacheGraphResponse(endpoint, params, data, ttl) {
const key = this.generateGraphCacheKey(endpoint, params);
await this.set(key, data, ttl);
}
async getCachedGraphResponse(endpoint, params) {
const key = this.generateGraphCacheKey(endpoint, params);
return await this.get(key);
}
async cachePnPResponse(command, params, data, ttl) {
const key = this.generatePnPCacheKey(command, params);
await this.set(key, data, ttl);
}
async getCachedPnPResponse(command, params) {
const key = this.generatePnPCacheKey(command, params);
return await this.get(key);
}
generateGraphCacheKey(endpoint, params) {
const paramsHash = JSON.stringify(params);
return `graph:${endpoint}:${Buffer.from(paramsHash).toString('base64')}`;
}
generatePnPCacheKey(command, params) {
const paramsHash = JSON.stringify(params);
return `pnp:${command}:${Buffer.from(paramsHash).toString('base64')}`;
}
// Cache invalidation patterns
async invalidateUserCache(userId) {
const pattern = userId ? `graph:*/users/${userId}*` : 'graph:*/users/*';
const keys = await this.getKeys(pattern);
for (const key of keys) {
await this.delete(key);
}
logger.info(`Invalidated ${keys.length} user cache entries`);
}
async invalidateSiteCache(siteId) {
const pattern = siteId ? `graph:*/sites/${siteId}*` : 'graph:*/sites/*';
const keys = await this.getKeys(pattern);
for (const key of keys) {
await this.delete(key);
}
logger.info(`Invalidated ${keys.length} site cache entries`);
}
async invalidateGroupCache(groupId) {
const pattern = groupId ? `graph:*/groups/${groupId}*` : 'graph:*/groups/*';
const keys = await this.getKeys(pattern);
for (const key of keys) {
await this.delete(key);
}
logger.info(`Invalidated ${keys.length} group cache entries`);
}
}