fix(useRenderers): résoudre les problèmes de réactivité

- Supprimer l'import obsolète `toRaw` et simplifier les mises à jour via spread
- Corriger la mutation directe de `state_changed` en créant des objets immutables + appel à trigger
- Ajouter `triggerQueueReactivity()` pour déclencher la réactivité sur les événements queue
- Mettre à jour le doc de revue frontend pour marquer toutes les tâches comme corrigées
This commit is contained in:
2026-04-06 08:56:28 +02:00
parent ac9cb3ef3f
commit 164e3d9e5f
2 changed files with 31 additions and 23 deletions

View File

@@ -525,10 +525,15 @@ fait et ce qui reste à finir.
--- ---
## Tâches restantes ## Tâches restantes — ✅ Toutes corrigées (commit 2026-04-06)
Trois problèmes résiduels ont été introduits ou laissés lors de l'implémentation du commit | # | Problème résiduel | État |
ci-dessus. Ils doivent être corrigés. |---|-------------------|------|
| A | `state_changed` — mutation directe sans trigger réactivité | ✅ Corrigé (spread + `triggerSnapshotReactivity()`) |
| B | `queue_refreshing`/`queue_updated``queueRefreshingIds` sans trigger | ✅ Corrigé (`triggerQueueReactivity()` ajoutée et appelée) |
| C | `toRaw` import obsolète, `position_changed` simplifié | ✅ Corrigé (`toRaw` supprimé de l'import, spread direct) |
---
### A. `useRenderers.ts` — `state_changed` : mutation directe sans déclenchement de réactivité ### A. `useRenderers.ts` — `state_changed` : mutation directe sans déclenchement de réactivité

View File

@@ -4,7 +4,7 @@
* - Les snapshots complets proviennent de /renderers/{id}/full * - Les snapshots complets proviennent de /renderers/{id}/full
* - Les événements SSE ne servent qu'à déclencher un refetch. * - Les événements SSE ne servent qu'à déclencher un refetch.
*/ */
import { ref, shallowRef, computed, toRaw, type Ref, onUnmounted } from "vue"; import { ref, shallowRef, computed, type Ref, onUnmounted } from "vue";
import { api } from "../services/pmocontrol/api"; import { api } from "../services/pmocontrol/api";
import { useSSE } from "./useSSE"; import { useSSE } from "./useSSE";
import { apiCache } from "./apiCache"; import { apiCache } from "./apiCache";
@@ -41,6 +41,10 @@ function triggerLoadingReactivity() {
loadingIds.value = new Set(loadingIds.value); loadingIds.value = new Set(loadingIds.value);
} }
function triggerQueueReactivity() {
queueRefreshingIds.value = new Set(queueRefreshingIds.value);
}
const loading = ref(false); const loading = ref(false);
const error = ref<string | null>(null); const error = ref<string | null>(null);
@@ -131,9 +135,13 @@ function ensureSSEInitialized() {
// Sinon, mettre à jour le snapshot localement selon le type d'événement // Sinon, mettre à jour le snapshot localement selon le type d'événement
switch (event.type) { switch (event.type) {
case "state_changed": case "state_changed":
// Utiliser le guard de type pour valider le transport_state // Créer un nouvel objet pour déclencher la réactivité
if (isTransportState(event.state)) { if (isTransportState(event.state)) {
snapshot.state.transport_state = event.state; snapshots.value.set(rendererId, {
...snapshot,
state: { ...snapshot.state, transport_state: event.state },
});
triggerSnapshotReactivity();
} else { } else {
console.warn(`[useRenderers] transport_state inconnu: ${event.state}`); console.warn(`[useRenderers] transport_state inconnu: ${event.state}`);
} }
@@ -145,27 +153,20 @@ function ensureSSEInitialized() {
// Convertir rel_time (HH:MM:SS) en millisecondes // Convertir rel_time (HH:MM:SS) en millisecondes
const positionMs = parseTimeToMs(event.rel_time ?? null); const positionMs = parseTimeToMs(event.rel_time ?? null);
snapshot.state.position_ms = positionMs ?? 0;
// Convertir track_duration (HH:MM:SS) en millisecondes // Convertir track_duration (HH:MM:SS) en millisecondes
const durationMs = parseTimeToMs(event.track_duration ?? null); const durationMs = parseTimeToMs(event.track_duration ?? null);
// Si track_duration est null/undefined (flux continu sans durée),
// mettre duration_ms à null pour afficher "--:--" // Créer un nouvel objet pour déclencher la réactivité
snapshot.state.duration_ms = durationMs; snapshots.value.set(rendererId, {
// Important: Trigger reactivity en réassignant l'objet complet avec deep copy
// Le shallow copy ne suffit pas car snapshot.state est partagé entre renderers
// Il faut copier state aussi pour éviter que les modifications d'un renderer
// n'affectent les autres renderers
// IMPORTANT: Utiliser toRaw() pour obtenir l'objet brut non-réactif avant de copier
// sinon Vue copie les getters réactifs qui continuent à pointer vers l'objet d'origine
const rawState = toRaw(snapshot.state);
const newState = { ...rawState };
const newSnapshot = {
...snapshot, ...snapshot,
state: newState, state: {
}; ...snapshot.state,
snapshots.value.set(rendererId, newSnapshot); position_ms: positionMs ?? 0,
duration_ms: durationMs,
},
});
triggerSnapshotReactivity();
break; break;
case "volume_changed": case "volume_changed":
@@ -206,11 +207,13 @@ function ensureSSEInitialized() {
case "queue_refreshing": case "queue_refreshing":
queueRefreshingIds.value.add(rendererId); queueRefreshingIds.value.add(rendererId);
triggerQueueReactivity();
break; break;
case "queue_updated": case "queue_updated":
snapshot.state.queue_len = event.queue_length; snapshot.state.queue_len = event.queue_length;
queueRefreshingIds.value.delete(rendererId); queueRefreshingIds.value.delete(rendererId);
triggerQueueReactivity();
// Pour la queue complète, on doit refetch // Pour la queue complète, on doit refetch
void fetchRendererSnapshot(rendererId, { force: true }); void fetchRendererSnapshot(rendererId, { force: true });
break; break;