fix(dictation): bail after worklet load if aborted; drop stale PCM on stop

Address two P2 review comments on useLocalDictation:

- Recheck startAbortedRef after audioWorklet.addModule() resolves. If
  stop/cancel fired during the module load, the start path previously
  continued to wire up the worklet, start the flush timer, and set
  isRecording=true, leaving dictation running after it was stopped.
  Now bails and tears down the mic/context it opened.

- Clear worklet.port.onmessage on stop/cleanup so PCM messages still
  queued on the main thread are dropped instead of appended to the reused
  audioBatchRef after the final flush. Also reset the batch on cleanup and
  at the start of each new recording so a stale batch can't leak into the
  next session/draft.
This commit is contained in:
npub13fn4ahfnvaa2qwylvegdgeajqs0mph6v4qsw4jcqnw4mjh3hzh2quuucm5
2026-07-11 16:19:21 +01:00
parent 3b182acc5e
commit 79da7cddef
@@ -164,11 +164,16 @@ export function useLocalDictation({
}
streamRef.current = null;
}
// Disconnect audio worklet.
// Disconnect audio worklet. Clear the port handler first so any PCM
// messages still queued on the main thread are dropped instead of
// appended to the (reused) audio batch.
if (workletRef.current) {
workletRef.current.port.onmessage = null;
workletRef.current.disconnect();
workletRef.current = null;
}
// Drop any audio still buffered so it can't leak into the next session.
audioBatchRef.current = [];
// Close audio context.
if (audioContextRef.current) {
void audioContextRef.current.close();
@@ -193,6 +198,9 @@ export function useLocalDictation({
// Clear abort flag for this new start attempt.
startAbortedRef.current = false;
// Reset any leftover audio buffer so a stale batch from a prior session
// can't be flushed into this new session/draft.
audioBatchRef.current = [];
setIsStarting(true);
onRecordingStartRef.current?.();
@@ -316,6 +324,18 @@ export function useLocalDictation({
URL.revokeObjectURL(blobUrl);
}
// Bail if stop/cancel was called while the worklet module was loading.
// Without this the worklet/flush timer would start and `isRecording`
// would be set true, leaving dictation running after it was stopped.
if (startAbortedRef.current) {
for (const track of stream.getTracks()) track.stop();
streamRef.current = null;
void audioContext.close();
audioContextRef.current = null;
invoke("stop_dictation").catch(() => {});
return;
}
const source = audioContext.createMediaStreamSource(stream);
const worklet = new AudioWorkletNode(audioContext, "dictation-processor");
workletRef.current = worklet;
@@ -382,6 +402,10 @@ export function useLocalDictation({
streamRef.current = null;
}
if (workletRef.current) {
// Clear the port handler so PCM messages still queued on the main
// thread are dropped rather than appended to the batch after the
// final flush below (and leaking into the next session).
workletRef.current.port.onmessage = null;
workletRef.current.disconnect();
workletRef.current = null;
}