import * as fs from 'fs/promises'; import * as path from 'path'; import { homedir } from 'os'; import { GitLabActivity, CalendarEvent, AzureDevOpsActivity } from '../types/index.js'; const CACHE_FILE = path.join(homedir(), '.activity-collector-mcp-cache.json'); const DEFAULT_CACHE_TTL = 3600000; // 1 hour in milliseconds interface CacheEntry { data: T; timestamp: number; source: string; } interface CacheData { gitlab: { [dateKey: string]: CacheEntry; }; googleCalendar: { [dateKey: string]: CacheEntry; }; outlookCalendar: { [dateKey: string]: CacheEntry; }; azureDevops: { [dateKey: string]: CacheEntry; }; } export class ActivityCache { private cache: CacheData = { gitlab: {}, googleCalendar: {}, outlookCalendar: {}, azureDevops: {}, }; private cacheTTL: number = DEFAULT_CACHE_TTL; private cacheStats = { gitlabHits: 0, gitlabMisses: 0, googleHits: 0, googleMisses: 0, outlookHits: 0, outlookMisses: 0, azureDevopsHits: 0, azureDevopsMisses: 0, }; async load(): Promise { try { const data = await fs.readFile(CACHE_FILE, 'utf-8'); const parsed = JSON.parse(data); // Cache files written before a bucket existed lack that key entirely, // so default every bucket rather than trusting the file's shape. this.cache = { gitlab: parsed.gitlab ?? {}, googleCalendar: parsed.googleCalendar ?? {}, outlookCalendar: parsed.outlookCalendar ?? {}, azureDevops: parsed.azureDevops ?? {}, }; } catch (error) { // Cache file doesn't exist yet, that's okay this.cache = { gitlab: {}, googleCalendar: {}, outlookCalendar: {}, azureDevops: {}, }; } } async save(): Promise { await fs.writeFile(CACHE_FILE, JSON.stringify(this.cache, null, 2), { mode: 0o600, }); } private getDateKey(date: Date): string { return date.toISOString().split('T')[0]; // YYYY-MM-DD } private isExpired(timestamp: number): boolean { return Date.now() - timestamp > this.cacheTTL; } // GitLab cache methods getGitLabActivity(date: Date): GitLabActivity | null { const key = this.getDateKey(date); const entry = this.cache.gitlab[key]; if (!entry) { this.cacheStats.gitlabMisses++; return null; } if (this.isExpired(entry.timestamp)) { delete this.cache.gitlab[key]; this.cacheStats.gitlabMisses++; return null; } this.cacheStats.gitlabHits++; return entry.data; } async setGitLabActivity(date: Date, activity: GitLabActivity): Promise { const key = this.getDateKey(date); this.cache.gitlab[key] = { data: activity, timestamp: Date.now(), source: 'gitlab', }; await this.save(); } // Google Calendar cache methods getGoogleCalendarEvents(date: Date): CalendarEvent[] | null { const key = this.getDateKey(date); const entry = this.cache.googleCalendar[key]; if (!entry) { this.cacheStats.googleMisses++; return null; } if (this.isExpired(entry.timestamp)) { delete this.cache.googleCalendar[key]; this.cacheStats.googleMisses++; return null; } this.cacheStats.googleHits++; return entry.data; } async setGoogleCalendarEvents(date: Date, events: CalendarEvent[]): Promise { const key = this.getDateKey(date); this.cache.googleCalendar[key] = { data: events, timestamp: Date.now(), source: 'google_calendar', }; await this.save(); } // Outlook Calendar cache methods getOutlookCalendarEvents(date: Date): CalendarEvent[] | null { const key = this.getDateKey(date); const entry = this.cache.outlookCalendar[key]; if (!entry) { this.cacheStats.outlookMisses++; return null; } if (this.isExpired(entry.timestamp)) { delete this.cache.outlookCalendar[key]; this.cacheStats.outlookMisses++; return null; } this.cacheStats.outlookHits++; return entry.data; } async setOutlookCalendarEvents(date: Date, events: CalendarEvent[]): Promise { const key = this.getDateKey(date); this.cache.outlookCalendar[key] = { data: events, timestamp: Date.now(), source: 'outlook_calendar', }; await this.save(); } // Azure DevOps cache methods getAzureDevOpsActivity(date: Date): AzureDevOpsActivity | null { const key = this.getDateKey(date); const entry = this.cache.azureDevops[key]; if (!entry) { this.cacheStats.azureDevopsMisses++; return null; } if (this.isExpired(entry.timestamp)) { delete this.cache.azureDevops[key]; this.cacheStats.azureDevopsMisses++; return null; } this.cacheStats.azureDevopsHits++; return entry.data; } async setAzureDevOpsActivity(date: Date, activity: AzureDevOpsActivity): Promise { const key = this.getDateKey(date); this.cache.azureDevops[key] = { data: activity, timestamp: Date.now(), source: 'azure_devops', }; await this.save(); } // Cache management async clearAll(): Promise { this.cache = { gitlab: {}, googleCalendar: {}, outlookCalendar: {}, azureDevops: {}, }; await this.save(); this.resetStats(); } async clearAzureDevOps(): Promise { this.cache.azureDevops = {}; await this.save(); } async clearGitLab(): Promise { this.cache.gitlab = {}; await this.save(); } async clearCalendars(): Promise { this.cache.googleCalendar = {}; this.cache.outlookCalendar = {}; await this.save(); } async clearExpired(): Promise { const now = Date.now(); // Clear expired GitLab entries for (const [key, entry] of Object.entries(this.cache.gitlab)) { if (now - entry.timestamp > this.cacheTTL) { delete this.cache.gitlab[key]; } } // Clear expired Google Calendar entries for (const [key, entry] of Object.entries(this.cache.googleCalendar)) { if (now - entry.timestamp > this.cacheTTL) { delete this.cache.googleCalendar[key]; } } // Clear expired Outlook Calendar entries for (const [key, entry] of Object.entries(this.cache.outlookCalendar)) { if (now - entry.timestamp > this.cacheTTL) { delete this.cache.outlookCalendar[key]; } } // Clear expired Azure DevOps entries for (const [key, entry] of Object.entries(this.cache.azureDevops)) { if (now - entry.timestamp > this.cacheTTL) { delete this.cache.azureDevops[key]; } } await this.save(); } getCacheStats() { const total = this.cacheStats.gitlabHits + this.cacheStats.gitlabMisses + this.cacheStats.googleHits + this.cacheStats.googleMisses + this.cacheStats.outlookHits + this.cacheStats.outlookMisses + this.cacheStats.azureDevopsHits + this.cacheStats.azureDevopsMisses; const hits = this.cacheStats.gitlabHits + this.cacheStats.googleHits + this.cacheStats.outlookHits + this.cacheStats.azureDevopsHits; const hitRate = total > 0 ? ((hits / total) * 100).toFixed(1) : '0'; return { ...this.cacheStats, totalHits: hits, totalRequests: total, hitRate: `${hitRate}%`, }; } resetStats() { this.cacheStats = { gitlabHits: 0, gitlabMisses: 0, googleHits: 0, googleMisses: 0, outlookHits: 0, outlookMisses: 0, azureDevopsHits: 0, azureDevopsMisses: 0, }; } getCacheInfo() { return { gitlabEntries: Object.keys(this.cache.gitlab).length, googleCalendarEntries: Object.keys(this.cache.googleCalendar).length, outlookCalendarEntries: Object.keys(this.cache.outlookCalendar).length, azureDevopsEntries: Object.keys(this.cache.azureDevops).length, cacheTTL: this.cacheTTL, cacheFile: CACHE_FILE, }; } setCacheTTL(ttl: number) { this.cacheTTL = ttl; } }