Files
towercontrol/src/services/integrations/jira/analytics-cache.ts
Julien Froidefond bd7ede412e feat: add cache monitoring scripts and enhance JiraAnalyticsCache
- 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.
2025-09-26 11:42:18 +02:00

268 lines
8.0 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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();