import { computed, onScopeDispose, watch } from 'vue'; import { useIdeStore } from '../stores/ide'; import { useWorktreesStore } from '../stores/worktrees'; import { wsClient } from '../lib/ws-client'; /** * Propriétaire UNIQUE des abonnements `watch` de worktrees (message WS ciblé P7). Monté dans App.vue, * donc jamais démonté : avant, seul `GitPanel` posait un `watch`, et il partait dès qu'on quittait * l'onglet Git · plus aucun worktree n'était alors observé et les compteurs de l'arbre gelaient. * * On observe ce que l'utilisateur regarde vraiment : le worktree actif et ceux des dépôts dépliés dans * l'arbre. Le coût suit donc l'attention, et non le nombre de dépôts enregistrés. Les worktrees * portant une session vivante sont, eux, épinglés côté serveur (`syncSessionPin`) : inutile de les * demander ici. * * Chaque `worktree_changes` reçu incrémente le jeton d'invalidation du store (`noteChange`), que les * vues à contenu paresseux (arbre de fichiers, diff, historique) observent pour se recharger. */ /** * Plafond d'abonnements simultanés. Le pool de watchers du serveur est borné (LRU) mais n'évince * jamais une entrée explicitement demandée : c'est donc au client de ne pas en réclamer trop. 40 * couvre très largement un travail réel (le worktree actif plus quelques dépôts dépliés). */ export const MAX_WATCHED = 40; export function useWatchedWorktrees(): void { const ide = useIdeStore(); const worktrees = useWorktreesStore(); /** Abonnements en cours, clé `repoId\0path` → fonction de désabonnement. */ const active = new Map void>(); const wanted = computed>(() => { const out: Array<{ repoId: string; path: string }> = []; const seen = new Set(); const add = (repoId: string, path: string): void => { const key = `${repoId}\0${path}`; if (seen.has(key)) return; seen.add(key); out.push({ repoId, path }); }; // Le worktree actif d'abord : il doit survivre au plafond. const ctx = ide.activeContext; if (ctx) add(ctx.repoId, ctx.wtPath); // Worktrees dont l'arbre de fichiers est déplié : ils peuvent l'être depuis le panneau Groupes, // sans que leur dépôt soit déplié dans l'Explorateur. Sans eux, cet arbre-là ne recevrait aucun // `worktree_changes` et resterait figé sur son premier chargement. for (const path of ide.expandedWtPaths) { const wt = worktrees.worktrees.find((w) => w.path === path); if (wt) add(wt.repoId, wt.path); } for (const repoId of ide.expandedRepoIds) { for (const wt of worktrees.worktreesForRepo(repoId)) add(wt.repoId, wt.path); } return out.slice(0, MAX_WATCHED); }); watch( wanted, (list) => { const keys = new Set(list.map((w) => `${w.repoId}\0${w.path}`)); for (const [key, off] of active) { if (keys.has(key)) continue; off(); active.delete(key); } for (const w of list) { const key = `${w.repoId}\0${w.path}`; if (active.has(key)) continue; active.set( key, wsClient.watchWorktree(w.repoId, w.path, (e) => worktrees.noteChange(e.repoId, e.path)), ); } }, { immediate: true }, ); onScopeDispose(() => { for (const off of active.values()) off(); active.clear(); }); }