Files
mabibli/MaBibli.Client/Services/EtatReseau.cs
T
Mathieu LimonierandClaude Opus 5 6a6d745af4 MaBibli 1.0.0
Gestion de bibliothèque personnelle auto-hébergée : catalogue, prêts,
scan de code-barres, consultation hors-ligne.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 22:36:16 +02:00

123 lines
5.1 KiB
C#

using Microsoft.JSInterop;
namespace MaBibli.Client.Services;
/// <summary>
/// État du réseau, tel que l'interface doit le montrer, et date du dernier rafraîchissement
/// des données mises en cache.
/// </summary>
/// <remarks>
/// <b>Deux sources, pas une.</b> <c>navigator.onLine</c> 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.
/// <para>
/// 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.
/// </para>
/// </remarks>
public sealed class EtatReseau(CacheHorsLigne cache) : IAsyncDisposable
{
private DotNetObjectReference<EtatReseau>? _reference;
private bool _navigateurEnLigne = true;
private bool _dernierAppelEchoue;
/// <summary>Faux dès qu'un appel a échoué ou que le navigateur signale la perte du réseau.</summary>
public bool EnLigne => _navigateurEnLigne && !_dernierAppelEchoue;
/// <summary>
/// Faut-il tenter un appel réseau ? <b>Oui dès que le navigateur a une connexion</b>, même
/// si le dernier appel a échoué.
/// </summary>
/// <remarks>
/// C'est ce qui permet de sortir tout seul du mode hors-ligne quand la panne venait du
/// <b>serveur</b> et non de l'appareil : <c>navigator.onLine</c> n'a alors jamais changé, donc
/// aucun événement <c>online</c> 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.
/// </remarks>
public bool TenterLeReseau => _navigateurEnLigne;
/// <summary>
/// Instant du dernier rafraîchissement réussi des données en cache, <c>null</c> si l'on n'a
/// jamais rien pu enregistrer sur cet appareil.
/// </summary>
public DateTimeOffset? DerniereSynchro { get; private set; }
/// <summary>Vrai quand ce qui est affiché vient du cache et non du réseau.</summary>
public bool DonneesDuCache => !EnLigne && DerniereSynchro is not null;
/// <summary>Raison affichable du blocage des actions d'écriture.</summary>
public const string MotifHorsLigne =
"Hors ligne : la bibliothèque reste consultable, mais rien ne peut être modifié.";
/// <summary>
/// Bascule en ligne ↔ hors ligne, et <b>elle seule</b>.
/// </summary>
/// <remarks>
/// La date de synchronisation a son propre événement (<see cref="SynchroChange"/>) : sans
/// cette séparation, un écran qui rechargerait ses données sur <c>Change</c> déclencherait
/// l'écriture d'un instantané, donc un nouvel événement, donc un rechargement — une boucle
/// sans fin.
/// </remarks>
public event Action? Change;
/// <summary>Nouvelle date de synchronisation : de quoi rafraîchir un affichage, rien de plus.</summary>
public event Action? SynchroChange;
/// <summary>À appeler une fois au démarrage : lit l'état courant et s'abonne aux bascules.</summary>
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);
/// <summary>Un appel HTTP a échoué : on est hors ligne, quoi qu'en dise le navigateur.</summary>
public void SignalerEchecReseau() => Definir(_navigateurEnLigne, true);
/// <summary>Un appel HTTP a abouti : la preuve la plus solide qu'on soit en ligne.</summary>
public void SignalerSuccesReseau() => Definir(true, false);
/// <summary>Enregistre qu'un instantané vient d'être rangé, avec sa date.</summary>
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;
}
}