feat: enhance Jira analytics with caching and force refresh
- Updated `getJiraAnalytics` to accept a `forceRefresh` parameter for optional cache bypass. - Modified `getProjectAnalytics` to check the cache and return cached data unless forced to refresh. - Adjusted `loadAnalytics` in `useJiraAnalytics` to trigger a forced refresh on manual updates. - Improved UI in `JiraDashboardPageClient` to indicate when data is served from cache.
This commit is contained in:
155
services/jira-analytics-cache.ts
Normal file
155
services/jira-analytics-cache.ts
Normal file
@@ -0,0 +1,155 @@
|
||||
/**
|
||||
* Service de cache pour les analytics Jira
|
||||
* Cache en mémoire avec invalidation manuelle
|
||||
*/
|
||||
|
||||
import { JiraAnalytics } from '@/lib/types';
|
||||
|
||||
interface CacheEntry {
|
||||
data: JiraAnalytics;
|
||||
timestamp: number;
|
||||
projectKey: string;
|
||||
configHash: string; // Hash de la config Jira pour détecter les changements
|
||||
}
|
||||
|
||||
class JiraAnalyticsCacheService {
|
||||
private cache = new Map<string, CacheEntry>();
|
||||
private readonly CACHE_KEY_PREFIX = 'jira-analytics:';
|
||||
|
||||
/**
|
||||
* 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 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}`;
|
||||
// Simple hash (pour production, utiliser crypto.createHash)
|
||||
let hash = 0;
|
||||
for (let i = 0; i < configString.length; i++) {
|
||||
const char = configString.charCodeAt(i);
|
||||
hash = ((hash << 5) - hash) + char;
|
||||
hash = hash & hash; // Convert to 32-bit integer
|
||||
}
|
||||
return hash.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère les analytics depuis le cache si disponible
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
console.log(`✅ Cache HIT pour projet ${config.projectKey} (${this.getAgeDescription(entry.timestamp)})`);
|
||||
return entry.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stocke les analytics dans le cache
|
||||
*/
|
||||
set(config: { baseUrl: string; email: string; apiToken: string; projectKey: string }, data: JiraAnalytics): void {
|
||||
const configHash = this.generateConfigHash(config);
|
||||
const cacheKey = this.getCacheKey(config.projectKey, configHash);
|
||||
|
||||
const entry: CacheEntry = {
|
||||
data,
|
||||
timestamp: Date.now(),
|
||||
projectKey: config.projectKey,
|
||||
configHash
|
||||
};
|
||||
|
||||
this.cache.set(cacheKey, entry);
|
||||
console.log(`💾 Analytics mises en cache pour projet ${config.projectKey}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
getStats(): {
|
||||
totalEntries: number;
|
||||
projects: Array<{ projectKey: string; age: string; size: number }>;
|
||||
} {
|
||||
const projects = Array.from(this.cache.entries()).map(([key, entry]) => ({
|
||||
projectKey: entry.projectKey,
|
||||
age: this.getAgeDescription(entry.timestamp),
|
||||
size: JSON.stringify(entry.data).length
|
||||
}));
|
||||
|
||||
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';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
|
||||
// Instance singleton
|
||||
export const jiraAnalyticsCache = new JiraAnalyticsCacheService();
|
||||
Reference in New Issue
Block a user