---
title: "Chapitre 4 : Synchronisation"
weight: 610
---

# 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 (
        <div>
            <p>Status : {isOnline ? 'En ligne' : 'Hors ligne'}</p>
            <p>Modifications en attente : {pendingCount}</p>
        </div>
    );
}
```

### 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 (
        <form onSubmit={handleSubmit}>
            {/* ... */}
            <p>En attente de sync : {pendingCount}</p>
        </form>
    );
}
```

### 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 (
        <div>
            <button onClick={handleUpdate}>Modifier</button>
            <button onClick={handleDelete}>Supprimer</button>
        </div>
    );
}
```

### 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 (
        <button
            onClick={handleSync}
            disabled={isSyncing || !isOnline || pendingCount === 0}
        >
            {isSyncing ? 'Synchronisation...' : `Synchroniser (${pendingCount})`}
        </button>
    );
}
```

## 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 <p>Aucun conflit</p>;
    }

    return (
        <ConflictResolver
            conflicts={conflicts}
            onResolve={handleResolve}
        />
    );
}
```

### 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 (
        <div className={isOffline ? 'bg-red-500' : 'bg-green-500'}>
            {isOnline ? 'En ligne' : 'Hors ligne'}
        </div>
    );
}
```

### 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 (
        <div>
            <p>Navigateur : {isOnline ? 'En ligne' : 'Hors ligne'}</p>
            <p>Serveur : {isServerReachable ? 'Accessible' : 'Inaccessible'}</p>
            <p>Dernière vérification : {new Date(lastCheck).toLocaleTimeString()}</p>
            <button onClick={checkNow}>Vérifier maintenant</button>
        </div>
    );
}
```

### 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 <Spinner />;

    return (
        <select>
            {countries.map(c => (
                <option key={c.code} value={c.code}>{c.label}</option>
            ))}
        </select>
    );
}
```

### 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 (
        <div>
            {isStale && <p>Mise à jour en cours...</p>}
            <button onClick={invalidate}>Forcer le rafraîchissement</button>
        </div>
    );
}
```

### 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 <Spinner />;

    return <img src={src} alt="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 (
        <Page title="Tiers">
            <Block>
                <div className="flex justify-between items-center">
                    <span>
                        {isOnline ? 'En ligne' : 'Hors ligne'}
                        {isFromCache && ' (cache)'}
                    </span>
                    {pendingCount > 0 && (
                        <Button
                            onClick={sync}
                            disabled={!isOnline || isSyncing}
                        >
                            Sync ({pendingCount})
                        </Button>
                    )}
                </div>
            </Block>

            <Block>
                <List>
                    {thirdparties?.map(t => (
                        <ListItem key={t.id}>
                            {t.name}
                        </ListItem>
                    ))}
                </List>
            </Block>
        </Page>
    );
}
```

## 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

[← Chapitre précédent](/training/module7-smartcommon-hooks/utilitaires) | [Retour au module](/training/module7-smartcommon-hooks)
