Files
pmomusic/pmoapp/webapp/src/composables/useSSE.ts
Eric Coissac 037ddc99ee ♻️ 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
2026-04-03 22:27:02 +02:00

244 lines
6.0 KiB
TypeScript

/**
* 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,
}
}