# Chapitre 4 : Synchronisation Offline SmartCommon fournit un module complet pour la synchronisation offline-first des applications PWA Dolibarr. ## Vue d'ensemble Le module sync permet de : - Travailler hors connexion avec les données locales - Synchroniser automatiquement quand la connexion revient - Gérer les conflits de données entre client et serveur ## useSyncClient Hook principal pour la synchronisation offline-first. ### Import ```javascript import { useSyncClient } from '@cap-rel/smartcommon'; ``` ### Configuration ```javascript function MyApp() { const { isOnline, isSyncing, pendingCount, sync, create, update, remove, getConflicts, resolveConflict } = useSyncClient({ apiUrl: '/api/smartauth', getAccessToken: () => localStorage.getItem('access_token'), scope: ['thirdparty', 'contact', 'product'] }); return (

Status : {isOnline ? 'En ligne' : 'Hors ligne'}

Modifications en attente : {pendingCount}

); } ``` ### Paramètres ^ Paramètre ^ Type ^ Description ^ | apiUrl | string | URL de base de l'API de synchronisation | | getAccessToken | function | Fonction retournant le token JWT | | scope | string[] | Liste des entités à synchroniser | ### Valeurs retournées ^ Propriété ^ Type ^ Description ^ | isOnline | boolean | Statut de connexion | | isSyncing | boolean | Synchronisation en cours | | pendingCount | number | Nombre de modifications en attente | | sync | function | Déclencher la synchronisation | | create | function | Créer une entité (offline-capable) | | update | function | Modifier une entité | | remove | function | Supprimer une entité | | upsert | function | Créer ou mettre à jour localement (cache) | | getConflicts | function | Récupérer les conflits | | resolveConflict | function | Résoudre un conflit | ### Créer une entité ```javascript function CreateThirdpartyForm() { const { create, pendingCount } = useSyncClient({ apiUrl: '/api/smartauth', getAccessToken: () => localStorage.getItem('access_token'), scope: ['thirdparty'] }); const handleCreate = async (data) => { // Crée localement avec un ID temporaire // Sera synchronisé quand la connexion revient const tempId = await create('thirdparty', { name: data.name, email: data.email, phone: data.phone }); console.log('Créé avec ID temporaire:', tempId); }; return (
{/* ... */}

En attente de sync : {pendingCount}

); } ``` ### Modifier et supprimer ```javascript function ThirdpartyActions({ thirdparty }) { const { update, remove } = useSyncClient({ apiUrl: '/api/smartauth', getAccessToken: () => localStorage.getItem('access_token'), scope: ['thirdparty'] }); const handleUpdate = async () => { await update('thirdparty', thirdparty.id, { name: 'Nouveau nom' }); }; const handleDelete = async () => { await remove('thirdparty', thirdparty.id); }; return (
); } ``` ### Upsert (cache local) La méthode `upsert` permet de stocker des données localement sans déclencher de synchronisation vers le serveur. Elle crée l'entité si elle n'existe pas, ou la met à jour si elle existe déjà. ```javascript function ThirdpartyDetail({ id }) { const { upsert, getEntity } = useSyncClient({ apiUrl: '/api/smartauth', getAccessToken: () => localStorage.getItem('access_token'), scope: ['thirdparty'] }); const cacheServerData = async () => { // Récupérer les données du serveur const data = await api.private.get(`thirdparties/${id}`).json(); // Stocker localement sans déclencher de sync await upsert('thirdparty', id, data); }; // Avec queueChange = true, la modification sera synchronisée const upsertAndSync = async (data) => { await upsert('thirdparty', id, data, true); }; // ... } ``` #### Paramètres ^ Paramètre ^ Type ^ Défaut ^ Description ^ | table | string | - | Nom de la table | | id | number/string | - | ID de l'entité | | data | object | - | Données de l'entité | | queueChange | boolean | false | Si true, ajoute la modification à la queue de sync | ### Synchronisation manuelle ```javascript function SyncButton() { const { sync, isSyncing, pendingCount, isOnline } = useSyncClient({ apiUrl: '/api/smartauth', getAccessToken: () => localStorage.getItem('access_token'), scope: ['thirdparty', 'contact'] }); const handleSync = async () => { const result = await sync(); console.log('Synchronisé:', result); }; return ( ); } ``` ## ConflictResolver Composant UI pour résoudre les conflits de synchronisation. ### Import ```javascript import { ConflictResolver } from '@cap-rel/smartcommon'; ``` ### Utilisation ```javascript function SyncManager() { const { getConflicts, resolveConflict } = useSyncClient({ apiUrl: '/api/smartauth', getAccessToken: () => localStorage.getItem('access_token'), scope: ['thirdparty'] }); const [conflicts, setConflicts] = useState([]); useEffect(() => { loadConflicts(); }, []); const loadConflicts = async () => { const list = await getConflicts(); setConflicts(list); }; const handleResolve = async (conflictId, resolution) => { await resolveConflict(conflictId, resolution); await loadConflicts(); }; if (conflicts.length === 0) { return

Aucun conflit

; } return ( ); } ``` ### Props ^ Prop ^ Type ^ Description ^ | conflicts | array | Liste des conflits à afficher | | onResolve | function | Callback appelé lors de la résolution | ### Structure d'un conflit ```javascript { id: 'conflict_123', entity: 'thirdparty', entityId: 456, localData: { name: 'Version locale', ... }, serverData: { name: 'Version serveur', ... }, localTimestamp: 1707900000000, serverTimestamp: 1707899000000 } ``` ### Résolutions possibles - **'local'** : Garder la version locale - **'server'** : Garder la version serveur - **'merge'** : Fusionner (si supporté) ## useOnlineStatus Hook pour détecter le statut online/offline avec health check serveur optionnel. ### Import ```javascript import { useOnlineStatus } from '@cap-rel/smartcommon'; ``` ### Utilisation simple ```javascript function NetworkStatus() { const { isOnline, isOffline } = useOnlineStatus(); return (
{isOnline ? 'En ligne' : 'Hors ligne'}
); } ``` ### Avec health check serveur ```javascript function ServerStatus() { const { isOnline, isServerReachable, lastCheck, checkNow } = useOnlineStatus({ healthCheckUrl: '/api/health', healthCheckInterval: 60000, // Vérifier toutes les 60s stabilityDelay: 2000, // Attendre 2s avant de déclarer "en ligne" timeout: 5000 // Timeout de 5s }); return (

Navigateur : {isOnline ? 'En ligne' : 'Hors ligne'}

Serveur : {isServerReachable ? 'Accessible' : 'Inaccessible'}

Dernière vérification : {new Date(lastCheck).toLocaleTimeString()}

); } ``` ### Paramètres ^ Paramètre ^ Type ^ Défaut ^ Description ^ | healthCheckUrl | string | null | URL pour vérifier le serveur (null = désactivé) | | healthCheckInterval | number | 30000 | Intervalle entre les vérifications (ms) | | stabilityDelay | number | 2000 | Délai avant de déclarer "en ligne" (ms) | | timeout | number | 5000 | Timeout du health check (ms) | ### Valeurs retournées ^ Propriété ^ Type ^ Description ^ | isOnline | boolean | Navigateur en ligne | | isOffline | boolean | Navigateur hors ligne | | isServerReachable | boolean/null | Serveur accessible (null si non testé) | | lastOnline | number | Timestamp du dernier état "en ligne" | | lastCheck | number | Timestamp de la dernière vérification | | checkNow | function | Forcer une vérification immédiate | ## useCachedQuery Hook pour le cache de requêtes avec stratégies multiples. ### Import ```javascript import { useCachedQuery, CACHE_STRATEGIES } from '@cap-rel/smartcommon'; ``` ### Stratégies disponibles ^ Stratégie ^ Description ^ | NETWORK_FIRST | Réseau d'abord, cache en fallback | | CACHE_FIRST | Cache d'abord si valide, sinon réseau | | STALE_WHILE_REVALIDATE | Afficher le cache, rafraîchir en arrière-plan | ### Exemple : Cache-first pour dictionnaires ```javascript function CountrySelect() { const { data: countries, isLoading, isFromCache } = useCachedQuery({ db: db.instance, store: 'queryCache', key: 'countries', fetchFn: () => api.get('dictionaries/countries').json(), strategy: CACHE_STRATEGIES.CACHE_FIRST, ttl: 86400000 // 24h }); if (isLoading) return ; return ( ); } ``` ### Exemple : Stale-while-revalidate pour config ```javascript function AppConfig() { const { data: config, isStale, refetch, invalidate } = useCachedQuery({ db: db.instance, store: 'queryCache', key: 'app-config', fetchFn: () => api.get('config').json(), strategy: CACHE_STRATEGIES.STALE_WHILE_REVALIDATE, staleTime: 300000 // 5 min }); return (
{isStale &&

Mise à jour en cours...

}
); } ``` ### Paramètres ^ Paramètre ^ Type ^ Défaut ^ Description ^ | db | object | - | Instance Dexie (db.instance) | | store | string | - | Nom du store IndexedDB | | key | string | - | Clé de cache | | fetchFn | function | - | Fonction de récupération des données | | strategy | string | NETWORK_FIRST | Stratégie de cache | | ttl | number | 3600000 | Durée de vie du cache (1h) | | staleTime | number | 60000 | Temps avant données "stale" (1min) | | enabled | boolean | true | Activer/désactiver le fetch | ### Valeurs retournées ^ Propriété ^ Type ^ Description ^ | data | any | Données récupérées/cachées | | isLoading | boolean | Chargement en cours | | isFromCache | boolean | Données provenant du cache | | isStale | boolean | Données périmées | | error | Error | Erreur éventuelle | | lastFetch | number | Timestamp du dernier fetch | | refetch | function | Relancer le fetch | | invalidate | function | Vider le cache et refetch | ## useAuthenticatedImage Hook pour charger des images authentifiées avec cache IndexedDB. ### Import ```javascript import { useAuthenticatedImage } from '@cap-rel/smartcommon'; ``` ### Utilisation ```javascript function UserAvatar({ userId }) { const { src, isLoading, isFromCache, error } = useAuthenticatedImage({ db: db.instance, store: 'imageCache', url: `/api/users/${userId}/photo`, token: accessToken, placeholder: '/images/default-avatar.png', ttl: 86400000, // 24h staleTime: 3600000 // 1h }); if (isLoading) return ; return Avatar; } ``` ### Paramètres ^ Paramètre ^ Type ^ Défaut ^ Description ^ | db | object | - | Instance Dexie | | store | string | 'imageCache' | Nom du store | | url | string | - | URL de l'image | | token | string | - | Token JWT | | ttl | number | 86400000 | Durée de vie (24h) | | staleTime | number | 3600000 | Temps avant stale (1h) | | placeholder | string | null | Image par défaut | ### Valeurs retournées ^ Propriété ^ Type ^ Description ^ | src | string | URL de l'image (blob ou placeholder) | | isLoading | boolean | Chargement en cours | | isFromCache | boolean | Image provenant du cache | | error | Error | Erreur éventuelle | ## Configuration IndexedDB Pour utiliser useCachedQuery et useAuthenticatedImage, configurer les stores Dexie : ```javascript const db = useDb({ name: 'myApp', version: 2, stores: { // Store pour les requêtes cachées queryCache: 'key', // Store pour les images imageCache: 'key', // Autres stores... items: 'id++, name' } }); ``` ## Exemple complet : Application offline-first ```javascript import { useEffect } from 'react'; import { useSyncClient, useOnlineStatus, useCachedQuery, useDb, Page, Block, List, ListItem, Button, ConflictResolver } from '@cap-rel/smartcommon'; function ThirdpartyList() { const db = useDb({ name: 'myApp', version: 1, stores: { queryCache: 'key', pendingChanges: 'id++, entity, action' } }); const { isOnline } = useOnlineStatus({ healthCheckUrl: '/api/health' }); const { sync, isSyncing, pendingCount, getConflicts } = useSyncClient({ apiUrl: '/api/smartauth', getAccessToken: () => localStorage.getItem('access_token'), scope: ['thirdparty'] }); const { data: thirdparties, isLoading, isFromCache, refetch } = useCachedQuery({ db: db.instance, store: 'queryCache', key: 'thirdparties', fetchFn: () => api.get('thirdparties').json(), strategy: 'swr' }); // Synchroniser automatiquement quand on revient en ligne useEffect(() => { if (isOnline && pendingCount > 0) { sync(); } }, [isOnline]); return (
{isOnline ? 'En ligne' : 'Hors ligne'} {isFromCache && ' (cache)'} {pendingCount > 0 && ( )}
{thirdparties?.map(t => ( {t.name} ))}
); } ``` ## Points clés à retenir 1. **useSyncClient** pour les opérations CRUD offline-capable 2. **useOnlineStatus** pour détecter la connectivité 3. **useCachedQuery** pour le cache intelligent avec stratégies 4. **useAuthenticatedImage** pour les images protégées 5. **ConflictResolver** pour la résolution de conflits UI 6. Configurer les stores IndexedDB pour le cache [[:15_training:module7-smartcommon-hooks:utilitaires|← Chapitre précédent]] | [[:15_training:module7-smartcommon-hooks:start|Retour au module]]