using System.Text.Json; using Microsoft.JSInterop; namespace MaBibli.Client.Services; /// Clés des instantanés rangés dans IndexedDB. /// /// Un instantané par vue de l'API, et non par requête : c'est ce qui permet de chercher et de /// trier hors-ligne sur tout le fonds, y compris des recherches jamais tapées en ligne. /// public static class ClesCache { /// Le catalogue entier, sans aucun filtre. La recherche hors-ligne s'y applique. public const string Catalogue = "catalogue"; public const string Auteurs = "auteurs"; public const string PretsEnCours = "prets-en-cours"; public const string Utilisateur = "utilisateur"; /// /// La liste d'envies de l'utilisateur courant. /// /// /// ⚠️ Cet instantané est le seul à contenir des données personnelles : le catalogue, /// les auteurs et les prêts sont communs au foyer. Il n'est donc écrit qu'avec ce que le /// serveur a bien voulu rendre à l'appelant — jamais la liste d'un autre, que l'API ne sait /// de toute façon pas produire. /// /// Sa raison d'être : la liste s'emporte en librairie, exactement l'endroit où le /// réseau manque. C'est le même usage que l'export .txt, lequel est produit côté /// serveur et devient donc impossible hors-ligne. /// /// public const string Souhaits = "souhaits"; } /// Un instantané relu du cache, avec la date de la synchronisation qui l'a produit. public sealed record Instantane(T Donnees, DateTimeOffset Date); /// /// Cache de consultation hors-ligne, en lecture seule (CLAUDE.md). /// /// /// Rien n'est mis en file d'attente et rien n'est synchronisé en retour : ce cache ne sert qu'à /// afficher la bibliothèque déjà enregistrée quand le réseau manque. Les écritures sont refusées /// en amont, par . /// /// Toute panne du stockage (IndexedDB indisponible, quota, navigation privée) est avalée : ne pas /// pouvoir cacher n'est pas une raison d'empêcher l'application de fonctionner en ligne. /// /// public sealed class CacheHorsLigne(IJSRuntime js) : IAsyncDisposable { private static readonly JsonSerializerOptions Json = new(JsonSerializerDefaults.Web); private IJSObjectReference? _module; /// Vrai tant que le stockage n'a pas montré qu'il était inutilisable. public bool Disponible { get; private set; } = true; private async ValueTask ModuleAsync() => _module ??= await js.InvokeAsync("import", "./js/cache-hors-ligne.js"); public async Task?> LireAsync(string cle) { try { var module = await ModuleAsync(); var brut = await module.InvokeAsync("lire", cle); if (brut?.Json is null) { return null; } var donnees = JsonSerializer.Deserialize(brut.Json, Json); return donnees is null ? null : new Instantane(donnees, brut.Date); } catch (Exception) { // Cache illisible : on se comporte comme s'il était vide. Disponible = false; return null; } } public async Task EcrireAsync(string cle, T valeur, DateTimeOffset date) { try { var module = await ModuleAsync(); await module.InvokeVoidAsync( "ecrire", cle, JsonSerializer.Serialize(valeur, Json), date.ToString("O")); } catch (Exception) { Disponible = false; } } /// État déclaré par le navigateur. Voir la nuance dans cache-hors-ligne.js. public async Task EnLigneAsync() { try { var module = await ModuleAsync(); return await module.InvokeAsync("enLigne"); } catch (Exception) { return true; // Sans information, on suppose le réseau : un échec d'appel corrigera. } } /// Abonne aux événements online/offline. public async Task SurveillerAsync(DotNetObjectReference destinataire) where T : class { try { var module = await ModuleAsync(); return await module.InvokeAsync("surveiller", destinataire); } catch (Exception) { return true; } } public async ValueTask DisposeAsync() { if (_module is null) { return; } try { await _module.DisposeAsync(); } catch (JSDisconnectedException) { // Page en cours de fermeture : il n'y a plus personne à qui parler. } } private sealed record EntreeCache(string? Json, DateTimeOffset Date); }