From b14e9c91c6b7b0dbe7469c91b166744f6bc00a3c Mon Sep 17 00:00:00 2001 From: Enrico Ros Date: Thu, 11 Sep 2025 10:41:45 -0700 Subject: [PATCH] Speech Recognition: add dispose (unmounts) as a one-way street --- .../speechrecognition/WebSpeechApiEngine.ts | 30 ++++++++++++++++--- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/src/common/components/speechrecognition/WebSpeechApiEngine.ts b/src/common/components/speechrecognition/WebSpeechApiEngine.ts index 9c21d3bd6..a40f4cae3 100644 --- a/src/common/components/speechrecognition/WebSpeechApiEngine.ts +++ b/src/common/components/speechrecognition/WebSpeechApiEngine.ts @@ -18,6 +18,7 @@ export class WebSpeechApiEngine implements IRecognitionEngine { private inactivityTimeoutId: ReturnType | null; private results: SpeechResult; private withinBeginEnd: boolean; + private disposed: boolean; constructor( @@ -36,6 +37,7 @@ export class WebSpeechApiEngine implements IRecognitionEngine { this.inactivityTimeoutId = null; this.results = createSpeechRecognitionResults(); this.withinBeginEnd = false; + this.disposed = false; // create the SpeechRecognition instance @@ -50,14 +52,27 @@ export class WebSpeechApiEngine implements IRecognitionEngine { this._api.continuous = true; // bind event handlers - this._api.onaudiostart = () => setState({ hasAudio: true }); - this._api.onaudioend = () => setState({ hasAudio: false }); + this._api.onaudiostart = () => { + if (this.disposed) return; + setState({ hasAudio: true }); + }; + this._api.onaudioend = () => { + if (this.disposed) return; + setState({ hasAudio: false }); + }; - this._api.onspeechstart = () => setState({ hasSpeech: true }); - this._api.onspeechend = () => setState({ hasSpeech: false }); + this._api.onspeechstart = () => { + if (this.disposed) return; + setState({ hasSpeech: true }); + }; + this._api.onspeechend = () => { + if (this.disposed) return; + setState({ hasSpeech: false }); + }; this._api.onstart = () => { + if (this.disposed) return; this.withinBeginEnd = true; // instant setState({ isActive: true }); // delayed @@ -71,6 +86,7 @@ export class WebSpeechApiEngine implements IRecognitionEngine { }; this._api.onend = () => { + if (this.disposed) return; this._clearInactivityTimeout(); this.withinBeginEnd = false; // instant @@ -94,6 +110,7 @@ export class WebSpeechApiEngine implements IRecognitionEngine { }; this._api.onerror = (event: any) => { + if (this.disposed) return; let errorMessage; switch (event.error) { case 'no-speech': @@ -131,6 +148,7 @@ export class WebSpeechApiEngine implements IRecognitionEngine { }; this._api.onresult = (event: ISpeechRecognitionEvent) => { + if (this.disposed) return; if (!event?.results?.length) return; // coalesce all the final pieces into a cohesive string @@ -178,6 +196,9 @@ export class WebSpeechApiEngine implements IRecognitionEngine { } dispose() { + // Mark as disposed to prevent any future callback execution + this.disposed = true; + // Clear any inactivity timeout to prevent it from running after unmount this._clearInactivityTimeout(); @@ -219,6 +240,7 @@ export class WebSpeechApiEngine implements IRecognitionEngine { private _reloadInactivityTimeout(timeoutMs: number, doneReason: SpeechDoneReason) { this._clearInactivityTimeout(); this.inactivityTimeoutId = setTimeout(() => { + if (this.disposed) return; this.inactivityTimeoutId = null; this.results.doneReason = doneReason; this._api.stop();