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.
cleanOrphanscoû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
resetSyncpendant 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
- Synchronisation offline - choisir son dispositif
- Stockage de données -
useDbet les stores Dexie - Requêtes API -
useApi - Mapping Dolibarr - React - déclarer un objet syncable