Sert les couvertures depuis IndexedDB aussi en ligne, et libère les blob:

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
mathieu
2026-08-20 23:09:17 +02:00
co-authored by Claude Opus 5
parent 70f6844a95
commit bdcf97a3e6
5 changed files with 145 additions and 26 deletions
+44 -17
View File
@@ -1,6 +1,7 @@
@using MaBibli.Client.Services
@inject CacheHorsLigne Cache
@inject EtatReseau Reseau
@implements IAsyncDisposable
@*
Couverture d'un livre, avec substitut.
@@ -20,10 +21,19 @@
seulement l'image. Sans ça, cliquer une vignette cassée agrandissait quand même une image
cassée (A4, IDEES.md).
Cache hors-ligne (A5, IDEES.md) : EN LIGNE, l'affichage utilise l'URL réseau telle quelle —
rien ne doit ralentir la voie rapide — et une mise en cache démarre en tâche de fond,
sans attendre son résultat, pour que la même couverture reste visible hors-ligne. HORS
LIGNE, on tente d'abord le cache : l'URL réseau échouerait de toute façon.
Cache des couvertures : le cache IndexedDB est consulté D'ABORD, en ligne comme hors ligne.
Une image déjà rangée sur l'appareil s'affiche sans aucune requête réseau — c'est ce qui rend
un rechargement instantané sur téléphone, là où covers.openlibrary.org impose deux
redirections avant d'aboutir sur archive.org. Le réseau n'est sollicité QUE si rien n'est en
cache, et la mise en cache démarre alors en tâche de fond.
⚠️ Ceci REMPLACE la règle d'origine (A5) « en ligne, rien ne doit ralentir la voie rapide »,
qui réservait le cache au hors-ligne. La lecture IndexedDB précède désormais l'affichage :
elle est locale et sans réseau, donc largement plus rapide que ce qu'elle remplace.
⚠️ Corollaire : l'URL d'objet doit être LIBÉRÉE quand le composant disparaît ou change
d'image (Dispose / changement d'URL). Un blob: retient son Blob en mémoire jusque-là, et il y
a maintenant une vignette par carte sur tous les écrans.
⚠️ L'allègement (redimensionner, convertir en WebP) n'est PAS fait ici : à mesurer avant
de s'y engager, comme pour l'AOT — voir IDEES.md.
*@
@@ -94,11 +104,11 @@
private bool ADesPixels => !string.IsNullOrWhiteSpace(Url);
/// <summary>
/// URL réellement posée en <c>src</c> : l'URL réseau en ligne (voie rapide, inchangée), le
/// blob mis en cache hors-ligne quand il existe, sinon l'URL réseau telle quelle (qui échoue
/// simplement, comme avant cette fonctionnalité).
/// URL réellement posée en <c>src</c> : le blob mis en cache dès qu'il existe — en ligne
/// comme hors ligne , sinon l'URL réseau telle quelle (qui échoue simplement hors-ligne,
/// exactement comme avant cette fonctionnalité).
/// </summary>
private string? UrlAffichee => !Reseau.EnLigne && _urlCachee is not null ? _urlCachee : Url;
private string? UrlAffichee => _urlCachee ?? Url;
private string Initiale =>
string.IsNullOrWhiteSpace(Titre) ? "?" : Titre.Trim()[..1].ToUpperInvariant();
@@ -131,21 +141,38 @@
}
_urlResoluePour = jeton;
_urlCachee = null;
await LibererAsync();
if (Reseau.EnLigne)
// Le cache d'abord, EN LIGNE COMME HORS LIGNE : une image déjà sur l'appareil s'affiche
// sans requête, sans redirection, et sans réveiller la radio du téléphone.
_urlCachee = await Cache.LireCouvertureCacheeAsync(Url!);
if (_urlCachee is null && Reseau.EnLigne)
{
// Tâche de fond, jamais attendue : la voie d'affichage en ligne ne doit rien à
// ce cache, elle reste aussi rapide qu'avant.
// Rien en cache : le réseau sert l'image (UrlAffichee retombe sur Url), et la mise
// en cache démarre en tâche de fond pour que le prochain chargement soit local.
// ⚠️ Jamais attendue : elle ne doit pas retarder un affichage qui va avoir lieu.
_ = Cache.MettreEnCacheCouvertureAsync(Url!);
}
else
{
_urlCachee = await Cache.LireCouvertureCacheeAsync(Url!);
StateHasChanged();
}
StateHasChanged();
}
/// <summary>Révoque l'URL d'objet en cours, s'il y en a une.</summary>
private async Task LibererAsync()
{
if (_urlCachee is null)
{
return;
}
var aLiberer = _urlCachee;
_urlCachee = null;
await Cache.LibererCouvertureAsync(aLiberer);
}
public async ValueTask DisposeAsync() => await LibererAsync();
protected override async Task OnAfterRenderAsync(bool premierRendu)
{
if (_aFocaliser)
+21
View File
@@ -191,6 +191,27 @@ public sealed class CacheHorsLigne(IJSRuntime js) : IAsyncDisposable
}
}
/// <summary>
/// Libère une URL d'objet obtenue par <see cref="LireCouvertureCacheeAsync"/>.
/// </summary>
/// <remarks>
/// ⚠️ Nécessaire depuis que le cache sert aussi en ligne : une URL <c>blob:</c> retient son
/// Blob en mémoire tant qu'elle n'est pas révoquée. Avec une vignette par carte sur tous les
/// écrans, ne pas libérer ferait enfler la mémoire de l'onglet au fil de la navigation.
/// </remarks>
public async Task LibererCouvertureAsync(string url)
{
try
{
var module = await ModuleAsync();
await module.InvokeVoidAsync("couvertureLiberer", url);
}
catch (Exception)
{
// Voir MettreEnCacheCouvertureAsync : rien à propager.
}
}
public async ValueTask DisposeAsync()
{
if (_module is null)
@@ -154,3 +154,15 @@ export async function couvertureLire(url) {
return null;
}
}
// Libère une URL d'objet rendue par `couvertureLire`. ⚠️ Indispensable depuis que le cache sert
// AUSSI en ligne : chaque `createObjectURL` retient son Blob en mémoire jusqu'à ce qu'on le
// révoque ou que la page se ferme. Hors-ligne seul, la fuite restait bornée à une consultation
// ponctuelle ; sur toutes les vignettes de tous les écrans, elle ne l'est plus.
export function couvertureLiberer(url) {
try {
if (url) URL.revokeObjectURL(url);
} catch (e) {
// Rien à faire : libérer est un agrément, jamais une condition de bon fonctionnement.
}
}