♻️ 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 { ref, computed } from 'vue'
|
||||||
import { api } from '../services/pmocontrol/api'
|
import { api } from '../services/pmocontrol/api'
|
||||||
import { sse } from '../services/pmocontrol/sse'
|
import { useSSE } from './useSSE'
|
||||||
import type {
|
import type {
|
||||||
MediaServerSummary,
|
MediaServerSummary,
|
||||||
ContainerEntry,
|
ContainerEntry,
|
||||||
@@ -35,12 +35,17 @@ const lastFetch = {
|
|||||||
|
|
||||||
const CACHE_DURATION_MS = 2000
|
const CACHE_DURATION_MS = 2000
|
||||||
|
|
||||||
// Connecter SSE une seule fois
|
// Initialiser SSE une seule fois via le composable centralisé
|
||||||
let sseConnected = false
|
let sseInitialized = false
|
||||||
function ensureSSEConnected() {
|
function ensureSSEInitialized() {
|
||||||
if (sseConnected) return
|
if (sseInitialized) return
|
||||||
|
|
||||||
sse.onMediaServerEvent((event) => {
|
const { onMediaServerEvent, connect } = useSSE()
|
||||||
|
|
||||||
|
// Démarrer la connexion SSE
|
||||||
|
connect()
|
||||||
|
|
||||||
|
onMediaServerEvent((event) => {
|
||||||
const serverId = event.server_id
|
const serverId = event.server_id
|
||||||
|
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
@@ -98,14 +103,14 @@ function ensureSSEConnected() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
sseConnected = true
|
sseInitialized = true
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Composable principal pour gérer les media servers
|
* Composable principal pour gérer les media servers
|
||||||
*/
|
*/
|
||||||
export function useMediaServers() {
|
export function useMediaServers() {
|
||||||
ensureSSEConnected()
|
ensureSSEInitialized()
|
||||||
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const loadingMore = ref(false)
|
const loadingMore = ref(false)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
*/
|
*/
|
||||||
import { ref, reactive, computed, toRaw, type Ref } from "vue";
|
import { ref, reactive, computed, toRaw, type Ref } from "vue";
|
||||||
import { api } from "../services/pmocontrol/api";
|
import { api } from "../services/pmocontrol/api";
|
||||||
import { sse } from "../services/pmocontrol/sse";
|
import { useSSE } from "./useSSE";
|
||||||
import type {
|
import type {
|
||||||
RendererSummary,
|
RendererSummary,
|
||||||
RendererState,
|
RendererState,
|
||||||
@@ -40,11 +40,17 @@ const snapshotState = reactive<RendererSnapshotState>({
|
|||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const error = ref<string | null>(null);
|
const error = ref<string | null>(null);
|
||||||
|
|
||||||
let sseConnected = false;
|
// Utiliser le composable SSE centralisé
|
||||||
function ensureSSEConnected() {
|
let sseInitialized = false;
|
||||||
if (sseConnected) return;
|
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 rendererId = event.renderer_id;
|
||||||
const timestamp = Date.parse(event.timestamp ?? "") || Date.now();
|
const timestamp = Date.parse(event.timestamp ?? "") || Date.now();
|
||||||
|
|
||||||
@@ -235,7 +241,7 @@ function ensureSSEConnected() {
|
|||||||
snapshotState.snapshots.set(rendererId, snapshot);
|
snapshotState.snapshots.set(rendererId, snapshot);
|
||||||
});
|
});
|
||||||
|
|
||||||
sseConnected = true;
|
sseInitialized = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const allRenderers = computed(() => Array.from(renderersCache.value.values()));
|
const allRenderers = computed(() => Array.from(renderersCache.value.values()));
|
||||||
@@ -284,7 +290,7 @@ function selectRenderer(id: string | null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function fetchRenderers(force = false) {
|
async function fetchRenderers(force = false) {
|
||||||
ensureSSEConnected();
|
ensureSSEInitialized();
|
||||||
|
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
if (!force && now - lastRenderersFetch.value < RENDERERS_CACHE_MS) {
|
if (!force && now - lastRenderersFetch.value < RENDERERS_CACHE_MS) {
|
||||||
@@ -311,7 +317,7 @@ async function fetchRendererSnapshot(
|
|||||||
rendererId: string,
|
rendererId: string,
|
||||||
opts?: { force?: boolean },
|
opts?: { force?: boolean },
|
||||||
) {
|
) {
|
||||||
ensureSSEConnected();
|
ensureSSEInitialized();
|
||||||
const force = opts?.force ?? false;
|
const force = opts?.force ?? false;
|
||||||
const hasSnapshot = snapshotState.snapshots.has(rendererId);
|
const hasSnapshot = snapshotState.snapshots.has(rendererId);
|
||||||
|
|
||||||
@@ -449,7 +455,7 @@ async function addAfterCurrent(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useRenderers() {
|
export function useRenderers() {
|
||||||
ensureSSEConnected();
|
ensureSSEInitialized();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
loading,
|
loading,
|
||||||
@@ -494,7 +500,7 @@ export function useRenderers() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useRenderer(rendererId: Ref<string>) {
|
export function useRenderer(rendererId: Ref<string>) {
|
||||||
ensureSSEConnected();
|
ensureSSEInitialized();
|
||||||
|
|
||||||
const renderer = computed(() => renderersCache.value.get(rendererId.value));
|
const renderer = computed(() => renderersCache.value.get(rendererId.value));
|
||||||
const snapshot = computed(
|
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 { ref, onMounted, onUnmounted, readonly } from "vue";
|
||||||
import { sse } from "../services/pmocontrol/sse";
|
import { useSSE } from "./useSSE";
|
||||||
|
|
||||||
// ─── Identifiant stable de l'instance navigateur ─────────────────────────────
|
// ─── Identifiant stable de l'instance navigateur ─────────────────────────────
|
||||||
|
|
||||||
@@ -200,10 +200,11 @@ export function useWebRenderer() {
|
|||||||
onConnectedCallback?.();
|
onConnectedCallback?.();
|
||||||
|
|
||||||
// S'abonner aux événements SSE du renderer pour piloter la lecture
|
// S'abonner aux événements SSE du renderer pour piloter la lecture
|
||||||
sse.connect();
|
const { connect, onRendererEvent } = useSSE();
|
||||||
|
connect();
|
||||||
const udn = data.udn;
|
const udn = data.udn;
|
||||||
sseUnsubscribe?.();
|
sseUnsubscribe?.();
|
||||||
sseUnsubscribe = sse.onRendererEvent((event) => {
|
sseUnsubscribe = onRendererEvent((event) => {
|
||||||
if (event.renderer_id !== udn) return;
|
if (event.renderer_id !== udn) return;
|
||||||
if (event.type !== "state_changed") return;
|
if (event.type !== "state_changed") return;
|
||||||
|
|
||||||
|
|||||||
1
pmoapp/webapp/tsconfig.tsbuildinfo
Normal file
1
pmoapp/webapp/tsconfig.tsbuildinfo
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"fileNames":[],"fileInfos":[],"root":[],"version":"5.8.3"}
|
||||||
Reference in New Issue
Block a user