## Context L'application charge les collaborateurs de session via `resolveCollaborator` appelé en séquence dans une boucle (N+1). Le hook `useLive` déclenche `router.refresh()` sur chaque événement SSE reçu, sans groupement, causant des re-renders en cascade si plusieurs événements arrivent simultanément. La fonction `cleanupOldEvents` existe dans `session-share-events.ts` mais n'est jamais appelée, laissant les événements s'accumuler indéfiniment. L'absence de `loading.tsx` sur les routes principales empêche le streaming App Router de s'activer. Les modals (`ShareModal`) sont inclus dans le bundle initial alors qu'ils sont rarement utilisés. ## Goals / Non-Goals **Goals:** - Éliminer le N+1 sur `resolveCollaborator` avec un fetch batché - Grouper les refreshes SSE consécutifs avec un debounce - Purger les événements SSE au fil de l'eau (après chaque `createEvent`) - Activer le streaming de navigation avec `loading.tsx` sur les routes à chargement lent - Réduire le bundle JS initial en lazy-loadant les modals **Non-Goals:** - Refactorer l'architecture SSE (sujet Phase 2) - Changer la stratégie de cache/revalidation (sujet Phase 2) - Optimiser les requêtes Prisma profondes (sujet Phase 3) - Modifier le comportement fonctionnel existant ## Decisions ### 1. Batch resolveCollaborator par collect + single query **Décision** : Dans `session-queries.ts`, collecter tous les `userId` des collaborateurs d'une liste de sessions, puis faire un seul `prisma.user.findMany({ where: { id: { in: [...ids] } } })`, et mapper les résultats en mémoire. **Alternatives** : Garder le N+1 mais ajouter un cache mémoire par requête → rejeté car ne résout pas le problème structurellement. ### 2. Debounce via useRef + setTimeout natif **Décision** : Dans `useLive.ts`, utiliser `useRef` pour stocker un timer et `setTimeout` / `clearTimeout` pour debounce à 300ms. Pas de dépendance externe. **Alternatives** : Bibliothèque `lodash.debounce` → rejeté pour éviter une dépendance pour 5 lignes. ### 3. cleanupOldEvents inline dans createEvent **Décision** : Appeler `cleanupOldEvents` à la fin de chaque `createEvent` (fire-and-forget, pas d'await bloquant). La purge garde les 50 derniers événements par session (seuil actuel). **Alternatives** : Cron externe → trop complexe pour un quick win ; interval côté API SSE → couplage non souhaité. ### 4. loading.tsx avec skeleton minimaliste **Décision** : Créer un `loading.tsx` par route principale (`/sessions`, `/weather`, `/users`) avec un skeleton générique (barres grises animées). Le composant est statique et ultra-léger. ### 5. next/dynamic avec ssr: false sur les modals **Décision** : Wrapper `ShareModal` (et `CollaborationToolbar` si pertinent) avec `next/dynamic({ ssr: false })`. Le composant parent gère le loading state. ## Risks / Trade-offs - **Debounce 300ms** → légère latence perçue sur les mises à jour collaboratives. Mitigation : valeur configurable via constante. - **cleanupOldEvents fire-and-forget** → si la purge échoue, les erreurs sont silencieuses. Mitigation : logger l'erreur sans bloquer. - **Batch resolveCollaborator** → si la liste de sessions est très grande (>500), la requête `IN` peut être lente. Mitigation : acceptable pour les volumes actuels ; paginer si nécessaire (Phase 3). - **next/dynamic ssr: false** → les modals ne sont pas rendus côté serveur. Acceptable car ils sont interactifs uniquement. ## Migration Plan Chaque optimisation est indépendante et déployable séparément. Pas de migration de données. Rollback : revert du commit concerné. L'ordre recommandé : (1) batch resolveCollaborator, (2) cleanupOldEvents, (3) debounce useLive, (4) loading.tsx, (5) next/dynamic.