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.).
14 KiB
📜 Message Search, History Pagination, and Offline Sync
Date: 2025-12-05
Version: 1.0.0
Statut: ✅ Implémenté
📋 Table des matières
- Vue d'ensemble
- History Pagination
- Message Search
- Offline Sync
- Spécifications techniques
- Exemples d'utilisation
- Limites et bonnes pratiques
- Impact sur l'UI
🎯 Vue d'ensemble
Ce document décrit trois fonctionnalités majeures ajoutées au veza-chat-server :
- History Pagination : Pagination efficace de l'historique avec cursors
before/after - Message Search : Recherche textuelle de messages dans une conversation
- 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 conversationbefore(DateTime ISO8601, optionnel) : Récupère les messages avant ce timestampafter(DateTime ISO8601, optionnel) : Récupère les messages après ce timestamplimit(usize, optionnel, défaut: 50, max: 100) : Nombre de messages à récupérer
Règles:
- Si
beforeest fourni : tri DESC (messages plus anciens) - Si
afterest fourni : tri ASC (messages plus récents) - Si les deux sont fournis : messages entre
afteretbefore(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 ancienshas_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;
🔍 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
{
"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 conversationquery(String, requis) : Terme de recherche (ne peut pas être vide)limit(usize, optionnel, défaut: 50, max: 100) : Nombre de résultats par pageoffset(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 parcreated_at DESC)query: La requête de recherche originaletotal: 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 conversationsince(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 depuissince(triés parcreated_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 conversationChatError::ValidationError: Query de recherche videChatError::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
-
History Pagination :
limitmax : 100 messages- Utiliser
before/afterplutôt que offset pour de meilleures performances
-
Message Search :
limitmax : 100 résultatsqueryminimum : 1 caractère- Recherche partielle (contient), pas de recherche exacte
-
Offline Sync :
- Pas de limite sur le nombre de messages (peut être volumineux)
- Le client doit gérer les updates et deletes
Bonnes pratiques
-
History Pagination :
- Toujours utiliser
beforepour charger plus d'anciens messages - Utiliser
afterpour charger les nouveaux messages - Stocker le
created_atdu premier/dernier message pour la pagination
- Toujours utiliser
-
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
-
Offline Sync :
- Stocker
last_synclocalement (AsyncStorage, localStorage) - Sync automatique après reconnexion
- Gérer les conflits si un message est édité localement et sur le serveur
- Stocker
🎨 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 efficaceidx_messages_content_trgm: Recherche textuelle rapideidx_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