// Cache de consultation hors-ligne (CLAUDE.md — « Stratégie hors-ligne »). // // Rôle unique de ce fichier : ranger et relire des instantanés JSON dans IndexedDB, et dire à // C# quand le navigateur bascule en ligne / hors ligne. AUCUNE logique métier ici. // // Pourquoi IndexedDB et pas le cache du service worker : c'est la seule option qui permette de // rechercher et trier hors-ligne sur TOUTE la bibliothèque. Un cache de réponses HTTP ne // restituerait que les URL déjà visitées — une recherche jamais tapée ne rendrait rien. // // Les valeurs sont stockées telles quelles, en chaîne JSON : c'est System.Text.Json côté C# qui // sérialise et désérialise, donc une seule forme fait autorité et le clone structuré d'IndexedDB // n'a rien à interpréter. const NOM_BASE = 'mabibli'; const MAGASIN = 'instantanes'; const VERSION = 1; let promesseBase = null; function ouvrir() { if (promesseBase) return promesseBase; promesseBase = new Promise((resoudre, rejeter) => { // IndexedDB peut être absent ou refusé (navigation privée stricte, stockage bloqué). // On rejette proprement : l'appelant C# retombe alors sur « pas de cache ». if (!self.indexedDB) { rejeter(new Error('IndexedDB indisponible')); return; } const requete = indexedDB.open(NOM_BASE, VERSION); requete.onupgradeneeded = () => { const base = requete.result; if (!base.objectStoreNames.contains(MAGASIN)) base.createObjectStore(MAGASIN); }; requete.onsuccess = () => resoudre(requete.result); requete.onerror = () => rejeter(requete.error); requete.onblocked = () => rejeter(new Error('IndexedDB bloqué')); }).catch(e => { promesseBase = null; throw e; }); return promesseBase; } function attendre(requete, transaction) { return new Promise((resoudre, rejeter) => { requete.onsuccess = () => resoudre(requete.result); requete.onerror = () => rejeter(requete.error); if (transaction) transaction.onabort = () => rejeter(transaction.error); }); } // Écrit un instantané. `dateIso` est l'instant de la synchronisation, pas celui de l'écriture : // c'est cette date que l'interface affiche pour dire de quand datent les données montrées. export async function ecrire(cle, json, dateIso) { const base = await ouvrir(); const tx = base.transaction(MAGASIN, 'readwrite'); const requete = tx.objectStore(MAGASIN).put({ json, date: dateIso }, cle); await attendre(requete, tx); return true; } // Renvoie { json, date } ou null si rien n'a jamais été rangé sous cette clé. export async function lire(cle) { const base = await ouvrir(); const tx = base.transaction(MAGASIN, 'readonly'); const valeur = await attendre(tx.objectStore(MAGASIN).get(cle), tx); return valeur ?? null; } export async function vider() { const base = await ouvrir(); const tx = base.transaction(MAGASIN, 'readwrite'); await attendre(tx.objectStore(MAGASIN).clear(), tx); return true; } // navigator.onLine ne vaut que par sa négation : « false » est fiable (aucune interface réseau), // « true » ne prouve rien (portail captif, serveur éteint). C# complète donc cet état avec le // résultat réel de ses appels HTTP — voir EtatReseau.SignalerEchecReseau. export function enLigne() { return navigator.onLine !== false; } export function surveiller(reference) { const prevenir = () => reference.invokeMethodAsync('SurChangementReseau', navigator.onLine !== false); self.addEventListener('online', prevenir); self.addEventListener('offline', prevenir); return navigator.onLine !== false; }