♻️ Centraliser la gestion SSE dans un composable unique
- Création d'un nouveau composant `useSSE` pour centraliser la gestion des connexions SSE - Remplacement direct de l'import `sse` par le nouveau composable dans :useMediaServers:, useRenderers: et useWebRenderer: - Refactorisation des fonctions `ensureSSEConnected` → ensureSCEInitialized pour utiliser le composable centralisé - Ajout d'une méthode `connect()` explicite dans chaque usage pour garantir la connexion au bon moment - Suppression des variables `sseConnected` (doublons) et remplacement par un état partagé via `useSSE` - Ajout de hooks spécialisés : useRendererEvents et useMediaServerEvent pour simplifier les abonnements filtrées - Nettoyage des imports obsolètes et harmonisation de la gestion d'état SSE
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
import { ref, computed } from 'vue'
|
||||
import { api } from '../services/pmocontrol/api'
|
||||
import { sse } from '../services/pmocontrol/sse'
|
||||
import { useSSE } from './useSSE'
|
||||
import type {
|
||||
MediaServerSummary,
|
||||
ContainerEntry,
|
||||
@@ -35,12 +35,17 @@ const lastFetch = {
|
||||
|
||||
const CACHE_DURATION_MS = 2000
|
||||
|
||||
// Connecter SSE une seule fois
|
||||
let sseConnected = false
|
||||
function ensureSSEConnected() {
|
||||
if (sseConnected) return
|
||||
// Initialiser SSE une seule fois via le composable centralisé
|
||||
let sseInitialized = false
|
||||
function ensureSSEInitialized() {
|
||||
if (sseInitialized) return
|
||||
|
||||
sse.onMediaServerEvent((event) => {
|
||||
const { onMediaServerEvent, connect } = useSSE()
|
||||
|
||||
// Démarrer la connexion SSE
|
||||
connect()
|
||||
|
||||
onMediaServerEvent((event) => {
|
||||
const serverId = event.server_id
|
||||
|
||||
switch (event.type) {
|
||||
@@ -98,14 +103,14 @@ function ensureSSEConnected() {
|
||||
}
|
||||
})
|
||||
|
||||
sseConnected = true
|
||||
sseInitialized = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Composable principal pour gérer les media servers
|
||||
*/
|
||||
export function useMediaServers() {
|
||||
ensureSSEConnected()
|
||||
ensureSSEInitialized()
|
||||
|
||||
const loading = ref(false)
|
||||
const loadingMore = ref(false)
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
*/
|
||||
import { ref, reactive, computed, toRaw, type Ref } from "vue";
|
||||
import { api } from "../services/pmocontrol/api";
|
||||
import { sse } from "../services/pmocontrol/sse";
|
||||
import { useSSE } from "./useSSE";
|
||||
import type {
|
||||
RendererSummary,
|
||||
RendererState,
|
||||
@@ -40,11 +40,17 @@ const snapshotState = reactive<RendererSnapshotState>({
|
||||
const loading = ref(false);
|
||||
const error = ref<string | null>(null);
|
||||
|
||||
let sseConnected = false;
|
||||
function ensureSSEConnected() {
|
||||
if (sseConnected) return;
|
||||
// Utiliser le composable SSE centralisé
|
||||
let sseInitialized = false;
|
||||
function ensureSSEInitialized() {
|
||||
if (sseInitialized) return;
|
||||
|
||||
sse.onRendererEvent((event) => {
|
||||
const { onRendererEvent, connect } = useSSE();
|
||||
|
||||
// Démarrer la connexion SSE
|
||||
connect();
|
||||
|
||||
onRendererEvent((event) => {
|
||||
const rendererId = event.renderer_id;
|
||||
const timestamp = Date.parse(event.timestamp ?? "") || Date.now();
|
||||
|
||||
@@ -235,7 +241,7 @@ function ensureSSEConnected() {
|
||||
snapshotState.snapshots.set(rendererId, snapshot);
|
||||
});
|
||||
|
||||
sseConnected = true;
|
||||
sseInitialized = true;
|
||||
}
|
||||
|
||||
const allRenderers = computed(() => Array.from(renderersCache.value.values()));
|
||||
@@ -284,7 +290,7 @@ function selectRenderer(id: string | null) {
|
||||
}
|
||||
|
||||
async function fetchRenderers(force = false) {
|
||||
ensureSSEConnected();
|
||||
ensureSSEInitialized();
|
||||
|
||||
const now = Date.now();
|
||||
if (!force && now - lastRenderersFetch.value < RENDERERS_CACHE_MS) {
|
||||
@@ -311,7 +317,7 @@ async function fetchRendererSnapshot(
|
||||
rendererId: string,
|
||||
opts?: { force?: boolean },
|
||||
) {
|
||||
ensureSSEConnected();
|
||||
ensureSSEInitialized();
|
||||
const force = opts?.force ?? false;
|
||||
const hasSnapshot = snapshotState.snapshots.has(rendererId);
|
||||
|
||||
@@ -449,7 +455,7 @@ async function addAfterCurrent(
|
||||
}
|
||||
|
||||
export function useRenderers() {
|
||||
ensureSSEConnected();
|
||||
ensureSSEInitialized();
|
||||
|
||||
return {
|
||||
loading,
|
||||
@@ -494,7 +500,7 @@ export function useRenderers() {
|
||||
}
|
||||
|
||||
export function useRenderer(rendererId: Ref<string>) {
|
||||
ensureSSEConnected();
|
||||
ensureSSEInitialized();
|
||||
|
||||
const renderer = computed(() => renderersCache.value.get(rendererId.value));
|
||||
const snapshot = computed(
|
||||
|
||||
244
pmoapp/webapp/src/composables/useSSE.ts
Normal file
244
pmoapp/webapp/src/composables/useSSE.ts
Normal file
@@ -0,0 +1,244 @@
|
||||
/**
|
||||
* Composable centralisé pour la gestion des Server-Sent Events
|
||||
*
|
||||
* Ce composable fournit une interface unifiée pour:
|
||||
* - Une seule connexion SSE (évite les connexions multiples)
|
||||
* - Des abonnements typés aux événements (renderers, servers)
|
||||
* - Le suivi de l'état de connexion
|
||||
*
|
||||
* Usage:
|
||||
* const { connected, onRendererEvent, onMediaServerEvent } = useSSE()
|
||||
*/
|
||||
import { ref, readonly, onUnmounted } from 'vue'
|
||||
import { sse } from '../services/pmocontrol/sse'
|
||||
import type {
|
||||
RendererEventPayload,
|
||||
MediaServerEventPayload
|
||||
} from '../services/pmocontrol/types'
|
||||
|
||||
// État global partagé
|
||||
const connected = ref(sse.isConnectedState())
|
||||
const connectionCallbacks: Set<(connected: boolean) => void> = new Set()
|
||||
|
||||
// Abonnement à l'état de connexion global
|
||||
function setupConnectionListener() {
|
||||
// S'assurer qu'on ne s'abonne qu'une seule fois
|
||||
if (connectionCallbacks.size === 0) {
|
||||
sse.onConnectionChange((isConnected) => {
|
||||
connected.value = isConnected
|
||||
connectionCallbacks.forEach(cb => cb(isConnected))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook principal pour utiliser SSE
|
||||
*/
|
||||
export function useSSE() {
|
||||
// S'assurer que le listener de connexion est configuré
|
||||
setupConnectionListener()
|
||||
|
||||
/**
|
||||
* Abonnement aux événements de type renderer
|
||||
* Retourne une fonction de cleanup
|
||||
*/
|
||||
function onRendererEvent(
|
||||
callback: (event: RendererEventPayload) => void
|
||||
): () => void {
|
||||
return sse.onRendererEvent(callback)
|
||||
}
|
||||
|
||||
/**
|
||||
* Abonnement aux événements de type media server
|
||||
* Retourne une fonction de cleanup
|
||||
*/
|
||||
function onMediaServerEvent(
|
||||
callback: (event: MediaServerEventPayload) => void
|
||||
): () => void {
|
||||
return sse.onMediaServerEvent(callback)
|
||||
}
|
||||
|
||||
/**
|
||||
* Abonnement aux changements de connexion
|
||||
* Retourne une fonction de cleanup
|
||||
*/
|
||||
function onConnectionChange(
|
||||
callback: (connected: boolean) => void
|
||||
): () => void {
|
||||
connectionCallbacks.add(callback)
|
||||
// Appeler immédiatement avec l'état actuel
|
||||
callback(connected.value)
|
||||
|
||||
// Retourner fonction de cleanup
|
||||
return () => {
|
||||
connectionCallbacks.delete(callback)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Force la connexion SSE
|
||||
*/
|
||||
function connect(): void {
|
||||
sse.connect()
|
||||
}
|
||||
|
||||
/**
|
||||
* Force la déconnexion SSE
|
||||
*/
|
||||
function disconnect(): void {
|
||||
sse.disconnect()
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si actuellement connecté
|
||||
*/
|
||||
function isConnected(): boolean {
|
||||
return connected.value
|
||||
}
|
||||
|
||||
return {
|
||||
// État (readonly pour éviter les modifications directes)
|
||||
connected: readonly(connected),
|
||||
|
||||
// Abonnements
|
||||
onRendererEvent,
|
||||
onMediaServerEvent,
|
||||
onConnectionChange,
|
||||
|
||||
// Actions
|
||||
connect,
|
||||
disconnect,
|
||||
isConnected,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook pour s'abonner à un type spécifique d'événement renderer
|
||||
* avec filtrage optionnel par rendererId
|
||||
*/
|
||||
export function useRendererEvents(
|
||||
rendererId: () => string | null,
|
||||
options?: {
|
||||
onStateChanged?: (event: RendererEventPayload) => void
|
||||
onPositionChanged?: (event: RendererEventPayload) => void
|
||||
onVolumeChanged?: (event: RendererEventPayload) => void
|
||||
onMetadataChanged?: (event: RendererEventPayload) => void
|
||||
onQueueUpdated?: (event: RendererEventPayload) => void
|
||||
onBindingChanged?: (event: RendererEventPayload) => void
|
||||
onTimerEvent?: (event: RendererEventPayload) => void
|
||||
}
|
||||
) {
|
||||
const { onRendererEvent } = useSSE()
|
||||
|
||||
let cleanup: (() => void) | null = null
|
||||
|
||||
function setup() {
|
||||
cleanup = onRendererEvent((event) => {
|
||||
const currentId = rendererId()
|
||||
|
||||
// Si un rendererId est spécifié, filtrer
|
||||
if (currentId && event.renderer_id !== currentId) {
|
||||
return
|
||||
}
|
||||
|
||||
// Dispatch vers le handler approprié
|
||||
switch (event.type) {
|
||||
case 'state_changed':
|
||||
case 'online':
|
||||
case 'offline':
|
||||
options?.onStateChanged?.(event)
|
||||
break
|
||||
case 'position_changed':
|
||||
options?.onPositionChanged?.(event)
|
||||
break
|
||||
case 'volume_changed':
|
||||
case 'mute_changed':
|
||||
options?.onVolumeChanged?.(event)
|
||||
break
|
||||
case 'metadata_changed':
|
||||
options?.onMetadataChanged?.(event)
|
||||
break
|
||||
case 'queue_updated':
|
||||
case 'queue_refreshing':
|
||||
options?.onQueueUpdated?.(event)
|
||||
break
|
||||
case 'binding_changed':
|
||||
options?.onBindingChanged?.(event)
|
||||
break
|
||||
case 'timer_started':
|
||||
case 'timer_updated':
|
||||
case 'timer_tick':
|
||||
case 'timer_expired':
|
||||
case 'timer_cancelled':
|
||||
options?.onTimerEvent?.(event)
|
||||
break
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
setup()
|
||||
|
||||
// Cleanup automatique au unmount du composant
|
||||
onUnmounted(() => {
|
||||
cleanup?.()
|
||||
})
|
||||
|
||||
return {
|
||||
refresh: setup, // Permet de recréer l'abonnement si besoin
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook pour s'abonner aux événements media server
|
||||
* avec filtrage optionnel par serverId
|
||||
*/
|
||||
export function useMediaServerEvents(
|
||||
serverId: () => string | null,
|
||||
options?: {
|
||||
onOnline?: (event: MediaServerEventPayload) => void
|
||||
onOffline?: (event: MediaServerEventPayload) => void
|
||||
onGlobalUpdated?: (event: MediaServerEventPayload) => void
|
||||
onContainersUpdated?: (event: MediaServerEventPayload) => void
|
||||
}
|
||||
) {
|
||||
const { onMediaServerEvent } = useSSE()
|
||||
|
||||
let cleanup: (() => void) | null = null
|
||||
|
||||
function setup() {
|
||||
cleanup = onMediaServerEvent((event) => {
|
||||
const currentId = serverId()
|
||||
|
||||
// Si un serverId est spécifié, filtrer
|
||||
if (currentId && event.server_id !== currentId) {
|
||||
return
|
||||
}
|
||||
|
||||
// Dispatch vers le handler approprié
|
||||
switch (event.type) {
|
||||
case 'online':
|
||||
options?.onOnline?.(event)
|
||||
break
|
||||
case 'offline':
|
||||
options?.onOffline?.(event)
|
||||
break
|
||||
case 'global_updated':
|
||||
options?.onGlobalUpdated?.(event)
|
||||
break
|
||||
case 'containers_updated':
|
||||
options?.onContainersUpdated?.(event)
|
||||
break
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
setup()
|
||||
|
||||
onUnmounted(() => {
|
||||
cleanup?.()
|
||||
})
|
||||
|
||||
return {
|
||||
refresh: setup,
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@
|
||||
*/
|
||||
|
||||
import { ref, onMounted, onUnmounted, readonly } from "vue";
|
||||
import { sse } from "../services/pmocontrol/sse";
|
||||
import { useSSE } from "./useSSE";
|
||||
|
||||
// ─── Identifiant stable de l'instance navigateur ─────────────────────────────
|
||||
|
||||
@@ -200,10 +200,11 @@ export function useWebRenderer() {
|
||||
onConnectedCallback?.();
|
||||
|
||||
// S'abonner aux événements SSE du renderer pour piloter la lecture
|
||||
sse.connect();
|
||||
const { connect, onRendererEvent } = useSSE();
|
||||
connect();
|
||||
const udn = data.udn;
|
||||
sseUnsubscribe?.();
|
||||
sseUnsubscribe = sse.onRendererEvent((event) => {
|
||||
sseUnsubscribe = onRendererEvent((event) => {
|
||||
if (event.renderer_id !== udn) return;
|
||||
if (event.type !== "state_changed") return;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user