diff --git a/Cargo.lock b/Cargo.lock index 5017a1ac..14e48ed2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,7 +4,7 @@ version = 4 [[package]] name = "PMOMusic" -version = "0.3.36" +version = "0.3.37" dependencies = [ "axum 0.8.7", "console-subscriber", diff --git a/pmoapp/webapp/src/composables/apiCache.ts b/pmoapp/webapp/src/composables/apiCache.ts index bc925ce2..efbfa29d 100644 --- a/pmoapp/webapp/src/composables/apiCache.ts +++ b/pmoapp/webapp/src/composables/apiCache.ts @@ -92,20 +92,34 @@ class ApiCacheService { if (cached) return cached; } - const existing = this.pendingRequests.get(key); - if (existing) { - return existing.promise as Promise; + // Utiliser une clé unique pour éviter les problèmes de race condition + // avec les requêtes en cours qui peuvent être supprimées avant résolution + const requestKey = `request:${key}`; + + // Récupérer ou créer la requête + let pendingRequest = this.pendingRequests.get(requestKey); + + // Si une requête est en cours et sa promesse n'a pas encore été resolved/rejected + // on retourne directement cette promesse + if (pendingRequest) { + try { + // Attendre la résolution pour s'assurer que c'est toujours valide + return await pendingRequest.promise as T; + } catch (e) { + // La requête a échoué, on continue pour faire une nouvelle requête + this.pendingRequests.delete(requestKey); + } } - let resolvePromise!: (value: unknown) => void; + let resolvePromise!: (value: T) => void; let rejectPromise!: (reason: unknown) => void; - const promise = new Promise((resolve, reject) => { + const promise = new Promise((resolve, reject) => { resolvePromise = resolve; rejectPromise = reject; }); - this.pendingRequests.set(key, { + this.pendingRequests.set(requestKey, { promise, subscribers: new Set(), }); @@ -124,7 +138,7 @@ class ApiCacheService { resolvePromise(data); - const pending = this.pendingRequests.get(key); + const pending = this.pendingRequests.get(requestKey); if (pending) { pending.subscribers.forEach(cb => cb(data)); } @@ -133,10 +147,10 @@ class ApiCacheService { rejectPromise(error); throw error; } finally { - this.pendingRequests.delete(key); + this.pendingRequests.delete(requestKey); } - return Promise.reject(new Error('Unreachable')); + return promise; } subscribe(endpoint: string, params: Record, callback: (data: T) => void): () => void { diff --git a/pmoapp/webapp/src/composables/useRenderers.ts b/pmoapp/webapp/src/composables/useRenderers.ts index d0c9d9e2..4af3729d 100644 --- a/pmoapp/webapp/src/composables/useRenderers.ts +++ b/pmoapp/webapp/src/composables/useRenderers.ts @@ -322,6 +322,7 @@ async function fetchRendererSnapshot( } } + // Éviter les requêtes multiples simultanées pour le même renderer if (snapshotState.loadingIds.has(rendererId)) { return; } @@ -333,7 +334,10 @@ async function fetchRendererSnapshot( snapshotState.lastSnapshotAt.set(rendererId, Date.now()); } catch (err) { console.error(`[useRenderers] Erreur snapshot ${rendererId}:`, err); + // En cas d'erreur, on supprime le snapshot pour permettre une nouvelle tentative + snapshotState.snapshots.delete(rendererId); } finally { + // Toujours nettoyer le flag de chargement snapshotState.loadingIds.delete(rendererId); } } @@ -545,10 +549,25 @@ export function useRenderer(rendererId: Ref) { const isStream = computed(() => snapshot.value?.is_stream ?? false); const queueRefreshing = computed(() => isQueueRefreshing(rendererId.value)); + // Debounce pour éviter les refreshs multiples trop fréquents + let refreshDebounceTimer: ReturnType | null = null; + const REFRESH_DEBOUNCE_MS = 500; + async function refresh(force = true) { + const currentRendererId = rendererId.value; + + // Debounce: ignorer si un refresh est en cours pour ce renderer + if (refreshDebounceTimer !== null) { + return; + } + + refreshDebounceTimer = setTimeout(() => { + refreshDebounceTimer = null; + }, REFRESH_DEBOUNCE_MS); + await Promise.all([ fetchRenderers(force), - fetchRendererSnapshot(rendererId.value, { force: true }), + fetchRendererSnapshot(currentRendererId, { force: true }), ]); } diff --git a/pmoapp/webapp/src/views/UnifiedControlView.vue b/pmoapp/webapp/src/views/UnifiedControlView.vue index e7e42f1b..68f5b397 100644 --- a/pmoapp/webapp/src/views/UnifiedControlView.vue +++ b/pmoapp/webapp/src/views/UnifiedControlView.vue @@ -135,12 +135,18 @@ onMounted(async () => { }); // Watch renderers pour sync automatique des tabs +// Note: on watch la taille du tableau + les IDs pour éviter un deep watch coûteux watch( - () => allRenderers.value, - (newRenderers) => { - syncWithRenderers(filterRenderers(newRenderers)); + () => ({ + length: allRenderers.value.length, + ids: allRenderers.value.map(r => r.id).join(','), + }), + (newVal, oldVal) => { + // Re-sync uniquement si le nombre ou les IDs ont changé + if (newVal.length !== oldVal?.length || newVal.ids !== oldVal?.ids) { + syncWithRenderers(filterRenderers(allRenderers.value)); + } }, - { deep: true }, ); // Watch l'UDN du WebRenderer local : quand il s'établit, resync pour faire apparaître notre onglet