using Microsoft.JSInterop; namespace MaBibli.Client.Services; /// /// État du réseau, tel que l'interface doit le montrer, et date du dernier rafraîchissement /// des données mises en cache. /// /// /// Deux sources, pas une. navigator.onLine ne vaut que par sa négation : « faux » /// est fiable, « vrai » ne prouve rien (portail captif, serveur arrêté, Wi-Fi sans Internet). /// L'état affiché combine donc l'événement du navigateur et le sort réel des appels HTTP — /// un appel qui échoue bascule l'application en « hors ligne » sans attendre l'accord du /// navigateur, et le premier appel qui repasse la rebascule en ligne. /// /// Ce service est le seul point d'observation de l'interface : la mise en page s'y abonne pour /// afficher le bandeau et la date de synchronisation, les écrans pour désactiver les actions /// d'écriture. /// /// public sealed class EtatReseau(CacheHorsLigne cache) : IAsyncDisposable { private DotNetObjectReference? _reference; private bool _navigateurEnLigne = true; private bool _dernierAppelEchoue; /// Faux dès qu'un appel a échoué ou que le navigateur signale la perte du réseau. public bool EnLigne => _navigateurEnLigne && !_dernierAppelEchoue; /// /// Faut-il tenter un appel réseau ? Oui dès que le navigateur a une connexion, même /// si le dernier appel a échoué. /// /// /// C'est ce qui permet de sortir tout seul du mode hors-ligne quand la panne venait du /// serveur et non de l'appareil : navigator.onLine n'a alors jamais changé, donc /// aucun événement online ne viendra jamais. Sans cette distinction, l'application /// resterait bloquée sur le cache jusqu'au rechargement de la page — constaté en essai. /// Le prix est d'une requête qui échoue par lecture tant que le serveur est absent : elle /// échoue vite, et l'affichage retombe sur le cache comme avant. /// public bool TenterLeReseau => _navigateurEnLigne; /// /// Instant du dernier rafraîchissement réussi des données en cache, null si l'on n'a /// jamais rien pu enregistrer sur cet appareil. /// public DateTimeOffset? DerniereSynchro { get; private set; } /// Vrai quand ce qui est affiché vient du cache et non du réseau. public bool DonneesDuCache => !EnLigne && DerniereSynchro is not null; /// Raison affichable du blocage des actions d'écriture. public const string MotifHorsLigne = "Hors ligne : la bibliothèque reste consultable, mais rien ne peut être modifié."; /// /// Bascule en ligne ↔ hors ligne, et elle seule. /// /// /// La date de synchronisation a son propre événement () : sans /// cette séparation, un écran qui rechargerait ses données sur Change déclencherait /// l'écriture d'un instantané, donc un nouvel événement, donc un rechargement — une boucle /// sans fin. /// public event Action? Change; /// Nouvelle date de synchronisation : de quoi rafraîchir un affichage, rien de plus. public event Action? SynchroChange; /// À appeler une fois au démarrage : lit l'état courant et s'abonne aux bascules. public async Task DemarrerAsync() { _reference ??= DotNetObjectReference.Create(this); var enLigne = await cache.SurveillerAsync(_reference); Definir(enLigne, false); } [JSInvokable] public void SurChangementReseau(bool enLigne) => Definir(enLigne, _dernierAppelEchoue && !enLigne); /// Un appel HTTP a échoué : on est hors ligne, quoi qu'en dise le navigateur. public void SignalerEchecReseau() => Definir(_navigateurEnLigne, true); /// Un appel HTTP a abouti : la preuve la plus solide qu'on soit en ligne. public void SignalerSuccesReseau() => Definir(true, false); /// Enregistre qu'un instantané vient d'être rangé, avec sa date. public void SignalerSynchro(DateTimeOffset date) { if (DerniereSynchro is { } precedente && precedente >= date) { return; } DerniereSynchro = date; SynchroChange?.Invoke(); } private void Definir(bool navigateurEnLigne, bool dernierAppelEchoue) { var avant = EnLigne; _navigateurEnLigne = navigateurEnLigne; _dernierAppelEchoue = dernierAppelEchoue; // L'événement ne parle que de l'état VISIBLE : les écrans s'y abonnent pour se recharger, // et le déclencher pour un changement interne les ferait boucler. if (avant != EnLigne) { Change?.Invoke(); } } public ValueTask DisposeAsync() { _reference?.Dispose(); _reference = null; return ValueTask.CompletedTask; } }