# 📜 Message Search, History Pagination, and Offline Sync **Date**: 2025-12-05 **Version**: 1.0.0 **Statut**: ✅ Implémenté --- ## 📋 Table des matières 1. [Vue d'ensemble](#vue-densemble) 2. [History Pagination](#history-pagination) 3. [Message Search](#message-search) 4. [Offline Sync](#offline-sync) 5. [Spécifications techniques](#spécifications-techniques) 6. [Exemples d'utilisation](#exemples-dutilisation) 7. [Limites et bonnes pratiques](#limites-et-bonnes-pratiques) 8. [Impact sur l'UI](#impact-sur-lui) --- ## 🎯 Vue d'ensemble Ce document décrit trois fonctionnalités majeures ajoutées au `veza-chat-server` : 1. **History Pagination** : Pagination efficace de l'historique avec cursors `before`/`after` 2. **Message Search** : Recherche textuelle de messages dans une conversation 3. **Offline Sync** : Synchronisation des messages manquants depuis la dernière connexion Toutes ces fonctionnalités sont : - ✅ Sécurisées (permissions strictes via `PermissionService`) - ✅ Performantes (index SQL optimisés) - ✅ Compatibles avec les statuts (edited, deleted, delivered, read) - ✅ Disponibles via WebSocket --- ## 📜 History Pagination ### Description Permet de récupérer l'historique d'une conversation avec pagination par cursors basés sur `created_at`. Plus efficace que l'offset/limit classique car : - Supporte les insertions concurrentes - Meilleure performance avec les index - Pas de problèmes de doublons lors de nouvelles insertions ### Inbound WebSocket Message ```json { "type": "FetchHistory", "conversation_id": "550e8400-e29b-41d4-a716-446655440000", "before": "2025-12-05T10:30:00Z", "after": null, "limit": 50 } ``` **Paramètres**: - `conversation_id` (UUID, requis) : ID de la conversation - `before` (DateTime ISO8601, optionnel) : Récupère les messages avant ce timestamp - `after` (DateTime ISO8601, optionnel) : Récupère les messages après ce timestamp - `limit` (usize, optionnel, défaut: 50, max: 100) : Nombre de messages à récupérer **Règles**: - Si `before` est fourni : tri DESC (messages plus anciens) - Si `after` est fourni : tri ASC (messages plus récents) - Si les deux sont fournis : messages entre `after` et `before` (tri ASC) - Si aucun n'est fourni : messages les plus récents (tri DESC) - Les résultats sont **toujours retournés en ordre ASC** (du plus ancien au plus récent) ### Outbound WebSocket Message ```json { "type": "HistoryChunk", "conversation_id": "550e8400-e29b-41d4-a716-446655440000", "messages": [ { "id": "...", "conversation_id": "...", "sender_id": "...", "content": "Hello world", "created_at": "2025-12-05T10:00:00Z", "is_edited": false, "is_deleted": false, ... } ], "has_more_before": true, "has_more_after": false } ``` **Champs**: - `messages` : Liste des messages (toujours triés ASC) - `has_more_before` : Indique s'il y a des messages plus anciens - `has_more_after` : Indique s'il y a des messages plus récents ### Exemples d'utilisation #### Charger les messages les plus récents ```json { "type": "FetchHistory", "conversation_id": "...", "before": null, "after": null, "limit": 50 } ``` #### Charger les messages plus anciens (scroll up) ```json { "type": "FetchHistory", "conversation_id": "...", "before": "2025-12-05T10:00:00Z", "after": null, "limit": 50 } ``` #### Charger les nouveaux messages (scroll down) ```json { "type": "FetchHistory", "conversation_id": "...", "before": null, "after": "2025-12-05T10:00:00Z", "limit": 50 } ``` ### Index SQL ```sql CREATE INDEX idx_messages_conv_created_at ON messages(conversation_id, created_at DESC); CREATE INDEX idx_messages_conv_created_not_deleted ON messages(conversation_id, created_at DESC) WHERE is_deleted = false; ``` --- ## 🔍 Message Search ### Description Recherche textuelle de messages dans une conversation. Utilise `ILIKE` avec index trigram pour une recherche performante et insensible à la casse. ### Inbound WebSocket Message ```json { "type": "SearchMessages", "conversation_id": "550e8400-e29b-41d4-a716-446655440000", "query": "hello world", "limit": 50, "offset": 0 } ``` **Paramètres**: - `conversation_id` (UUID, requis) : ID de la conversation - `query` (String, requis) : Terme de recherche (ne peut pas être vide) - `limit` (usize, optionnel, défaut: 50, max: 100) : Nombre de résultats par page - `offset` (usize, optionnel, défaut: 0) : Offset pour pagination ### Outbound WebSocket Message ```json { "type": "SearchResults", "conversation_id": "550e8400-e29b-41d4-a716-446655440000", "messages": [ { "id": "...", "content": "Hello world!", "created_at": "2025-12-05T10:00:00Z", ... } ], "query": "hello world", "total": 123 } ``` **Champs**: - `messages` : Liste des messages correspondants (triés par `created_at DESC`) - `query` : La requête de recherche originale - `total` : Nombre total de résultats (pour pagination) ### Exemples d'utilisation #### Recherche simple ```json { "type": "SearchMessages", "conversation_id": "...", "query": "meeting", "limit": 20, "offset": 0 } ``` #### Pagination des résultats ```json { "type": "SearchMessages", "conversation_id": "...", "query": "meeting", "limit": 20, "offset": 20 } ``` ### Index SQL ```sql CREATE EXTENSION IF NOT EXISTS pg_trgm; CREATE INDEX idx_messages_content_trgm ON messages USING GIN(content gin_trgm_ops); CREATE INDEX idx_messages_conv_content_trgm ON messages USING GIN(conversation_id, content gin_trgm_ops); ``` ### Comportement - ✅ Recherche insensible à la casse (`ILIKE`) - ✅ Recherche partielle (contient le terme) - ✅ Exclut les messages supprimés par défaut - ✅ Tri par `created_at DESC` (plus récents en premier) --- ## 🔄 Offline Sync ### Description Synchronise tous les messages manquants depuis la dernière connexion. Inclut : - Messages créés depuis `since` - Messages édités depuis `since` (même si créés avant) - Messages supprimés depuis `since` (même si créés avant) Permet aux clients mobiles d'avoir une synchronisation fiable après une déconnexion. ### Inbound WebSocket Message ```json { "type": "SyncMessages", "conversation_id": "550e8400-e29b-41d4-a716-446655440000", "since": "2025-12-05T09:00:00Z" } ``` **Paramètres**: - `conversation_id` (UUID, requis) : ID de la conversation - `since` (DateTime ISO8601, requis) : Timestamp de la dernière synchronisation ### Outbound WebSocket Message ```json { "type": "SyncChunk", "conversation_id": "550e8400-e29b-41d4-a716-446655440000", "messages": [ { "id": "...", "content": "New message", "created_at": "2025-12-05T10:00:00Z", "is_edited": false, "is_deleted": false, ... }, { "id": "...", "content": "Edited content", "created_at": "2025-12-05T08:00:00Z", "is_edited": true, "edited_at": "2025-12-05T10:30:00Z", ... }, { "id": "...", "content": "Deleted message", "created_at": "2025-12-05T08:30:00Z", "is_deleted": true, "deleted_at": "2025-12-05T10:45:00Z", ... } ], "last_sync": "2025-12-05T11:00:00Z" } ``` **Champs**: - `messages` : Tous les messages créés ou modifiés depuis `since` (triés par `created_at ASC`) - `last_sync` : Timestamp actuel (à utiliser pour la prochaine sync) ### Exemples d'utilisation #### Synchronisation initiale ```json { "type": "SyncMessages", "conversation_id": "...", "since": "2025-12-05T00:00:00Z" } ``` #### Synchronisation après déconnexion ```json { "type": "SyncMessages", "conversation_id": "...", "since": "2025-12-05T09:30:00Z" } ``` ### Index SQL ```sql CREATE INDEX idx_messages_conv_created_sync ON messages(conversation_id, created_at ASC) WHERE is_deleted = false; CREATE INDEX idx_messages_conv_updated_sync ON messages(conversation_id, updated_at ASC) WHERE is_deleted = false; ``` ### Comportement - ✅ Inclut tous les messages créés depuis `since` - ✅ Inclut tous les messages édités depuis `since` (même créés avant) - ✅ Inclut tous les messages supprimés depuis `since` (même créés avant) - ✅ Tri par `created_at ASC` (du plus ancien au plus récent) - ✅ Le client doit gérer les updates (édits) et deletes (suppressions) --- ## 🔧 Spécifications techniques ### Repository Methods #### `fetch_history` ```rust pub async fn fetch_history( &self, conversation_id: Uuid, before: Option>, after: Option>, limit: usize, include_deleted: bool, ) -> Result<(Vec, bool, bool)> ``` Retourne : `(messages, has_more_before, has_more_after)` #### `search_messages` ```rust pub async fn search_messages( &self, conversation_id: Uuid, query: &str, limit: usize, offset: usize, include_deleted: bool, ) -> Result<(Vec, i64)> ``` Retourne : `(messages, total_count)` #### `fetch_since` ```rust pub async fn fetch_since( &self, conversation_id: Uuid, since: DateTime, ) -> Result> ``` ### Permissions Toutes les fonctionnalités nécessitent : - `can_read_conversation(user_id, conversation_id)` : L'utilisateur doit avoir accès à la conversation ### Erreurs possibles - `ChatError::Unauthorized` : Pas de permission pour lire la conversation - `ChatError::ValidationError` : Query de recherche vide - `ChatError::InternalError` : Erreur de base de données --- ## 📱 Exemples d'utilisation ### Client Web (React) ```typescript // History Pagination const fetchHistory = async (conversationId: string, before?: Date) => { ws.send(JSON.stringify({ type: "FetchHistory", conversation_id: conversationId, before: before?.toISOString(), after: null, limit: 50 })); }; // Message Search const searchMessages = async (conversationId: string, query: string) => { ws.send(JSON.stringify({ type: "SearchMessages", conversation_id: conversationId, query: query, limit: 50, offset: 0 })); }; // Offline Sync const syncMessages = async (conversationId: string, lastSync: Date) => { ws.send(JSON.stringify({ type: "SyncMessages", conversation_id: conversationId, since: lastSync.toISOString() })); }; ``` ### Client Mobile (React Native) ```typescript // Sync après reconnexion const syncAfterReconnect = async (conversationId: string) => { const lastSync = await AsyncStorage.getItem(`last_sync_${conversationId}`); const since = lastSync ? new Date(lastSync) : new Date(0); ws.send(JSON.stringify({ type: "SyncMessages", conversation_id: conversationId, since: since.toISOString() })); // Écouter SyncChunk et mettre à jour last_sync ws.on('message', (msg) => { if (msg.type === 'SyncChunk') { AsyncStorage.setItem(`last_sync_${conversationId}`, msg.last_sync); // Mettre à jour l'UI avec les messages } }); }; ``` --- ## ⚠️ Limites et bonnes pratiques ### Limites 1. **History Pagination** : - `limit` max : 100 messages - Utiliser `before`/`after` plutôt que offset pour de meilleures performances 2. **Message Search** : - `limit` max : 100 résultats - `query` minimum : 1 caractère - Recherche partielle (contient), pas de recherche exacte 3. **Offline Sync** : - Pas de limite sur le nombre de messages (peut être volumineux) - Le client doit gérer les updates et deletes ### Bonnes pratiques 1. **History Pagination** : - Toujours utiliser `before` pour charger plus d'anciens messages - Utiliser `after` pour charger les nouveaux messages - Stocker le `created_at` du premier/dernier message pour la pagination 2. **Message Search** : - Implémenter un debounce sur la recherche (300-500ms) - Limiter la longueur minimale de la query (3 caractères recommandé) - Afficher un indicateur de chargement pendant la recherche 3. **Offline Sync** : - Stocker `last_sync` localement (AsyncStorage, localStorage) - Sync automatique après reconnexion - Gérer les conflits si un message est édité localement et sur le serveur --- ## 🎨 Impact sur l'UI ### History Pagination **Scroll infini vers le haut** : ```typescript const [messages, setMessages] = useState([]); const [hasMore, setHasMore] = useState(true); const loadMore = async () => { if (!hasMore) return; const oldestMessage = messages[0]; const before = oldestMessage?.created_at; fetchHistory(conversationId, before).then((chunk) => { setMessages([...chunk.messages, ...messages]); setHasMore(chunk.has_more_before); }); }; ``` ### Message Search **Barre de recherche avec résultats** : ```typescript const [searchQuery, setSearchQuery] = useState(""); const [searchResults, setSearchResults] = useState([]); const handleSearch = debounce((query: string) => { if (query.length < 3) return; searchMessages(conversationId, query).then((results) => { setSearchResults(results.messages); }); }, 300); ``` ### Offline Sync **Indicateur de synchronisation** : ```typescript const [isSyncing, setIsSyncing] = useState(false); const sync = async () => { setIsSyncing(true); const lastSync = await getLastSync(conversationId); syncMessages(conversationId, lastSync); // setIsSyncing(false) dans le handler SyncChunk }; ``` --- ## 📊 Performance ### Index utilisés - `idx_messages_conv_created_at` : Pagination efficace - `idx_messages_content_trgm` : Recherche textuelle rapide - `idx_messages_conv_created_sync` : Sync optimisée ### Métriques attendues - **History Pagination** : < 50ms pour 50 messages - **Message Search** : < 100ms pour 1000 messages - **Offline Sync** : < 200ms pour 100 messages --- ## 🔐 Sécurité - ✅ Toutes les fonctionnalités vérifient les permissions via `PermissionService` - ✅ Les messages supprimés sont exclus par défaut (sauf si `include_deleted = true`) - ✅ Validation des paramètres (query non vide, limit max, etc.) - ✅ Pas d'injection SQL (utilisation de paramètres liés) --- ## 📝 Migration Pour activer ces fonctionnalités, exécuter : ```bash psql -d veza_db -f migrations/006_history_search_sync.sql ``` Cette migration crée tous les index nécessaires. --- **Fin du document**