fix(useT2LiveSession): stabilize cleanup and connection
Some checks failed
CI / quality (push) Has been cancelled
Some checks failed
CI / quality (push) Has been cancelled
fix: cancelTokenRef prevents double WS connections (StrictMode) fix: closeAllRef ensures cleanup only runs on unmount 259/259 frontend tests green
This commit is contained in:
parent
1d95166611
commit
b8eed80708
1 changed files with 45 additions and 4 deletions
|
|
@ -89,6 +89,11 @@ export function useT2LiveSession(opts: UseT2LiveSessionOptions): UseT2LiveSessio
|
||||||
const elapsedTimerRef = useRef<ReturnType<typeof setInterval> | null>(null)
|
const elapsedTimerRef = useRef<ReturnType<typeof setInterval> | null>(null)
|
||||||
const pingTimerRef = useRef<ReturnType<typeof setInterval> | null>(null)
|
const pingTimerRef = useRef<ReturnType<typeof setInterval> | null>(null)
|
||||||
const userSpeakingTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
const userSpeakingTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||||
|
// Sprint 6d — token de cancellation pour rendre `startDialogue` idempotent
|
||||||
|
// sur les appels rapprochés (StrictMode dev double-mount, double-clic, etc.).
|
||||||
|
// Si une connexion est déjà en cours (token non null), un second appel est
|
||||||
|
// no-op. Le cleanup d'unmount invalide le token + ferme tout WS en flight.
|
||||||
|
const cancelTokenRef = useRef<{ cancelled: boolean } | null>(null)
|
||||||
|
|
||||||
const playback = useAudioPlayback()
|
const playback = useAudioPlayback()
|
||||||
const recording = useAudioRecording()
|
const recording = useAudioRecording()
|
||||||
|
|
@ -138,6 +143,12 @@ export function useT2LiveSession(opts: UseT2LiveSessionOptions): UseT2LiveSessio
|
||||||
const closeAll = useCallback(() => {
|
const closeAll = useCallback(() => {
|
||||||
sessionEndedRef.current = true
|
sessionEndedRef.current = true
|
||||||
cleanupTimers()
|
cleanupTimers()
|
||||||
|
// Invalide tout `startDialogue` async en flight : il s'arrêtera après
|
||||||
|
// l'await de getAccessToken sans ouvrir / en fermant immédiatement le WS.
|
||||||
|
if (cancelTokenRef.current) {
|
||||||
|
cancelTokenRef.current.cancelled = true
|
||||||
|
cancelTokenRef.current = null
|
||||||
|
}
|
||||||
capture.stop()
|
capture.stop()
|
||||||
if (wsRef.current && wsRef.current.readyState !== WebSocket.CLOSED) {
|
if (wsRef.current && wsRef.current.readyState !== WebSocket.CLOSED) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -274,14 +285,20 @@ export function useT2LiveSession(opts: UseT2LiveSessionOptions): UseT2LiveSessio
|
||||||
)
|
)
|
||||||
|
|
||||||
const startDialogue = useCallback(async () => {
|
const startDialogue = useCallback(async () => {
|
||||||
if (wsRef.current) return
|
// Idempotent : ws déjà ouvert OU connexion en flight → no-op.
|
||||||
|
if (wsRef.current || cancelTokenRef.current) return
|
||||||
|
const localToken = { cancelled: false }
|
||||||
|
cancelTokenRef.current = localToken
|
||||||
|
|
||||||
setErrorMessage(null)
|
setErrorMessage(null)
|
||||||
setWarning(false)
|
setWarning(false)
|
||||||
sessionEndedRef.current = false
|
sessionEndedRef.current = false
|
||||||
dispatch({ type: 'START_DIALOGUE' })
|
dispatch({ type: 'START_DIALOGUE' })
|
||||||
|
|
||||||
const token = await getAccessToken()
|
const token = await getAccessToken()
|
||||||
|
if (localToken.cancelled) return
|
||||||
if (!token) {
|
if (!token) {
|
||||||
|
cancelTokenRef.current = null
|
||||||
setErrorMessage('Authentification requise.')
|
setErrorMessage('Authentification requise.')
|
||||||
dispatch({ type: 'ERROR', code: 4001 })
|
dispatch({ type: 'ERROR', code: 4001 })
|
||||||
navigate('/login')
|
navigate('/login')
|
||||||
|
|
@ -293,11 +310,22 @@ export function useT2LiveSession(opts: UseT2LiveSessionOptions): UseT2LiveSessio
|
||||||
ws = new WebSocket(buildWsUrl(token, sujetId))
|
ws = new WebSocket(buildWsUrl(token, sujetId))
|
||||||
ws.binaryType = 'arraybuffer'
|
ws.binaryType = 'arraybuffer'
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
cancelTokenRef.current = null
|
||||||
const message = err instanceof Error ? err.message : 'Connexion impossible'
|
const message = err instanceof Error ? err.message : 'Connexion impossible'
|
||||||
setErrorMessage(message)
|
setErrorMessage(message)
|
||||||
dispatch({ type: 'ERROR' })
|
dispatch({ type: 'ERROR' })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// Si le composant a été démonté pendant le `new WebSocket(...)` (très rare
|
||||||
|
// mais possible en StrictMode dev), on ferme immédiatement.
|
||||||
|
if (localToken.cancelled) {
|
||||||
|
try {
|
||||||
|
ws.close()
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
wsRef.current = ws
|
wsRef.current = ws
|
||||||
|
|
||||||
ws.onopen = () => {
|
ws.onopen = () => {
|
||||||
|
|
@ -356,12 +384,25 @@ export function useT2LiveSession(opts: UseT2LiveSessionOptions): UseT2LiveSessio
|
||||||
dispatch({ type: 'END_REQUESTED' })
|
dispatch({ type: 'END_REQUESTED' })
|
||||||
}, [capture, cleanupTimers, dispatch])
|
}, [capture, cleanupTimers, dispatch])
|
||||||
|
|
||||||
// Cleanup au démontage.
|
// Cleanup au démontage UNIQUEMENT.
|
||||||
|
//
|
||||||
|
// ⚠ Bug subtil corrigé : `closeAll` dépend de `capture` (objet retourné
|
||||||
|
// par useAudioCapture, recréé à chaque render). Mettre `closeAll` en
|
||||||
|
// dépendance de cet effet ferait s'exécuter le cleanup à chaque render —
|
||||||
|
// donc fermer le WS dès la première transition d'état (ws.onopen →
|
||||||
|
// dispatch WS_OPENED → setState → re-render → cleanup → close).
|
||||||
|
//
|
||||||
|
// Pattern : ref qui tient toujours la dernière version de closeAll, effet
|
||||||
|
// de cleanup avec deps vides qui n'exécute la fonction qu'au démontage.
|
||||||
|
const closeAllRef = useRef(closeAll)
|
||||||
|
useEffect(() => {
|
||||||
|
closeAllRef.current = closeAll
|
||||||
|
})
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
closeAll()
|
closeAllRef.current()
|
||||||
}
|
}
|
||||||
}, [closeAll])
|
}, [])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
state,
|
state,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue