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

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 :

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

{ 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

{
    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 :

{
    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 :

{ 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

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