import 'package:shelf/shelf.dart'; /// Réponse pour une image indisponible. /// /// Statut 410 et non 404 : la `Cascade` du serveur retombe sur le handler /// suivant pour tout 404 et renvoie finalement celui du handler statique, sans /// notre `cache-control`. Un 410 s'arrête ici. /// /// Un statut d'erreur plutôt qu'une image vide : le client affiche alors son /// propre repli (icône, initiale) au lieu d'une tuile blanche. Le `max-age` /// est essentiel : sans lui le navigateur redemande le logo à chaque /// affichage de la grille et le flot de requêtes mortes reprend aussitôt. Response missingImageResponse() => Response( 410, headers: { 'cache-control': 'public, max-age=600', 'access-control-allow-origin': '*', }, ); /// Mémoire courte des hôtes d'images injoignables. /// /// POURQUOI : les URL de picons pointent chez l'hébergeur du revendeur, pas /// sur le panneau. Quand cette machine tombe (maintenance, DNS, quota), elle /// répond 5xx en quelques millisecondes, le navigateur réessaie à chaque /// rendu de la grille et le proxy relaie des centaines de requêtes mortes par /// seconde. Le serveur tournant sur un unique isolate Dart, ce flot passe /// devant les paquets vidéo dans la boucle d'événements : l'image saccade /// pendant qu'on s'acharne sur un hôte qu'on sait hors service. /// /// On coupe donc court : au bout de [threshold] échecs consécutifs, l'hôte /// est considéré mort pendant [ttl] et les requêtes suivantes sont servies /// localement, sans appel sortant. class AssetFailureCache { AssetFailureCache({ this.threshold = 3, this.ttl = const Duration(minutes: 5), DateTime Function()? clock, }) : _clock = clock ?? DateTime.now; /// Nombre d'échecs consécutifs avant de couper un hôte. Un 5xx passager ne /// doit pas priver l'utilisateur de ses logos. final int threshold; /// Durée de la coupure. Assez courte pour qu'un hôte réparé revienne seul. final Duration ttl; final DateTime Function() _clock; final Map _failures = {}; final Map _downUntil = {}; /// L'hôte est-il réputé hors service en ce moment ? bool isDown(String host) { final key = host.toLowerCase(); final until = _downUntil[key]; if (until == null) return false; if (_clock().isBefore(until)) return true; // Coupure expirée : on repart d'une ardoise vierge pour laisser une // vraie chance au prochain appel. _downUntil.remove(key); _failures.remove(key); return false; } void recordFailure(String host) { final key = host.toLowerCase(); final count = (_failures[key] ?? 0) + 1; _failures[key] = count; if (count >= threshold) { _downUntil[key] = _clock().add(ttl); } } void recordSuccess(String host) { final key = host.toLowerCase(); _failures.remove(key); _downUntil.remove(key); } }