---
title: "Synchronisation - catalogue de référence"
weight: 195
---

# Synchronisation - catalogue de référence

`useReferenceSync` embarque hors connexion un référentiel que l'application
**consulte sans le modifier** : produits, catégories, tiers, contacts, ainsi que
leurs images et documents PDF.

Il complète [`useSyncClient`](/front/synchronisation), qui lui est
transactionnel et travaille dans sa propre base. Les deux se combinent souvent
dans la même application : le catalogue descend par `useReferenceSync`, les
écritures métier remontent par `useSyncClient` ou par une file d'attente métier.

## En quoi il diffère de useSyncClient

| | `useSyncClient` | `useReferenceSync` |
| --- | --- | --- |
| Sens | Bidirectionnel | Descendant seulement |
| Stockage | Base dédiée `smartauth_sync` | **Vos** stores Dexie |
| Lecture par l'application | `getEntity`, `queryEntities` | Requêtes Dexie directes sur vos stores |
| Conflits | Détectés et résolus | Sans objet, rien ne remonte |
| Fichiers joints | Non gérés | Images et PDF, téléchargés en lots ZIP |
| Enregistrement client | `register(deviceUuid)` explicite | Automatique et idempotent à chaque passe |

L'avantage décisif du second est que vos écrans continuent d'interroger vos
propres tables, avec vos index et vos requêtes. Rien ne change dans le code de
lecture quand vous passez une liste en mode hors connexion.

## Mise en route

```javascript
import { useReferenceSync } from '@cap-rel/smartcommon';
import { useMyModuleDb } from 'src/db';
import { APP_VERSION } from 'src/utils/constants/vite';

const ENTITIES = [
    { objectType: 'product', store: 'products' },
    { objectType: 'category', store: 'categories', cleanOrphans: true }
];

const DOCUMENTS = [
    {
        objectType: 'product',
        store: 'productDocuments',
        fk: 'product_id',
        doctypes: ['image', 'pdf']
    }
];

export const useCatalogSync = () => {
    const db = useMyModuleDb();

    return useReferenceSync({
        db,
        appVersion: APP_VERSION,
        entities: ENTITIES,
        documents: DOCUMENTS,
        metaStore: 'syncMeta'
    });
};
```

Le store de métadonnées doit exister dans votre schéma Dexie, sous la forme
d'un simple couple clé / valeur :

```javascript
const db = useDb({
    name: 'myModule',
    version: 1,
    stores: {
        products: 'id, ref, label',
        categories: 'id, label',
        productDocuments: '++local_id, product_id, server_id, type',
        syncMeta: 'key'
    }
});
```

## Options

| Option | Type | Défaut | Description |
| --- | --- | --- | --- |
| `db` | object | - | Instance Dexie du module |
| `appVersion` | string | `'1.0.0'` | Version envoyée à `sync/register`, utile pour le diagnostic serveur |
| `entities` | array | `[]` | Types d'objets à tirer, voir ci-dessous |
| `documents` | array | `[]` | Documents à télécharger, voir ci-dessous |
| `dataFeeds` | array | `[]` | Dictionnaires et blocs de configuration, voir ci-dessous |
| `metaStore` | string | `'syncMeta'` | Store clé / valeur pour `clientUuid` et les marqueurs de delta |
| `getSyncPreferences` | function | `null` | `async () => prefs`, passée aux résolveurs `enabled` et `doctypes` |
| `onProgress` | function | `null` | Miroir de `syncProgress`, pour une barre de progression externe |

### entities

```javascript
{ objectType: 'product', store: 'products', mapper: mapProduct, cleanOrphans: false }
```

| Clé | Description |
| --- | --- |
| `objectType` | Type d'objet côté SmartAuth, tel que déclaré dans le registre |
| `store` | Nom du store Dexie de destination |
| `mapper` | Optionnel, `(raw) => mapped`, renommage ou filtrage de champs avant écriture |
| `cleanOrphans` | Optionnel, `false` par défaut. À `true`, force un tirage complet et supprime en local tout ce que le serveur n'a pas renvoyé |

Chaque type est tiré par pages de 500 via `GET sync/pull`, avec un marqueur de
delta propre stocké sous la clé `lastSyncAt_<objectType>`. Ce marqueur n'est
écrit **qu'une fois toutes les pages passées** : une coupure en cours de route
fait reprendre la passe entière plutôt que de laisser un trou.

> `cleanOrphans` coûte un tirage complet à chaque synchronisation. Ne l'activez
> que sur les petits référentiels, et seulement si le serveur ne publie pas
> déjà ses exclusions dans la liste des suppressions.

### documents

```javascript
{
    objectType: 'product',
    store: 'productDocuments',
    fk: 'product_id',
    doctypes: (prefs) => [prefs.syncImages && 'image', prefs.syncPdfs && 'pdf'].filter(Boolean),
    enabled: (prefs) => prefs.syncProductDocuments
}
```

