- Introduced `cache-monitor.ts` for real-time cache monitoring, providing stats and actions for managing Jira analytics cache. - Updated `package.json` with new cache-related scripts for easy access. - Enhanced `JiraAnalyticsCacheService` to support TTL for cache entries, automatic cleanup of expired entries, and improved logging for cache operations. - Added methods for calculating time until expiry and formatting TTL for better visibility.
268 lines
8.0 KiB
TypeScript
268 lines
8.0 KiB
TypeScript
/**
|
||
* Service de cache pour les analytics Jira
|
||
* Cache en mémoire avec invalidation manuelle et TTL
|
||
*/
|
||
|
||
import { JiraAnalytics } from '@/lib/types';
|
||
import { createHash } from 'crypto';
|
||
|
||
interface CacheEntry {
|
||
data: JiraAnalytics;
|
||
timestamp: number;
|
||
projectKey: string;
|
||
configHash: string; // Hash de la config Jira pour détecter les changements
|
||
ttl: number; // Time To Live en millisecondes
|
||
}
|
||
|
||
class JiraAnalyticsCacheService {
|
||
private cache = new Map<string, CacheEntry>();
|
||
private readonly CACHE_KEY_PREFIX = 'jira-analytics:';
|
||
private readonly DEFAULT_TTL = 30 * 60 * 1000; // 30 minutes par défaut
|
||
private cleanupInterval: NodeJS.Timeout | null = null;
|
||
|
||
/**
|
||
* Génère une clé de cache basée sur la config Jira
|
||
*/
|
||
private getCacheKey(projectKey: string, configHash: string): string {
|
||
return `${this.CACHE_KEY_PREFIX}${projectKey}:${configHash}`;
|
||
}
|
||
|
||
/**
|
||
* Génère un hash sécurisé de la configuration Jira pour détecter les changements
|
||
*/
|
||
private generateConfigHash(config: { baseUrl: string; email: string; apiToken: string; projectKey: string }): string {
|
||
const configString = `${config.baseUrl}|${config.email}|${config.apiToken}|${config.projectKey}`;
|
||
return createHash('sha256').update(configString).digest('hex').substring(0, 16);
|
||
}
|
||
|
||
/**
|
||
* Récupère les analytics depuis le cache si disponible et non expirées
|
||
*/
|
||
get(config: { baseUrl: string; email: string; apiToken: string; projectKey: string }): JiraAnalytics | null {
|
||
const configHash = this.generateConfigHash(config);
|
||
const cacheKey = this.getCacheKey(config.projectKey, configHash);
|
||
|
||
const entry = this.cache.get(cacheKey);
|
||
|
||
if (!entry) {
|
||
console.log(`📋 Cache MISS pour projet ${config.projectKey}`);
|
||
return null;
|
||
}
|
||
|
||
// Vérifier que la config n'a pas changé
|
||
if (entry.configHash !== configHash) {
|
||
console.log(`🔄 Config changée pour projet ${config.projectKey}, invalidation du cache`);
|
||
this.cache.delete(cacheKey);
|
||
return null;
|
||
}
|
||
|
||
// Vérifier si l'entrée a expiré
|
||
const now = Date.now();
|
||
const isExpired = (now - entry.timestamp) > entry.ttl;
|
||
|
||
if (isExpired) {
|
||
console.log(`⏰ Cache EXPIRÉ pour projet ${config.projectKey} (${this.getAgeDescription(entry.timestamp)})`);
|
||
this.cache.delete(cacheKey);
|
||
return null;
|
||
}
|
||
|
||
console.log(`✅ Cache HIT pour projet ${config.projectKey} (${this.getAgeDescription(entry.timestamp)}, expire dans ${this.getTimeUntilExpiry(entry.timestamp, entry.ttl)})`);
|
||
return entry.data;
|
||
}
|
||
|
||
/**
|
||
* Stocke les analytics dans le cache avec TTL personnalisé
|
||
*/
|
||
set(config: { baseUrl: string; email: string; apiToken: string; projectKey: string }, data: JiraAnalytics, ttl?: number): void {
|
||
const configHash = this.generateConfigHash(config);
|
||
const cacheKey = this.getCacheKey(config.projectKey, configHash);
|
||
|
||
const entry: CacheEntry = {
|
||
data,
|
||
timestamp: Date.now(),
|
||
projectKey: config.projectKey,
|
||
configHash,
|
||
ttl: ttl || this.DEFAULT_TTL
|
||
};
|
||
|
||
this.cache.set(cacheKey, entry);
|
||
console.log(`💾 Analytics mises en cache pour projet ${config.projectKey} (TTL: ${this.formatTTL(entry.ttl)})`);
|
||
|
||
// Démarrer le nettoyage automatique si pas déjà fait
|
||
this.startCleanupInterval();
|
||
}
|
||
|
||
/**
|
||
* Invalide le cache pour un projet spécifique
|
||
*/
|
||
invalidate(config: { baseUrl: string; email: string; apiToken: string; projectKey: string }): void {
|
||
const configHash = this.generateConfigHash(config);
|
||
const cacheKey = this.getCacheKey(config.projectKey, configHash);
|
||
|
||
const deleted = this.cache.delete(cacheKey);
|
||
if (deleted) {
|
||
console.log(`🗑️ Cache invalidé pour projet ${config.projectKey}`);
|
||
} else {
|
||
console.log(`ℹ️ Aucun cache à invalider pour projet ${config.projectKey}`);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Invalide tout le cache
|
||
*/
|
||
invalidateAll(): void {
|
||
const size = this.cache.size;
|
||
this.cache.clear();
|
||
console.log(`🗑️ Tout le cache analytics invalidé (${size} entrées supprimées)`);
|
||
}
|
||
|
||
/**
|
||
* Retourne les statistiques du cache avec informations TTL
|
||
*/
|
||
getStats(): {
|
||
totalEntries: number;
|
||
projects: Array<{
|
||
projectKey: string;
|
||
age: string;
|
||
size: number;
|
||
ttl: string;
|
||
expiresIn: string;
|
||
isExpired: boolean;
|
||
}>;
|
||
} {
|
||
const now = Date.now();
|
||
const projects = Array.from(this.cache.entries()).map(([, entry]) => ({
|
||
projectKey: entry.projectKey,
|
||
age: this.getAgeDescription(entry.timestamp),
|
||
size: JSON.stringify(entry.data).length,
|
||
ttl: this.formatTTL(entry.ttl),
|
||
expiresIn: this.getTimeUntilExpiry(entry.timestamp, entry.ttl),
|
||
isExpired: (now - entry.timestamp) > entry.ttl
|
||
}));
|
||
|
||
return {
|
||
totalEntries: this.cache.size,
|
||
projects
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Formate l'âge d'une entrée de cache
|
||
*/
|
||
private getAgeDescription(timestamp: number): string {
|
||
const ageMs = Date.now() - timestamp;
|
||
const ageMinutes = Math.floor(ageMs / (1000 * 60));
|
||
const ageHours = Math.floor(ageMinutes / 60);
|
||
|
||
if (ageHours > 0) {
|
||
return `il y a ${ageHours}h${ageMinutes % 60}m`;
|
||
} else if (ageMinutes > 0) {
|
||
return `il y a ${ageMinutes}m`;
|
||
} else {
|
||
return 'maintenant';
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Calcule le temps restant avant expiration
|
||
*/
|
||
private getTimeUntilExpiry(timestamp: number, ttl: number): string {
|
||
const now = Date.now();
|
||
const remainingMs = ttl - (now - timestamp);
|
||
|
||
if (remainingMs <= 0) return 'expiré';
|
||
|
||
const remainingMinutes = Math.floor(remainingMs / (1000 * 60));
|
||
const remainingHours = Math.floor(remainingMinutes / 60);
|
||
|
||
if (remainingHours > 0) {
|
||
return `${remainingHours}h${remainingMinutes % 60}m`;
|
||
} else {
|
||
return `${remainingMinutes}m`;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Formate un TTL en texte lisible
|
||
*/
|
||
private formatTTL(ttl: number): string {
|
||
const minutes = Math.floor(ttl / (1000 * 60));
|
||
const hours = Math.floor(minutes / 60);
|
||
|
||
if (hours > 0) {
|
||
return `${hours}h${minutes % 60}m`;
|
||
} else {
|
||
return `${minutes}m`;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Démarre l'intervalle de nettoyage automatique
|
||
*/
|
||
private startCleanupInterval(): void {
|
||
if (this.cleanupInterval) return; // Déjà démarré
|
||
|
||
// Nettoyer toutes les 5 minutes
|
||
this.cleanupInterval = setInterval(() => {
|
||
this.cleanupExpiredEntries();
|
||
}, 5 * 60 * 1000);
|
||
|
||
console.log('🧹 Nettoyage automatique du cache démarré (toutes les 5 minutes)');
|
||
}
|
||
|
||
/**
|
||
* Nettoie les entrées expirées du cache
|
||
*/
|
||
private cleanupExpiredEntries(): void {
|
||
const now = Date.now();
|
||
let cleanedCount = 0;
|
||
|
||
for (const [key, entry] of this.cache.entries()) {
|
||
if ((now - entry.timestamp) > entry.ttl) {
|
||
this.cache.delete(key);
|
||
cleanedCount++;
|
||
}
|
||
}
|
||
|
||
if (cleanedCount > 0) {
|
||
console.log(`🧹 Nettoyage automatique: ${cleanedCount} entrées expirées supprimées`);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Vérifie si une entrée existe pour un projet
|
||
*/
|
||
has(config: { baseUrl: string; email: string; apiToken: string; projectKey: string }): boolean {
|
||
const configHash = this.generateConfigHash(config);
|
||
const cacheKey = this.getCacheKey(config.projectKey, configHash);
|
||
return this.cache.has(cacheKey);
|
||
}
|
||
|
||
/**
|
||
* Arrête le nettoyage automatique
|
||
*/
|
||
stopCleanupInterval(): void {
|
||
if (this.cleanupInterval) {
|
||
clearInterval(this.cleanupInterval);
|
||
this.cleanupInterval = null;
|
||
console.log('🛑 Nettoyage automatique du cache arrêté');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Force un nettoyage immédiat des entrées expirées
|
||
*/
|
||
forceCleanup(): number {
|
||
const beforeCount = this.cache.size;
|
||
this.cleanupExpiredEntries();
|
||
const afterCount = this.cache.size;
|
||
const cleanedCount = beforeCount - afterCount;
|
||
|
||
console.log(`🧹 Nettoyage forcé: ${cleanedCount} entrées supprimées`);
|
||
return cleanedCount;
|
||
}
|
||
}
|
||
|
||
// Instance singleton
|
||
export const jiraAnalyticsCache = new JiraAnalyticsCacheService();
|