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