11 KiB
sessionId
| sessionId |
|---|
| session-261018-151548-e6aw |
Requirements
Overview & Goals
Quand un nouvel utilisateur rejoint un serveur, il doit voir l'historique des messages des canaux texte du serveur, et recevoir en temps réel les nouveaux messages. Aujourd'hui, certains membres n'ont pas de cache de permissions calculées, ce qui casse la diffusion temps réel et masque les erreurs côté client.
Scope
In Scope
- Diagnostiquer précisément pourquoi un membre récent ne voit pas l'historique.
- Corriger le flux d'arrivée sur un serveur (
join_server) pour que les permissions soient immédiatement disponibles. - Réparer les membres existants qui n'ont aucune permission calculée.
- Rendre visibles les échecs de chargement de l'historique côté frontend.
Out of Scope
- Changer le modèle de rôles ou de permissions par canal.
- Ajouter de nouvelles permissions par défaut configurables par l'utilisateur.
- Refonte du système d'événements de l'EventBus.
User Stories
- En tant que nouveau membre, je veux voir les anciens messages d'un canal dès que je le rejoins, afin de suivre la conversation.
- En tant que membre, je veux recevoir les nouveaux messages en direct sans recharger la page.
- En tant qu'administrateur, je veux que les membres existants soient réparés sans manipulation manuelle de la base.
Functional Requirements
- Après
join_server, un membre standard peut lire (READ_CHANNEL) tous les canaux texte du serveur viaGET /messages. - Après
join_server, le membre reçoit les événements temps réel de messages des canaux du serveur. - Les membres existants sans cache (5 sur le serveur principal dans la base locale) sont réparés au démarrage ou par une opération de réparation.
- Si le chargement de l'historique échoue, l'interface affiche un message d'erreur au lieu d'un canal vide silencieux.
Non-Functional Requirements
- L'opération de join doit rester transactionnelle et ne pas bloquer la réponse HTTP plus que nécessaire.
- La réparation doit être idempotente (relançable sans effet de bord).
Technical Design
Current Implementation
Lecture de l'historique (chemin REST)
src/routes/message/handlers.rsget_allexigeREAD_CHANNELviarequire_channel_permission.src/http/permissions.rscheck_channel_permission(l.192-218) calcule les droits en direct à partir deserver_user,role_user,channel_role_permissionetchannel_user_permission. Pour un canal de serveur, la base de bits part de0;PermissionSet::DEFAULT(src/permissions.rsl.96-105) n'est appliqué qu'aux canaux sans serveur.- Le rôle par défaut « Member » reçoit
READ_CHANNELsur chaque canal créé viasrc/services/channel.rscreate_channel(l.69-86). Dans la base locale, chaque rôle par défaut a une permission par canal (11/11 et 4/4), et tous les membres ont ce rôle. Le contrôle REST passe donc pour les membres actuels.
Arrivée d'un membre
src/services/server.rsjoin_server(l.77-137) insèreserver_useretrole_userpuis émetServerUserCreatedEventaprès commit. Il n'appelle pas directement la synchronisation du cache.src/services/permission_sync.rsstart_listen_event(l.85+) écouteServerUserCreatedEvent(l.100) et appellecomputed_permission.full_sync_userde façon asynchrone.src/repositories/computed_permission.rsfull_sync_user(l.108+) recalcule les lignescomputed_permissiondu membre.
Diffusion temps réel
src/services/realtime_registry.rsconstruit l'audience des canaux (refresh_channell.25-57,initializel.76+) à partir de la tablecomputed_permission, uniquement pour les lignes portantREAD_CHANNEL.src/routes/gateway/mod.rs(l.141-293) envoie tous les événements de messages et de canaux viausers_for_channel. Un membre absent de ce registre ne reçoit rien en direct.
Constat dans la base locale (lecture seule, oxspeak.db)
- Serveur
B3D2…(9 membres) : 4 membres ont 12 lignes de cache (11 canaux + niveau serveur), 5 membres ont 0 ligne. - Serveur
F142…(2 membres) : un membre a 5 lignes, l'autre 0 ligne. - Le serveur
B3D2…contient des messages (ex. canalTest: 11 messages), donc le problème n'est pas une absence de données.
Affichage côté client
frontend/src/stores/message.tsrequestPage(l.94-112) lève une erreur sur une réponse non OK, maisfetchMessages(l.148-175) ne fait queconsole.error. Un 403 apparaît donc comme un canal vide.
Key Decisions
Décision 1 : Source de vérité de l'audience temps réel
- Option A (recommandée) : garder
computed_permissioncomme cache, mais le rafraîchir de façon synchrone dansjoin_server(appel àfull_sync_userpuisrealtime_registry.refresh_user) avant de répondre. - Option B : rendre l'audience temps réel dépendante de
check_channel_permission(plus coûteux, à évaluer sur des serveurs volumineux).
Décision 2 : Réparation des membres existants
- Option A (recommandée) : au démarrage, lancer
full_sync_serverpour chaque serveur (idempotent, coût acceptable à faible volume). - Option B : une route d'administration ou une commande ponctuelle de réparation.
Décision 3 : Erreur côté frontend
- Afficher un état d'erreur dans
fetchMessagesau lieu d'un canal vide. Ce n'est pas une correction de la cause, mais elle rend le problème visible.
Proposed Changes
services/server.rsjoin_server: après commit, synchroniser le cache du membre et rafraîchir le registre temps réel, en conservant l'émission deServerUserCreatedEvent.services/permission_sync.rs: journaliser les échecs du listenersync_user(aujourd'hui silencieux) pour expliquer les membres restés sans cache.- Démarrage (
core/mod.rs, aprèsrealtime_registry.initialize) : appeler une réparation idempotentefull_sync_serverpour chaque serveur. frontend/src/stores/message.tsfetchMessages: exposer une erreur de chargement et l'afficher.
Architecture Diagram
graph LR
J[join_server] --> DB[(server_user, role_user)]
J --> S[full_sync_user]
S --> C[(computed_permission)]
S --> R[realtime_registry]
R --> G[gateway: message events]
F[GET /messages] --> P[check_channel_permission]
P --> DB
Risks
- Le listener asynchrone peut échouer silencieusement : la cause exacte des 5 membres sans cache doit être confirmée (journaux, rejeu).
- La réparation au démarrage peut être longue sur de gros serveurs : la limiter aux serveurs dont un membre n'a pas de cache.
- Le symptôme exact « ne voit pas les anciens messages » n'a pas encore été reproduit à l'interface ; l'historique REST semble autorisé pour les membres actuels. Il faut reproduire avec un compte réellement concerné avant de conclure.
- Verrou
lock_scopedefull_sync_user: éviter les appels imbriqués pendant la transaction de join.
Testing
Validation Approach
- Reproduire le problème par un test d'intégration qui crée un serveur, ajoute un canal avec des messages, fait rejoindre un nouvel utilisateur via
join_server, puis vérifieGET /messageset la présence dans le registre temps réel. - Utiliser les helpers existants de
src/http/test_support.rset le style des tests desrc/routes/channel/tests.rs.
Key Scenarios
- Un nouvel membre rejoint un serveur ayant des canaux et des messages :
GET /messagesrenvoie les messages,computed_permissioncontient une ligneREAD_CHANNELpar canal. - Après join,
users_for_channel(channel_id)contient le nouveau membre pour chaque canal texte. - La réparation au démarrage ajoute les lignes manquantes pour les 5 membres sans cache et ne modifie pas les autres.
- Côté frontend, un 403 sur
/messagesaffiche un message d'erreur au lieu d'un canal vide.
Edge Cases
- Membre qui rejoint un serveur déjà joint (chemin « déjà membre » de
join_server) : pas de doublon, cache inchangé. - Canal créé avant l'existence du rôle par défaut : vérifier la présence de
channel_role_permission. - Relance de la réparation : idempotente.
- Mauvais mot de passe de serveur : aucune ligne créée, aucun événement émis.
- Régression : les DM (canaux sans serveur) continuent d'utiliser
channel_userpour leur audience.
Test Changes
- Ajouter un test de join dans le module de tests des routes message (à créer à côté de
src/routes/message/handlers.rs). - Ajouter un test unitaire sur
full_sync_serveridempotent danssrc/repositories/computed_permission.rs. - Ne pas modifier les tests existants de
realtime_registry.rs.
Delivery Steps
✓ Step 1: Reproduire et confirmer la cause avec un test de join
Un test d'intégration reproduit le cas « nouveau membre sans historique » et confirme la cause côté cache et audience temps réel.
- Écrire un test qui crée un serveur et un canal texte avec des messages, puis fait rejoindre un nouvel utilisateur via
join_server. - Vérifier
GET /messages(handlerget_alldanssrc/routes/message/handlers.rs) et l'absence éventuelle de lignescomputed_permission. - Vérifier la présence du membre dans
RealtimeRegistry::users_for_channel. - Consigner dans les journaux pourquoi le listener
sync_userdesrc/services/permission_sync.rsn'a pas rempli le cache.
✓ Step 2: Synchroniser les permissions dès l'arrivée d'un membre
Un membre qui rejoint un serveur dispose immédiatement de son cache de permissions et de sa place dans le registre temps réel.
- Dans
join_server(src/services/server.rs), après commit, appelercomputed_permission.full_sync_userpour le nouveau membre. - Appeler
realtime_registry.refresh_userpour mettre à jour les canaux de l'utilisateur. - Garder l'émission de
ServerUserCreatedEventpour les autres abonnés. - Journaliser l'échec de synchronisation au lieu de l'ignorer dans le listener
sync_user.
✓ Step 3: Réparer les membres existants au démarrage
Les membres déjà présents sans cache de permissions récupèrent leurs lignes au prochain démarrage du serveur.
- Ajouter une réparation idempotente qui appelle
full_sync_serverpour chaque serveur dont un membre n'a pas de lignecomputed_permission. - Brancher la réparation dans
src/core/mod.rsaprèsrealtime_registry.initialize. - Ajouter un test unitaire vérifiant que deux exécutions successives ne modifient pas les lignes.
✓ Step 4: Afficher les erreurs de chargement de l'historique dans le frontend
Un échec de chargement de l'historique est visible par l'utilisateur au lieu d'un canal vide silencieux.
- Dans
frontend/src/stores/message.ts,fetchMessagesexpose l'état d'erreur (par exempleloadError) au lieu de seulementconsole.error. - Afficher un message d'erreur avec un bouton de nouvel essai dans le composant de liste de messages.
- Ne pas modifier le comportement de
fetchBeforeetfetchAfter.