veza/veza-chat-server/docs/CHAT_HISTORY_SEARCH_SYNC.md
okinrev b7955a680c P0: stabilisation backend/chat/stream + nouvelle base migrations v1
Backend Go:
- Remplacement complet des anciennes migrations par la base V1 alignée sur ORIGIN.
- Durcissement global du parsing JSON (BindAndValidateJSON + RespondWithAppError).
- Sécurisation de config.go, CORS, statuts de santé et monitoring.
- Implémentation des transactions P0 (RBAC, duplication de playlists, social toggles).
- Ajout d’un job worker structuré (emails, analytics, thumbnails) + tests associés.
- Nouvelle doc backend : AUDIT_CONFIG, BACKEND_CONFIG, AUTH_PASSWORD_RESET, JOB_WORKER_*.

Chat server (Rust):
- Refonte du pipeline JWT + sécurité, audit et rate limiting avancé.
- Implémentation complète du cycle de message (read receipts, delivered, edit/delete, typing).
- Nettoyage des panics, gestion d’erreurs robuste, logs structurés.
- Migrations chat alignées sur le schéma UUID et nouvelles features.

Stream server (Rust):
- Refonte du moteur de streaming (encoding pipeline + HLS) et des modules core.
- Transactions P0 pour les jobs et segments, garanties d’atomicité.
- Documentation détaillée de la pipeline (AUDIT_STREAM_*, DESIGN_STREAM_PIPELINE, TRANSACTIONS_P0_IMPLEMENTATION).

Documentation & audits:
- TRIAGE.md et AUDIT_STABILITY.md à jour avec l’état réel des 3 services.
- Cartographie complète des migrations et des transactions (DB_MIGRATIONS_*, DB_TRANSACTION_PLAN, AUDIT_DB_TRANSACTIONS, TRANSACTION_TESTS_PHASE3).
- Scripts de reset et de cleanup pour la lab DB et la V1.

Ce commit fige l’ensemble du travail de stabilisation P0 (UUID, backend, chat et stream) avant les phases suivantes (Coherence Guardian, WS hardening, etc.).
2025-12-06 11:14:38 +01:00

14 KiB

📜 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
  2. History Pagination
  3. Message Search
  4. Offline Sync
  5. Spécifications techniques
  6. Exemples d'utilisation
  7. Limites et bonnes pratiques
  8. Impact sur l'UI

🎯 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

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

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

{
  "type": "FetchHistory",
  "conversation_id": "...",
  "before": null,
  "after": null,
  "limit": 50
}

Charger les messages plus anciens (scroll up)

{
  "type": "FetchHistory",
  "conversation_id": "...",
  "before": "2025-12-05T10:00:00Z",
  "after": null,
  "limit": 50
}

Charger les nouveaux messages (scroll down)

{
  "type": "FetchHistory",
  "conversation_id": "...",
  "before": null,
  "after": "2025-12-05T10:00:00Z",
  "limit": 50
}

Index 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;

Description

Recherche textuelle de messages dans une conversation. Utilise ILIKE avec index trigram pour une recherche performante et insensible à la casse.

Inbound WebSocket Message

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

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

{
  "type": "SearchMessages",
  "conversation_id": "...",
  "query": "meeting",
  "limit": 20,
  "offset": 0
}

Pagination des résultats

{
  "type": "SearchMessages",
  "conversation_id": "...",
  "query": "meeting",
  "limit": 20,
  "offset": 20
}

Index 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

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

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

{
  "type": "SyncMessages",
  "conversation_id": "...",
  "since": "2025-12-05T00:00:00Z"
}

Synchronisation après déconnexion

{
  "type": "SyncMessages",
  "conversation_id": "...",
  "since": "2025-12-05T09:30:00Z"
}

Index 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

pub async fn fetch_history(
    &self,
    conversation_id: Uuid,
    before: Option<DateTime<Utc>>,
    after: Option<DateTime<Utc>>,
    limit: usize,
    include_deleted: bool,
) -> Result<(Vec<Message>, bool, bool)>

Retourne : (messages, has_more_before, has_more_after)

search_messages

pub async fn search_messages(
    &self,
    conversation_id: Uuid,
    query: &str,
    limit: usize,
    offset: usize,
    include_deleted: bool,
) -> Result<(Vec<Message>, i64)>

Retourne : (messages, total_count)

fetch_since

pub async fn fetch_since(
    &self,
    conversation_id: Uuid,
    since: DateTime<Utc>,
) -> Result<Vec<Message>>

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)

// 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)

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

const [messages, setMessages] = useState<Message[]>([]);
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 :

const [searchQuery, setSearchQuery] = useState("");
const [searchResults, setSearchResults] = useState<Message[]>([]);

const handleSearch = debounce((query: string) => {
  if (query.length < 3) return;
  
  searchMessages(conversationId, query).then((results) => {
    setSearchResults(results.messages);
  });
}, 300);

Offline Sync

Indicateur de synchronisation :

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 :

psql -d veza_db -f migrations/006_history_search_sync.sql

Cette migration crée tous les index nécessaires.


Fin du document