@twofeetup/clickup-mcp
Version:
Optimized ClickUp MCP Server - High-performance AI integration with consolidated tools and response optimization
268 lines (267 loc) • 7.71 kB
JavaScript
/**
* SPDX-FileCopyrightText: © 2025 Sjoerd Tiemensma
* SPDX-License-Identifier: MIT
*
* Cache Service
*
* Provides caching for frequently accessed data to reduce API calls
* Implements TTL-based expiration and cache invalidation
*/
import { Logger } from '../logger.js';
const logger = new Logger('CacheService');
/**
* Generic cache service with TTL support
*/
export class CacheService {
constructor(defaultTTL = 5 * 60 * 1000) {
this.defaultTTL = defaultTTL;
this.cache = new Map();
this.hits = 0;
this.misses = 0;
logger.info('CacheService initialized', { defaultTTL });
// Periodically clean up expired entries
setInterval(() => this.cleanup(), 60 * 1000); // Every minute
}
/**
* Get value from cache
*/
get(key) {
const entry = this.cache.get(key);
if (!entry) {
this.misses++;
logger.debug('Cache miss', { key });
return null;
}
// Check if expired
if (Date.now() > entry.expiresAt) {
this.cache.delete(key);
this.misses++;
logger.debug('Cache expired', { key });
return null;
}
this.hits++;
logger.debug('Cache hit', { key });
return entry.data;
}
/**
* Set value in cache with optional TTL
*/
set(key, value, ttl) {
const expiryTime = ttl || this.defaultTTL;
const now = Date.now();
this.cache.set(key, {
data: value,
expiresAt: now + expiryTime,
createdAt: now
});
logger.debug('Cache set', { key, ttl: expiryTime });
}
/**
* Delete value from cache
*/
delete(key) {
const deleted = this.cache.delete(key);
if (deleted) {
logger.debug('Cache deleted', { key });
}
return deleted;
}
/**
* Clear all cache entries
*/
clear() {
this.cache.clear();
this.hits = 0;
this.misses = 0;
logger.info('Cache cleared');
}
/**
* Clear cache entries matching a pattern
*/
clearPattern(pattern) {
const regex = typeof pattern === 'string' ? new RegExp(pattern) : pattern;
let count = 0;
for (const key of this.cache.keys()) {
if (regex.test(key)) {
this.cache.delete(key);
count++;
}
}
logger.info('Cache cleared by pattern', { pattern: pattern.toString(), count });
return count;
}
/**
* Remove expired entries
*/
cleanup() {
const now = Date.now();
let removed = 0;
for (const [key, entry] of this.cache.entries()) {
if (now > entry.expiresAt) {
this.cache.delete(key);
removed++;
}
}
if (removed > 0) {
logger.debug('Cache cleanup', { removed });
}
}
/**
* Get cache statistics
*/
getStats() {
const total = this.hits + this.misses;
return {
hits: this.hits,
misses: this.misses,
size: this.cache.size,
hitRate: total > 0 ? this.hits / total : 0
};
}
/**
* Get or set a value (fetch if not cached)
*/
async getOrSet(key, fetchFn, ttl) {
// Try to get from cache
const cached = this.get(key);
if (cached !== null) {
return cached;
}
// Fetch and cache
const value = await fetchFn();
this.set(key, value, ttl);
return value;
}
}
/**
* Workspace-specific cache with predefined TTLs
*/
export class WorkspaceCache {
constructor() {
this.cache = new CacheService(WorkspaceCache.TTLs.HIERARCHY);
logger.info('WorkspaceCache initialized');
}
/**
* Get workspace hierarchy
*/
getHierarchy(teamId) {
return this.cache.get(`hierarchy:${teamId}`);
}
/**
* Set workspace hierarchy
*/
setHierarchy(teamId, data) {
this.cache.set(`hierarchy:${teamId}`, data, WorkspaceCache.TTLs.HIERARCHY);
}
/**
* Get workspace members
*/
getMembers(teamId) {
return this.cache.get(`members:${teamId}`);
}
/**
* Set workspace members
*/
setMembers(teamId, members) {
this.cache.set(`members:${teamId}`, members, WorkspaceCache.TTLs.MEMBERS);
}
/**
* Get space tags
*/
getTags(spaceId) {
return this.cache.get(`tags:${spaceId}`);
}
/**
* Set space tags
*/
setTags(spaceId, tags) {
this.cache.set(`tags:${spaceId}`, tags, WorkspaceCache.TTLs.TAGS);
}
/**
* Get custom fields for a list
*/
getCustomFields(listId) {
return this.cache.get(`custom_fields:${listId}`);
}
/**
* Set custom fields for a list
*/
setCustomFields(listId, fields) {
this.cache.set(`custom_fields:${listId}`, fields, WorkspaceCache.TTLs.CUSTOM_FIELDS);
}
/**
* Invalidate all caches for a workspace
*/
invalidateWorkspace(teamId) {
this.cache.clearPattern(`^(hierarchy|members|tags|custom_fields):${teamId}`);
logger.info('Workspace cache invalidated', { teamId });
}
/**
* Invalidate member cache when members change
*/
invalidateMembers(teamId) {
this.cache.delete(`members:${teamId}`);
logger.info('Members cache invalidated', { teamId });
}
/**
* Invalidate tags cache when tags change
*/
invalidateTags(spaceId) {
this.cache.delete(`tags:${spaceId}`);
logger.info('Tags cache invalidated', { spaceId });
}
/**
* Clear all caches
*/
clear() {
this.cache.clear();
}
/**
* Get cache statistics
*/
getStats() {
return this.cache.getStats();
}
}
// Cache TTLs (in milliseconds)
WorkspaceCache.TTLs = {
HIERARCHY: 5 * 60 * 1000, // 5 minutes
MEMBERS: 10 * 60 * 1000, // 10 minutes
TAGS: 15 * 60 * 1000, // 15 minutes
STATUSES: 30 * 60 * 1000, // 30 minutes
CUSTOM_FIELDS: 30 * 60 * 1000 // 30 minutes
};
// Export singleton instances
export const cacheService = new CacheService();
export const workspaceCache = new WorkspaceCache();
/**
* Invalidate all workspace-related caches after modifications
* Call this after creating, updating, or deleting tasks, lists, folders, etc.
*/
export function invalidateWorkspaceCaches() {
logger.info('Invalidating workspace caches after modification');
// Clear workspace hierarchy cache (will be refreshed on next request)
workspaceCache.clear();
// Clear general caches
cacheService.clearPattern(/^(container|hierarchy|members|tags|custom_fields):/);
logger.debug('Workspace caches invalidated');
}
/**
* Refresh workspace caches in the background after a modification
* This ensures the cache is pre-warmed for the next request without blocking the response
* @param workspaceService - The workspace service instance to use for refreshing
*/
export function refreshWorkspaceCachesInBackground(workspaceService) {
// Start background refresh without awaiting
(async () => {
try {
logger.info('Starting background cache refresh after modification');
await workspaceService.getWorkspaceHierarchy(true);
logger.info('Background cache refresh completed');
}
catch (error) {
logger.error('Background cache refresh failed', { error: error.message });
// Don't throw - this is a background operation
}
})();
}