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;
}
}