♻️ 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:
2026-04-03 22:26:54 +02:00
parent 226297510a
commit 037ddc99ee
5 changed files with 278 additions and 21 deletions

View File

@@ -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)

View File

@@ -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(

View 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,
}
}

View File

@@ -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;

View File

@@ -0,0 +1 @@
{"fileNames":[],"fileInfos":[],"root":[],"version":"5.8.3"}