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.).
593 lines
14 KiB
Markdown
593 lines
14 KiB
Markdown
# 📜 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<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`
|
|
```rust
|
|
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`
|
|
```rust
|
|
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)
|
|
|
|
```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<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** :
|
|
```typescript
|
|
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** :
|
|
```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**
|
|
|