digital-samba-mcp-server
Version:
Digital Samba MCP Server - Model Context Protocol server for Digital Samba's video conferencing API
306 lines • 9.36 kB
JavaScript
/**
* Digital Samba MCP Server - Cache Module
*
* This module provides caching functionality for the Digital Samba MCP Server.
* It implements a flexible caching system for API responses, reducing load on
* the Digital Samba API and improving response times for clients.
*
* Features include:
* - Configurable TTL (Time-To-Live) for cached responses
* - Memory-based cache storage
* - Optional Redis-based storage for distributed deployments
* - Cache invalidation strategies
* - Support for conditional requests (ETag, If-Modified-Since)
*
* @module cache
* @author Digital Samba Team
* @version 0.1.0
*/
// Node.js built-in modules
import { randomBytes } from 'crypto';
// External dependencies
// None for now
// Local modules
import logger from './logger.js';
/**
* Default cache options
*/
export const defaultCacheOptions = {
ttl: 5 * 60 * 1000, // 5 minutes by default
maxItems: 1000,
useEtag: true,
keyGenerator: (namespace, id) => `${namespace}:${id}`,
serializer: (value) => JSON.stringify(value),
deserializer: (value) => JSON.parse(value)
};
/**
* Memory-based cache implementation
*/
export class MemoryCache {
/**
* Creates a new MemoryCache
* @param options Cache options
*/
constructor(options = {}) {
this.options = { ...defaultCacheOptions, ...options };
this.cache = new Map();
logger.info('Cache initialized', {
ttl: this.options.ttl,
maxItems: this.options.maxItems
});
// Start periodic cleanup
this.startCleanupInterval();
}
/**
* Generates an ETag for a value
* @param value Value to generate ETag for
* @returns ETag string
*/
generateEtag(value) {
const serialized = typeof value === 'string'
? value
: this.options.serializer(value);
// Simple ETag generation using a hash of the content
// In production, consider using a more robust algorithm
return randomBytes(8).toString('hex');
}
/**
* Starts the cleanup interval to remove expired items
*/
startCleanupInterval() {
const cleanupInterval = Math.min(this.options.ttl / 2, 60 * 1000); // At most once per minute
setInterval(() => {
this.cleanup();
}, cleanupInterval);
}
/**
* Cleans up expired cache entries
*/
cleanup() {
const now = Date.now();
let expiredCount = 0;
for (const [key, entry] of this.cache.entries()) {
if (entry.expires <= now) {
this.cache.delete(key);
expiredCount++;
}
}
if (expiredCount > 0) {
logger.debug('Cache cleanup completed', { expiredCount });
}
}
/**
* Sets a value in the cache
* @param namespace Cache namespace
* @param id Item identifier
* @param value Value to cache
* @param ttl Optional TTL override
* @returns The cached entry
*/
set(namespace, id, value, ttl) {
const key = this.options.keyGenerator(namespace, id);
const now = Date.now();
const expires = now + (ttl || this.options.ttl);
// Generate ETag if enabled
const etag = this.options.useEtag ? this.generateEtag(value) : undefined;
const entry = {
value,
expires,
etag,
lastModified: new Date()
};
// Check if we need to evict items
if (this.options.maxItems && this.cache.size >= this.options.maxItems && !this.cache.has(key)) {
this.evictOldest();
}
this.cache.set(key, entry);
logger.debug('Cache item set', { namespace, id, ttl: ttl || this.options.ttl });
return entry;
}
/**
* Gets a value from the cache
* @param namespace Cache namespace
* @param id Item identifier
* @returns The cached entry or undefined if not found or expired
*/
get(namespace, id) {
const key = this.options.keyGenerator(namespace, id);
const entry = this.cache.get(key);
if (!entry) {
logger.debug('Cache miss', { namespace, id });
return undefined;
}
// Check if expired
if (entry.expires <= Date.now()) {
logger.debug('Cache entry expired', { namespace, id });
this.cache.delete(key);
return undefined;
}
logger.debug('Cache hit', { namespace, id });
return entry;
}
/**
* Deletes a value from the cache
* @param namespace Cache namespace
* @param id Item identifier
* @returns Whether the item was deleted
*/
delete(namespace, id) {
const key = this.options.keyGenerator(namespace, id);
const deleted = this.cache.delete(key);
if (deleted) {
logger.debug('Cache item deleted', { namespace, id });
}
return deleted;
}
/**
* Invalidates a specific cache entry in a namespace
* @param namespace Cache namespace
* @param id Item identifier
* @returns Whether the item was invalidated
*/
invalidate(namespace, id) {
const key = this.options.keyGenerator(namespace, id);
const deleted = this.cache.delete(key);
if (deleted) {
logger.debug('Cache item invalidated', { namespace, id });
}
return deleted;
}
/**
* Invalidates all items in a namespace
* @param namespace Cache namespace
* @returns Number of items invalidated
*/
invalidateNamespace(namespace) {
let count = 0;
const prefix = `${namespace}:`;
for (const key of this.cache.keys()) {
if (key.startsWith(prefix)) {
this.cache.delete(key);
count++;
}
}
if (count > 0) {
logger.debug('Cache namespace invalidated', { namespace, count });
}
return count;
}
/**
* Checks if a value is fresh based on conditional headers
* @param namespace Cache namespace
* @param id Item identifier
* @param etag ETag to compare
* @param modifiedSince Last-Modified date to compare
* @returns Whether the cached value is considered fresh
*/
isFresh(namespace, id, etag, modifiedSince) {
const entry = this.get(namespace, id);
if (!entry) {
return false;
}
// Check ETag if provided
if (etag && entry.etag === etag) {
return true;
}
// Check Last-Modified if provided
if (modifiedSince && entry.lastModified && entry.lastModified <= modifiedSince) {
return true;
}
return false;
}
/**
* Evicts the oldest item from the cache
* @returns Whether an item was evicted
*/
evictOldest() {
if (this.cache.size === 0) {
return false;
}
let oldestKey = null;
let oldestTime = Infinity;
for (const [key, entry] of this.cache.entries()) {
if (entry.expires < oldestTime) {
oldestKey = key;
oldestTime = entry.expires;
}
}
if (oldestKey) {
this.cache.delete(oldestKey);
logger.debug('Cache item evicted', { key: oldestKey });
return true;
}
return false;
}
/**
* Clears the entire cache
*/
clear() {
const size = this.cache.size;
this.cache.clear();
logger.info('Cache cleared', { itemsCleared: size });
}
/**
* Gets cache statistics
* @returns Cache statistics
*/
getStats() {
const now = Date.now();
let validItems = 0;
let expiredItems = 0;
for (const entry of this.cache.values()) {
if (entry.expires > now) {
validItems++;
}
else {
expiredItems++;
}
}
return {
totalItems: this.cache.size,
validItems,
expiredItems,
maxItems: this.options.maxItems
};
}
}
/**
* Redis-backed cache implementation
* This implementation uses Redis for storage, making it suitable for distributed deployments.
* Note: This is a placeholder for the Redis implementation.
* The actual implementation would use a Redis client library.
*/
export class RedisCache {
constructor(options = {}) {
// Initialize with Redis client
logger.info('Redis cache initialized');
throw new Error('Redis cache not yet implemented');
}
}
/**
* Create a caching middleware for Express
* This is a placeholder function for a potential Express middleware
*/
export function createCacheMiddleware(options = {}) {
const cache = new MemoryCache(options);
// Middleware implementation would go here
logger.info('Cache middleware created');
return {
cache,
middleware: (namespace) => {
return (req, res, next) => {
// Middleware implementation
next();
};
}
};
}
/**
* Exports the default cache
*/
export default {
MemoryCache,
RedisCache,
createCacheMiddleware
};
//# sourceMappingURL=cache.js.map