Offline Synchronization
SmartCommon provides a complete synchronization module to manage the offline functionality of PWA applications.
Architecture
The sync module consists of:
| Element | Type | Description |
|---|---|---|
useSyncClient |
React Hook | Main interface for synchronization |
SyncEngine |
Class | Synchronization engine (push/pull/conflicts) |
SyncStorage |
Class | IndexedDB layer for local storage (Dexie) |
SyncApi |
Class | HTTP client with JWT auth and automatic retry |
ConflictResolver |
React Component | Conflict resolution interface |
useSyncClient
See Hooks - useSyncClient for complete hook documentation.
Synchronization Flow
Push (local to server)
1. User modifies data locally (create/update/remove)
2. Changes are stored in IndexedDB (pending_changes)
3. On next sync.push(), changes are sent to the server
4. Server confirms or reports conflicts
5. Local temp_id are replaced with server IDs
Pull (server to local)
1. sync.pull() requests changes since lastSyncTime
2. Server returns modified entities
3. Local entities are updated
4. In case of conflict (local + server modification), a conflict is created
Conflict Management
When an entity is modified both locally and on the server, a conflict is created.
Programmatic Resolution
const conflicts = await sync.getConflicts();
for (const conflict of conflicts) {
// Keep client version
await sync.resolveConflict(conflict.conflict_id, 'client');
// Keep server version
await sync.resolveConflict(conflict.conflict_id, 'server');
// Manually merge
await sync.resolveConflict(conflict.conflict_id, {
...conflict.server_data,
label: conflict.client_data.label // keep local label
});
}
Resolution with ConflictResolver
import { ConflictResolver } from '@cap-rel/smartcommon';
const MyConflictPage = () => {
const sync = useSyncClient({ /* ... */ });
const [conflicts, setConflicts] = useState([]);
useEffect(() => {
sync.getConflicts().then(setConflicts);
}, []);
if (conflicts.length === 0) return null;
return (
<ConflictResolver
conflicts={conflicts}
onResolve={async (conflictId, resolution) => {
await sync.resolveConflict(conflictId, resolution);
setConflicts(prev => prev.filter(c => c.conflict_id !== conflictId));
}}
onCancel={() => setConflicts([])}
/>
);
};
The ConflictResolver component displays:
- Side-by-side comparison of client and server data
- Markers on conflicting fields
- Three options: keep client, keep server, merge field by field
- Navigation between multiple conflicts
ConflictResolver Props
| Prop | Type | Description |
|---|---|---|
conflicts |
array | Array of conflicts (conflict_id, table, object_id, client_data, server_data, field_conflicts) |
onResolve |
function | Called when a conflict is resolved |
onCancel |
function | Called to close the resolver |
renderField |
function | Custom rendering of a field (optional) |
labels |
object | UI labels (optional, French by default) |
IndexedDB Schema
The sync module uses the following stores:
| Store | Description |
|---|---|
entities |
Synchronized data |
pending_changes |
Local changes waiting for push |
pending_conflicts |
Unresolved conflicts |
sync_meta |
Synchronization metadata (lastSyncTime, etc.) |
local_tombstones |
Locally deleted entities |
See Also
- SmartCommon Hooks - All hooks
- PWA - PWA Configuration
- Data Storage - Local storage