mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
fix(dictation): scope all native stops to the owning session
The unmount cleanup handler (registered for every useLocalDictation instance) called stop_dictation unconditionally, so a non-recording composer unmounting (e.g. closing a thread reply composer) while another composer was actively dictating would tear down the singleton engine and drop the real recording's final transcript. Scope every frontend stop_dictation call to a specific session: - cleanup() passes nativeSessionRef.current (0 for an instance that never started; native session IDs start at 1, so a never-recorded composer's stop can never match the live session and correctly no-ops). - startRecording bail paths pass the just-started sessionId, robust against a concurrent restart during an await window. - error path and cancelRecording drop their redundant unconditional stop and rely on cleanup()'s scoped stop.
This commit is contained in:
parent
2d1c522789
commit
3fff53a9c2
@@ -149,8 +149,16 @@ export function useLocalDictation({
|
||||
|
||||
const cleanup = useCallback(() => {
|
||||
// Flush any remaining audio before teardown, then stop the native engine.
|
||||
// Scope the stop to THIS hook instance's session. `cleanup` runs as every
|
||||
// instance's unmount handler, so an unscoped stop here would let a
|
||||
// non-recording composer (e.g. a thread reply composer closing) tear down
|
||||
// the singleton engine owned by another composer that is actively
|
||||
// recording. `nativeSessionRef.current` is 0 for an instance that never
|
||||
// started a session (native IDs start at 1), so its stop can never match
|
||||
// the live session and correctly no-ops.
|
||||
const stoppingSession = nativeSessionRef.current;
|
||||
void flushAudioBatch().then(() => {
|
||||
invoke("stop_dictation").catch(() => {});
|
||||
invoke("stop_dictation", { session: stoppingSession }).catch(() => {});
|
||||
});
|
||||
// Stop batch timer.
|
||||
if (batchTimerRef.current) {
|
||||
@@ -212,7 +220,7 @@ export function useLocalDictation({
|
||||
|
||||
// Bail if aborted during engine start.
|
||||
if (startAbortedRef.current) {
|
||||
invoke("stop_dictation").catch(() => {});
|
||||
invoke("stop_dictation", { session: sessionId }).catch(() => {});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -243,7 +251,7 @@ export function useLocalDictation({
|
||||
// Bail if stop/cancel was called while we were awaiting.
|
||||
if (startAbortedRef.current) {
|
||||
unlistenTranscript();
|
||||
invoke("stop_dictation").catch(() => {});
|
||||
invoke("stop_dictation", { session: sessionId }).catch(() => {});
|
||||
return;
|
||||
}
|
||||
unlistenTranscriptRef.current = unlistenTranscript;
|
||||
@@ -272,7 +280,7 @@ export function useLocalDictation({
|
||||
if (startAbortedRef.current) {
|
||||
unlistenTranscript();
|
||||
unlistenState();
|
||||
invoke("stop_dictation").catch(() => {});
|
||||
invoke("stop_dictation", { session: sessionId }).catch(() => {});
|
||||
return;
|
||||
}
|
||||
unlistenStateRef.current = unlistenState;
|
||||
@@ -291,7 +299,7 @@ export function useLocalDictation({
|
||||
if (startAbortedRef.current) {
|
||||
for (const track of stream.getTracks()) track.stop();
|
||||
streamRef.current = null;
|
||||
invoke("stop_dictation").catch(() => {});
|
||||
invoke("stop_dictation", { session: sessionId }).catch(() => {});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -332,7 +340,7 @@ export function useLocalDictation({
|
||||
streamRef.current = null;
|
||||
void audioContext.close();
|
||||
audioContextRef.current = null;
|
||||
invoke("stop_dictation").catch(() => {});
|
||||
invoke("stop_dictation", { session: sessionId }).catch(() => {});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -355,9 +363,10 @@ export function useLocalDictation({
|
||||
setIsRecording(true);
|
||||
setIsTranscribing(true);
|
||||
} catch (error) {
|
||||
// Stop the native engine if it was started but a later step failed
|
||||
// (e.g. mic permission denied, AudioWorklet setup error).
|
||||
invoke("stop_dictation").catch(() => {});
|
||||
// Tear down and stop the native engine if it was started but a later
|
||||
// step failed (e.g. mic permission denied, AudioWorklet setup error).
|
||||
// `cleanup()` performs the session-scoped stop, so it only tears down
|
||||
// this instance's own session — never another composer's.
|
||||
cleanup();
|
||||
setIsRecording(false);
|
||||
setIsTranscribing(false);
|
||||
@@ -429,8 +438,10 @@ export function useLocalDictation({
|
||||
const cancelRecording = useCallback(() => {
|
||||
// Signal any in-flight startRecording to bail after its next await.
|
||||
startAbortedRef.current = true;
|
||||
// `cleanup()` performs the session-scoped native stop, so cancelling a
|
||||
// composer that isn't the active recorder can't tear down another
|
||||
// composer's session.
|
||||
cleanup();
|
||||
invoke("stop_dictation").catch(() => {});
|
||||
setIsRecording(false);
|
||||
setIsTranscribing(false);
|
||||
}, [cleanup]);
|
||||
|
||||
Reference in New Issue
Block a user