| Clé | Description |
| --- | --- |
| `objectType` | Type **natif** attendu par le contrôleur de documents de SmartAuth (`product`, `category`, `thirdparty`, `project`, `intervention`) |
| `store` | Store Dexie recevant les blobs |
| `fk` | Nom de la colonne portant l'identifiant de l'objet |
| `doctypes` | Tableau, ou fonction des préférences, parmi `image`, `thumb`, `pdf` |
| `enabled` | Booléen, ou fonction des préférences. Permet de rendre le téléchargement optionnel pour l'utilisateur |

Attention à un piège : un module peut enregistrer ses **propres** types
syncables côté serveur, par exemple `capfullpos_product` pour filtrer le
catalogue vendable. Ces types valent pour `entities`, mais le contrôleur de
documents, lui, ne connaît que les types natifs. Les deux listes ne se
recopient donc pas l'une l'autre.

Les blobs sont récupérés en **lots ZIP** plutôt qu'un par un, avec repli sur un
téléchargement individuel pour les fichiers trop volumineux. C'est ce qui évite
d'inonder le serveur d'une requête par vignette à l'ouverture d'une session.

Ligne écrite en local :

```javascript
{
    local_id,            // auto-incrémenté
    [fk]: object_id,     // product_id, category_id...
    server_id, type, filename, relative_path, mime_type,
    blob, size, synced_at, server_updated_at
}
```

Le doctype `thumb`, quand le serveur l'autorise, ne renvoie que la vignette au
lieu de l'original pleine résolution. Sur une grille de tuiles, la différence de
poids embarqué est considérable.

### dataFeeds

Pour les dictionnaires et blocs de configuration, qui ne passent pas par le
moteur de synchronisation mais par un simple GET :

```javascript
{ key: 'paymentModes', endpoint: 'syncdata/payment-modes', store: 'paymentModes', clearBefore: true }
```

| Clé | Description |
| --- | --- |
| `key` | Identifiant du flux, sert aussi de clé de ligne pour un objet unique |
| `endpoint` | Chemin appelé sur l'API privée |
| `store` | Store Dexie de destination |
| `mapper` | Optionnel, appliqué à chaque élément |
| `extract` | Optionnel, `(res) => payload`. Par défaut `res.data` |
| `clearBefore` | Optionnel, vide le store avant insertion, pour un remplacement complet |

## Valeurs retournées

| Propriété | Type | Description |
| --- | --- | --- |
| `isSyncing` | boolean | Passe en cours |
| `syncProgress` | object/null | `{ step, current, total }`, `step` valant le nom du store traité |
| `lastSyncAt` | Date/null | Date de la dernière passe complète |
| `error` | Error/null | Dernière erreur |
| `isInitialized` | boolean | Base fournie et prête |
| `hasApi` | boolean | Contexte API disponible |
| `syncNow` | function | Lance une passe |
| `resetSync` | function | Vide tous les stores configurés puis relance une passe complète |

## Déroulement d'une passe

```
1. Enregistrement du client (idempotent) : POST sync/register
2. Entités, dans l'ordre déclaré : GET sync/pull paginé
3. Documents : métadonnées, comparaison locale, téléchargement des lots ZIP
4. Flux de données : un GET par flux
5. Écriture du marqueur lastSyncAt
```

Une erreur sur une étape est enregistrée dans le résultat et **n'interrompt
pas** les suivantes. Deux exceptions arrêtent tout immédiatement :

- une annulation, déclenchée par `resetSync` pendant une passe ;
- un **403**, qui lève une `ForbiddenSyncError`. L'arrêt est immédiat et
  volontaire : insister sur une série de requêtes refusées fait blacklister
  l'application par les pare-feux applicatifs.

Une passe refuse par ailleurs de démarrer hors connexion, et refuse de se
superposer à une passe déjà en cours.

## Afficher la progression

```javascript
const CatalogSyncOverlay = () => {
    const { isSyncing, syncProgress, lastSyncAt, error, syncNow } = useCatalogSync();

    if (!isSyncing) {
        return <button onClick={syncNow}>Mettre à jour le catalogue</button>;
    }

    return (
        <div>
            <p>{syncProgress?.step ?? 'Préparation'}</p>
            {syncProgress?.total > 0 && (
                <progress value={syncProgress.current} max={syncProgress.total} />
            )}
        </div>
    );
};
```

`syncProgress` repasse à `null` en fin de passe. Sur les entités, `current` et
`total` valent tous deux le nombre d'éléments déjà écrits : la progression est
une volumétrie qui monte, pas un pourcentage, parce que le total n'est pas connu
avant la dernière page.

## Piège : lire pendant une première synchronisation

Les marqueurs `lastSyncAt_<objectType>` ne sont écrits qu'après succès complet.
Un écran qui teste la présence du marqueur pour décider s'il peut lire en local
affichera donc une liste vide pendant toute la première passe, alors même que
des lignes arrivent au fil de l'eau. Testez le contenu du store, pas le
marqueur.

## Voir aussi

- [Synchronisation offline](/front/synchronisation) - choisir son dispositif
- [Stockage de données](/front/stockage-de-donnees) - `useDb` et les stores Dexie
- [Requêtes API](/front/requetes-api) - `useApi`
- [Mapping Dolibarr - React](/back/mapping-dolibarr-react) - déclarer un objet syncable
