Fusionner le cache hors-ligne IndexedDB
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> # Conflicts: # MaBibli.Client/wwwroot/css/app.css
This commit is contained in:
@@ -8,15 +8,59 @@ using MaBibli.Shared.Entites;
|
||||
namespace MaBibli.Client.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Accès à l'API depuis le client Blazor.
|
||||
/// Accès à l'API depuis le client Blazor, avec repli sur le cache hors-ligne.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Aucun cache : la phase 3 suppose le réseau disponible. Le cache hors-ligne est la phase 6.
|
||||
/// <b>Le piège que ce service existe pour éviter</b> (CLAUDE.md) : en Blazor WebAssembly le code
|
||||
/// tourne dans le navigateur alors que SQLite vit sur le serveur. Le service worker met en cache
|
||||
/// les <i>assets</i> — HTML, CSS, WASM — mais jamais les réponses de l'API. Sans le travail fait
|
||||
/// ici, l'application démarre parfaitement hors-ligne… et affiche une bibliothèque vide.
|
||||
/// <para>
|
||||
/// La règle est simple et sans exception : <b>les lectures retombent sur le cache, les écritures
|
||||
/// sont refusées</b>. Aucune file d'attente, aucune synchronisation en retour, aucun conflit à
|
||||
/// résoudre — c'est la décision actée, et c'est ce qui rend le hors-ligne fiable plutôt que
|
||||
/// « presque bon ».
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public sealed class ServiceLivresApi(HttpClient http)
|
||||
public sealed class ServiceLivresApi(HttpClient http, CacheHorsLigne cache, EtatReseau reseau)
|
||||
{
|
||||
private static readonly JsonSerializerOptions Json = new(JsonSerializerDefaults.Web);
|
||||
|
||||
/// <summary>
|
||||
/// Recharge tous les instantanés depuis le réseau.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Appelé au démarrage et à chaque retour du réseau : c'est ce qui garantit que la
|
||||
/// bibliothèque entière est disponible hors-ligne, et pas seulement les écrans déjà ouverts.
|
||||
/// </remarks>
|
||||
/// <returns>Vrai si les données ont pu être rafraîchies.</returns>
|
||||
public async Task<bool> SynchroniserAsync(CancellationToken ct = default)
|
||||
{
|
||||
if (!reseau.TenterLeReseau)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await ListerAsync(new CritereLivres(), ct);
|
||||
await ListerAuteursAsync(ct);
|
||||
await ListerPretsEnCoursAsync(ct);
|
||||
await ObtenirUtilisateurAsync(ct);
|
||||
return reseau.EnLigne;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Une synchronisation ratée n'est pas une panne de l'application : l'écran affichera
|
||||
// ce qu'il a, et la date de synchronisation dira qu'il n'est plus tout frais.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<LivreDto>> ListerAsync(CritereLivres criteres, CancellationToken ct = default)
|
||||
{
|
||||
var parametres = new List<string>();
|
||||
@@ -43,56 +87,112 @@ public sealed class ServiceLivresApi(HttpClient http)
|
||||
|
||||
var url = "api/livres" + (parametres.Count > 0 ? "?" + string.Join('&', parametres) : string.Empty);
|
||||
|
||||
return await http.GetFromJsonAsync<List<LivreDto>>(url, Json, ct) ?? [];
|
||||
var (ok, liste) = await EssayerAsync(() => http.GetFromJsonAsync<List<LivreDto>>(url, Json, ct));
|
||||
|
||||
if (ok)
|
||||
{
|
||||
// Seule la liste NON filtrée fait un instantané valable : c'est elle qui permettra
|
||||
// de chercher hors-ligne autre chose que ce qui était affiché au moment de la coupure.
|
||||
if (parametres.Count == 0)
|
||||
{
|
||||
await MemoriserAsync(ClesCache.Catalogue, liste ?? []);
|
||||
}
|
||||
|
||||
return liste ?? [];
|
||||
}
|
||||
|
||||
var instantane = await LireCacheAsync<List<LivreDto>>(ClesCache.Catalogue);
|
||||
return instantane is null ? [] : FiltreLivresLocal.Appliquer(instantane.Donnees, criteres);
|
||||
}
|
||||
|
||||
public async Task<LivreDto?> ObtenirAsync(int id, CancellationToken ct = default)
|
||||
{
|
||||
var reponse = await http.GetAsync($"api/livres/{id}", ct);
|
||||
return reponse.StatusCode == HttpStatusCode.NotFound
|
||||
? null
|
||||
: await LireAsync<LivreDto>(reponse, ct);
|
||||
var (ok, livre) = await EssayerAsync(async () =>
|
||||
{
|
||||
var reponse = await http.GetAsync($"api/livres/{id}", ct);
|
||||
return reponse.StatusCode == HttpStatusCode.NotFound
|
||||
? null
|
||||
: await LireAsync<LivreDto>(reponse, ct);
|
||||
});
|
||||
|
||||
if (ok)
|
||||
{
|
||||
return livre;
|
||||
}
|
||||
|
||||
// Hors-ligne, la fiche se relit dans l'instantané du catalogue : pas d'appel par livre à
|
||||
// mettre en cache, donc rien à maintenir en plus.
|
||||
var instantane = await LireCacheAsync<List<LivreDto>>(ClesCache.Catalogue);
|
||||
return instantane?.Donnees.FirstOrDefault(l => l.Id == id);
|
||||
}
|
||||
|
||||
public async Task<ResultatApi<LivreDto>> CreerAsync(EnregistrementLivre saisie, CancellationToken ct = default)
|
||||
{
|
||||
var reponse = await http.PostAsJsonAsync("api/livres", saisie, Json, ct);
|
||||
return await InterpreterAsync<LivreDto>(reponse, ct);
|
||||
}
|
||||
public async Task<ResultatApi<LivreDto>> CreerAsync(EnregistrementLivre saisie, CancellationToken ct = default) =>
|
||||
await EcrireAsync<LivreDto>(() => http.PostAsJsonAsync("api/livres", saisie, Json, ct), ct);
|
||||
|
||||
public async Task<ResultatApi<LivreDto>> ModifierAsync(int id, EnregistrementLivre saisie, CancellationToken ct = default)
|
||||
{
|
||||
var reponse = await http.PutAsJsonAsync($"api/livres/{id}", saisie, Json, ct);
|
||||
return await InterpreterAsync<LivreDto>(reponse, ct);
|
||||
}
|
||||
public async Task<ResultatApi<LivreDto>> ModifierAsync(int id, EnregistrementLivre saisie, CancellationToken ct = default) =>
|
||||
await EcrireAsync<LivreDto>(() => http.PutAsJsonAsync($"api/livres/{id}", saisie, Json, ct), ct);
|
||||
|
||||
/// <summary>
|
||||
/// Change le statut de lecture <b>de l'utilisateur courant</b> ; <c>null</c> le remet à
|
||||
/// « non commencé ». Le serveur sait qui appelle : le client ne transmet jamais d'identité.
|
||||
/// </summary>
|
||||
public async Task<ResultatApi<LivreDto>> ChangerStatutAsync(
|
||||
int id, Statut? statut, CancellationToken ct = default)
|
||||
{
|
||||
var reponse = await http.PatchAsJsonAsync(
|
||||
$"api/livres/{id}/statut", new ChangementStatut { Statut = statut }, Json, ct);
|
||||
return await InterpreterAsync<LivreDto>(reponse, ct);
|
||||
}
|
||||
int id, Statut? statut, CancellationToken ct = default) =>
|
||||
await EcrireAsync<LivreDto>(
|
||||
() => http.PatchAsJsonAsync(
|
||||
$"api/livres/{id}/statut", new ChangementStatut { Statut = statut }, Json, ct),
|
||||
ct);
|
||||
|
||||
public async Task<IReadOnlyList<AuteurDto>> ListerAuteursAsync(CancellationToken ct = default) =>
|
||||
await http.GetFromJsonAsync<List<AuteurDto>>("api/auteurs", Json, ct) ?? [];
|
||||
public async Task<IReadOnlyList<AuteurDto>> ListerAuteursAsync(CancellationToken ct = default)
|
||||
{
|
||||
var (ok, auteurs) = await EssayerAsync(
|
||||
() => http.GetFromJsonAsync<List<AuteurDto>>("api/auteurs", Json, ct));
|
||||
|
||||
if (ok)
|
||||
{
|
||||
await MemoriserAsync(ClesCache.Auteurs, auteurs ?? []);
|
||||
return auteurs ?? [];
|
||||
}
|
||||
|
||||
var instantane = await LireCacheAsync<List<AuteurDto>>(ClesCache.Auteurs);
|
||||
return instantane?.Donnees ?? [];
|
||||
}
|
||||
|
||||
public async Task<AuteurDto?> ObtenirAuteurAsync(int id, CancellationToken ct = default)
|
||||
{
|
||||
var reponse = await http.GetAsync($"api/auteurs/{id}", ct);
|
||||
return reponse.StatusCode == HttpStatusCode.NotFound
|
||||
? null
|
||||
: await LireAsync<AuteurDto>(reponse, ct);
|
||||
var (ok, auteur) = await EssayerAsync(async () =>
|
||||
{
|
||||
var reponse = await http.GetAsync($"api/auteurs/{id}", ct);
|
||||
return reponse.StatusCode == HttpStatusCode.NotFound
|
||||
? null
|
||||
: await LireAsync<AuteurDto>(reponse, ct);
|
||||
});
|
||||
|
||||
if (ok)
|
||||
{
|
||||
return auteur;
|
||||
}
|
||||
|
||||
var instantane = await LireCacheAsync<List<AuteurDto>>(ClesCache.Auteurs);
|
||||
return instantane?.Donnees.FirstOrDefault(a => a.Id == id);
|
||||
}
|
||||
|
||||
/// <summary>Rapprochements d'auteurs ambigus, que le serveur refuse d'appliquer seul.</summary>
|
||||
/// <summary>
|
||||
/// Rapprochements d'auteurs ambigus, que le serveur refuse d'appliquer seul.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Volontairement <b>pas</b> mis en cache : c'est une liste de décisions à prendre, or aucune
|
||||
/// décision ne peut être enregistrée hors-ligne. L'afficher sans pouvoir y répondre
|
||||
/// n'apporterait rien.
|
||||
/// </remarks>
|
||||
public async Task<IReadOnlyList<SuggestionRapprochement>> ListerRapprochementsAsync(
|
||||
CancellationToken ct = default) =>
|
||||
await http.GetFromJsonAsync<List<SuggestionRapprochement>>("api/auteurs/rapprochements", Json, ct) ?? [];
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var (ok, suggestions) = await EssayerAsync(
|
||||
() => http.GetFromJsonAsync<List<SuggestionRapprochement>>("api/auteurs/rapprochements", Json, ct));
|
||||
|
||||
return ok ? suggestions ?? [] : [];
|
||||
}
|
||||
|
||||
public Task<string?> AccepterRapprochementAsync(int sourceId, int cibleId, CancellationToken ct = default) =>
|
||||
DeciderAsync("accepter", sourceId, cibleId, ct);
|
||||
@@ -102,29 +202,65 @@ public sealed class ServiceLivresApi(HttpClient http)
|
||||
|
||||
private async Task<string?> DeciderAsync(string action, int sourceId, int cibleId, CancellationToken ct)
|
||||
{
|
||||
var reponse = await http.PostAsJsonAsync(
|
||||
$"api/auteurs/rapprochements/{action}",
|
||||
new DecisionRapprochement { SourceId = sourceId, CibleId = cibleId },
|
||||
Json,
|
||||
ct);
|
||||
if (!reseau.EnLigne)
|
||||
{
|
||||
return EtatReseau.MotifHorsLigne;
|
||||
}
|
||||
|
||||
return reponse.IsSuccessStatusCode ? null : await MessageErreurAsync(reponse, ct);
|
||||
try
|
||||
{
|
||||
var reponse = await http.PostAsJsonAsync(
|
||||
$"api/auteurs/rapprochements/{action}",
|
||||
new DecisionRapprochement { SourceId = sourceId, CibleId = cibleId },
|
||||
Json,
|
||||
ct);
|
||||
|
||||
reseau.SignalerSuccesReseau();
|
||||
return reponse.IsSuccessStatusCode ? null : await MessageErreurAsync(reponse, ct);
|
||||
}
|
||||
catch (HttpRequestException)
|
||||
{
|
||||
reseau.SignalerEchecReseau();
|
||||
return EtatReseau.MotifHorsLigne;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Les livres actuellement dehors, du prêt le plus ancien au plus récent.</summary>
|
||||
public async Task<IReadOnlyList<PretDto>> ListerPretsEnCoursAsync(CancellationToken ct = default) =>
|
||||
await http.GetFromJsonAsync<List<PretDto>>("api/prets/en-cours", Json, ct) ?? [];
|
||||
public async Task<IReadOnlyList<PretDto>> ListerPretsEnCoursAsync(CancellationToken ct = default)
|
||||
{
|
||||
var (ok, prets) = await EssayerAsync(
|
||||
() => http.GetFromJsonAsync<List<PretDto>>("api/prets/en-cours", Json, ct));
|
||||
|
||||
if (ok)
|
||||
{
|
||||
await MemoriserAsync(ClesCache.PretsEnCours, prets ?? []);
|
||||
return prets ?? [];
|
||||
}
|
||||
|
||||
var instantane = await LireCacheAsync<List<PretDto>>(ClesCache.PretsEnCours);
|
||||
return instantane?.Donnees ?? [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Historique complet des prêts d'un livre, du plus récent au plus ancien.
|
||||
/// </summary>
|
||||
/// <remarks>Liste vide = jamais prêté ; <c>null</c> = livre inconnu.</remarks>
|
||||
/// <remarks>
|
||||
/// Liste vide = jamais prêté ; <c>null</c> = livre inconnu <b>ou</b> réseau absent. L'appelant
|
||||
/// doit consulter <see cref="EtatReseau.EnLigne"/> pour distinguer les deux : l'historique
|
||||
/// n'est pas mis en cache (une ligne par livre, pour une information rarement consultée),
|
||||
/// et afficher « jamais prêté » hors-ligne serait un mensonge.
|
||||
/// </remarks>
|
||||
public async Task<IReadOnlyList<PretDto>?> ListerPretsLivreAsync(int livreId, CancellationToken ct = default)
|
||||
{
|
||||
var reponse = await http.GetAsync($"api/livres/{livreId}/prets", ct);
|
||||
return reponse.StatusCode == HttpStatusCode.NotFound
|
||||
? null
|
||||
: await LireAsync<List<PretDto>>(reponse, ct);
|
||||
var (ok, prets) = await EssayerAsync(async () =>
|
||||
{
|
||||
var reponse = await http.GetAsync($"api/livres/{livreId}/prets", ct);
|
||||
return reponse.StatusCode == HttpStatusCode.NotFound
|
||||
? null
|
||||
: await LireAsync<List<PretDto>>(reponse, ct);
|
||||
});
|
||||
|
||||
return ok ? prets : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -140,18 +276,17 @@ public sealed class ServiceLivresApi(HttpClient http)
|
||||
DatePret = EnUtc(datePret),
|
||||
};
|
||||
|
||||
var reponse = await http.PostAsJsonAsync($"api/livres/{livreId}/prets", saisie, Json, ct);
|
||||
return await InterpreterAsync<PretDto>(reponse, ct);
|
||||
return await EcrireAsync<PretDto>(
|
||||
() => http.PostAsJsonAsync($"api/livres/{livreId}/prets", saisie, Json, ct), ct);
|
||||
}
|
||||
|
||||
/// <summary>Clôt un prêt. Le prêt n'est pas supprimé : il devient l'historique du livre.</summary>
|
||||
public async Task<ResultatApi<PretDto>> RendreAsync(
|
||||
int pretId, DateTime? dateRetour = null, CancellationToken ct = default)
|
||||
{
|
||||
var reponse = await http.PostAsJsonAsync(
|
||||
$"api/prets/{pretId}/retour", new RetourPret { DateRetour = EnUtc(dateRetour) }, Json, ct);
|
||||
return await InterpreterAsync<PretDto>(reponse, ct);
|
||||
}
|
||||
int pretId, DateTime? dateRetour = null, CancellationToken ct = default) =>
|
||||
await EcrireAsync<PretDto>(
|
||||
() => http.PostAsJsonAsync(
|
||||
$"api/prets/{pretId}/retour", new RetourPret { DateRetour = EnUtc(dateRetour) }, Json, ct),
|
||||
ct);
|
||||
|
||||
/// <summary>
|
||||
/// Convertit une date issue d'un <input type="date"> en instant UTC.
|
||||
@@ -166,29 +301,164 @@ public sealed class ServiceLivresApi(HttpClient http)
|
||||
|
||||
public async Task<string?> SupprimerAsync(int id, CancellationToken ct = default)
|
||||
{
|
||||
var reponse = await http.DeleteAsync($"api/livres/{id}", ct);
|
||||
return reponse.IsSuccessStatusCode ? null : await MessageErreurAsync(reponse, ct);
|
||||
if (!reseau.EnLigne)
|
||||
{
|
||||
return EtatReseau.MotifHorsLigne;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var reponse = await http.DeleteAsync($"api/livres/{id}", ct);
|
||||
reseau.SignalerSuccesReseau();
|
||||
return reponse.IsSuccessStatusCode ? null : await MessageErreurAsync(reponse, ct);
|
||||
}
|
||||
catch (HttpRequestException)
|
||||
{
|
||||
reseau.SignalerEchecReseau();
|
||||
return EtatReseau.MotifHorsLigne;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Interroge la cascade BnF → OpenLibrary. <b>Exige le réseau</b> : rien n'est mis en cache,
|
||||
/// et hors-ligne l'appel n'est même pas tenté (<c>null</c> immédiat).
|
||||
/// </summary>
|
||||
public async Task<ResultatLookupIsbn?> ChercherIsbnAsync(string isbn, CancellationToken ct = default)
|
||||
{
|
||||
var reponse = await http.GetAsync($"api/isbn/{Uri.EscapeDataString(isbn)}", ct);
|
||||
return reponse.IsSuccessStatusCode
|
||||
? await LireAsync<ResultatLookupIsbn>(reponse, ct)
|
||||
: null;
|
||||
var (ok, resultat) = await EssayerAsync(async () =>
|
||||
{
|
||||
var reponse = await http.GetAsync($"api/isbn/{Uri.EscapeDataString(isbn)}", ct);
|
||||
return reponse.IsSuccessStatusCode
|
||||
? await LireAsync<ResultatLookupIsbn>(reponse, ct)
|
||||
: null;
|
||||
});
|
||||
|
||||
return ok ? resultat : null;
|
||||
}
|
||||
|
||||
public async Task<UtilisateurCourant> ObtenirUtilisateurAsync(CancellationToken ct = default)
|
||||
{
|
||||
var (ok, utilisateur) = await EssayerAsync(
|
||||
() => http.GetFromJsonAsync<UtilisateurCourant>("api/moi", Json, ct));
|
||||
|
||||
if (ok && utilisateur is not null)
|
||||
{
|
||||
await MemoriserAsync(ClesCache.Utilisateur, utilisateur);
|
||||
return utilisateur;
|
||||
}
|
||||
|
||||
// Hors-ligne, le bandeau garde le nom de la dernière session plutôt que de faire croire
|
||||
// à une déconnexion.
|
||||
var instantane = await LireCacheAsync<UtilisateurCourant>(ClesCache.Utilisateur);
|
||||
return instantane?.Donnees ?? UtilisateurCourant.Anonyme;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Exécute un appel de lecture. Renvoie <c>false</c> — sans exception — quand le réseau
|
||||
/// manque, ce qui est la bascule vers le cache. Une annulation demandée par l'appelant,
|
||||
/// elle, continue de remonter : c'est une frappe au clavier, pas une panne.
|
||||
/// </summary>
|
||||
private async Task<(bool Ok, T? Valeur)> EssayerAsync<T>(Func<Task<T?>> appel)
|
||||
{
|
||||
// TenterLeReseau et non EnLigne : quand la panne vient du serveur, le navigateur n'a
|
||||
// jamais quitté le réseau et n'émettra donc aucun événement « online ». C'est la lecture
|
||||
// suivante qui doit constater le retour, sans quoi l'application resterait sur le cache
|
||||
// jusqu'au rechargement de la page.
|
||||
if (!reseau.TenterLeReseau)
|
||||
{
|
||||
return (false, default);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return await http.GetFromJsonAsync<UtilisateurCourant>("api/moi", Json, ct)
|
||||
?? UtilisateurCourant.Anonyme;
|
||||
var valeur = await appel();
|
||||
reseau.SignalerSuccesReseau();
|
||||
return (true, valeur);
|
||||
}
|
||||
catch (HttpRequestException)
|
||||
{
|
||||
return UtilisateurCourant.Anonyme;
|
||||
reseau.SignalerEchecReseau();
|
||||
return (false, default);
|
||||
}
|
||||
catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException)
|
||||
{
|
||||
// Délai dépassé sans annulation de l'appelant : le serveur ne répond pas.
|
||||
reseau.SignalerEchecReseau();
|
||||
return (false, default);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Exécute une écriture, ou la <b>refuse</b> hors-ligne avec un motif affichable.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// L'interface doit désactiver ces actions bien avant d'arriver ici (CLAUDE.md : pas de
|
||||
/// bouton qui échoue silencieusement). Ce refus est la seconde ligne, pour le cas où le
|
||||
/// réseau tombe entre l'affichage et le clic.
|
||||
/// </remarks>
|
||||
private async Task<ResultatApi<T>> EcrireAsync<T>(
|
||||
Func<Task<HttpResponseMessage>> appel, CancellationToken ct)
|
||||
{
|
||||
if (!reseau.EnLigne)
|
||||
{
|
||||
return new ResultatApi<T>(default, EtatReseau.MotifHorsLigne);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var reponse = await appel();
|
||||
reseau.SignalerSuccesReseau();
|
||||
var resultat = await InterpreterAsync<T>(reponse, ct);
|
||||
|
||||
if (resultat.EstOk)
|
||||
{
|
||||
// La base a changé : l'instantané doit suivre, sinon une coupure juste après une
|
||||
// modification montrerait un catalogue d'avant.
|
||||
_ = RafraichirEnFondAsync();
|
||||
}
|
||||
|
||||
return resultat;
|
||||
}
|
||||
catch (HttpRequestException)
|
||||
{
|
||||
reseau.SignalerEchecReseau();
|
||||
return new ResultatApi<T>(default, EtatReseau.MotifHorsLigne);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Remet l'instantané à jour sans faire attendre l'utilisateur.</summary>
|
||||
private async Task RafraichirEnFondAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
await ListerAsync(new CritereLivres());
|
||||
await ListerPretsEnCoursAsync();
|
||||
await ListerAuteursAsync();
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Le cache restera daté de la synchronisation précédente : l'interface le dit.
|
||||
}
|
||||
}
|
||||
|
||||
private async Task MemoriserAsync<T>(string cle, T valeur)
|
||||
{
|
||||
var date = DateTimeOffset.Now;
|
||||
await cache.EcrireAsync(cle, valeur, date);
|
||||
reseau.SignalerSynchro(date);
|
||||
}
|
||||
|
||||
/// <summary>Relit un instantané et fait remonter sa date, celle que l'interface affiche.</summary>
|
||||
private async Task<Instantane<T>?> LireCacheAsync<T>(string cle)
|
||||
{
|
||||
var instantane = await cache.LireAsync<T>(cle);
|
||||
|
||||
if (instantane is not null)
|
||||
{
|
||||
reseau.SignalerSynchro(instantane.Date);
|
||||
}
|
||||
|
||||
return instantane;
|
||||
}
|
||||
|
||||
private static async Task<T?> LireAsync<T>(HttpResponseMessage reponse, CancellationToken ct)
|
||||
|
||||
Reference in New Issue
Block a user