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 pingTimerRef = useRef<ReturnType<typeof setInterval> | 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 recording = useAudioRecording()
|
||||
|
|
@ -138,6 +143,12 @@ export function useT2LiveSession(opts: UseT2LiveSessionOptions): UseT2LiveSessio
|
|||
const closeAll = useCallback(() => {
|
||||
sessionEndedRef.current = true
|
||||
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()
|
||||
if (wsRef.current && wsRef.current.readyState !== WebSocket.CLOSED) {
|
||||
try {
|
||||
|
|
@ -274,14 +285,20 @@ export function useT2LiveSession(opts: UseT2LiveSessionOptions): UseT2LiveSessio
|
|||
)
|
||||
|
||||
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)
|
||||
setWarning(false)
|
||||
sessionEndedRef.current = false
|
||||
dispatch({ type: 'START_DIALOGUE' })
|
||||
|
||||
const token = await getAccessToken()
|
||||
if (localToken.cancelled) return
|
||||
if (!token) {
|
||||
cancelTokenRef.current = null
|
||||
setErrorMessage('Authentification requise.')
|
||||
dispatch({ type: 'ERROR', code: 4001 })
|
||||
navigate('/login')
|
||||
|
|
@ -293,11 +310,22 @@ export function useT2LiveSession(opts: UseT2LiveSessionOptions): UseT2LiveSessio
|
|||
ws = new WebSocket(buildWsUrl(token, sujetId))
|
||||
ws.binaryType = 'arraybuffer'
|
||||
} catch (err) {
|
||||
cancelTokenRef.current = null
|
||||
const message = err instanceof Error ? err.message : 'Connexion impossible'
|
||||
setErrorMessage(message)
|
||||
dispatch({ type: 'ERROR' })
|
||||
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
|
||||
|
||||
ws.onopen = () => {
|
||||
|
|
@ -356,12 +384,25 @@ export function useT2LiveSession(opts: UseT2LiveSessionOptions): UseT2LiveSessio
|
|||
dispatch({ type: 'END_REQUESTED' })
|
||||
}, [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(() => {
|
||||
return () => {
|
||||
closeAll()
|
||||
closeAllRef.current()
|
||||
}
|
||||
}, [closeAll])
|
||||
}, [])
|
||||
|
||||
return {
|
||||
state,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue