mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
fix(dictation): serialize audio flushes so stop awaits in-flight IPC
A timer-tick flushAudioBatch() whose native IPC is still awaiting could be left un-awaited by stopRecording()/cleanup(), which only awaited the flush they kicked off themselves. Stopping in that window let stop_dictation fire before an earlier flush's chunks were enqueued native-side, dropping late chunks and losing transcript words. Serialize flushes through a flushChainRef promise tail: each flush drains the batch synchronously (distinct FIFO payload) then chains its IPC send after any prior in-flight flush. stopRecording()/cleanup() await the chain tail (the empty-batch path now returns the tail), guaranteeing every started flush is fully enqueued before stop. Chain is reset per session in startRecording. Bump MessageComposer.tsx file-size override (+15) for the dictation wiring that pushed it 1033->1049, matching the existing load-bearing-growth pattern.
This commit is contained in:
parent
25339523ec
commit
5d847e5767
@@ -339,7 +339,10 @@ const overrides = new Map([
|
||||
// +3: onLinkShortcutRef wiring (ref decl + editor option + assignment) for
|
||||
// the ⌘K link-editor shortcut, mirroring the existing onEditLinkRef
|
||||
// pattern. Queued to split with the rest of this list.
|
||||
["src/features/messages/ui/MessageComposer.tsx", 1036],
|
||||
// +15 local-dictation integration: useLocalDictation wiring (mic toggle,
|
||||
// transcript-append handler, recording/transcribing state, stop-on-send/edit
|
||||
// guards). Load-bearing feature growth; queued to split with the rest.
|
||||
["src/features/messages/ui/MessageComposer.tsx", 1052],
|
||||
// global-agent-config: model-tuning section (BuzzAgentModelTuningFields via
|
||||
// EditAgentAdvancedFields) + providerValid gate + effectiveProvider derivation
|
||||
// + globalProvider threading into getPersonaProviderOptions. All load-bearing
|
||||
|
||||
@@ -84,6 +84,10 @@ export function useLocalDictation({
|
||||
const workletRef = useRef<AudioWorkletNode | null>(null);
|
||||
const batchTimerRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
const audioBatchRef = useRef<Float32Array[]>([]);
|
||||
// Tail of the serialized flush chain. Each flush appends its IPC work to
|
||||
// this promise so flushes run strictly one-after-another; awaiting the tail
|
||||
// guarantees every already-started flush's chunks are enqueued native-side.
|
||||
const flushChainRef = useRef<Promise<void>>(Promise.resolve());
|
||||
const unlistenTranscriptRef = useRef<UnlistenFn | null>(null);
|
||||
const unlistenStateRef = useRef<UnlistenFn | null>(null);
|
||||
const onRecordingStartRef = useRef(onRecordingStart);
|
||||
@@ -137,10 +141,17 @@ export function useLocalDictation({
|
||||
}, []);
|
||||
|
||||
/** Flush accumulated audio batch to the native STT engine. Returns a promise
|
||||
* that resolves once the IPC call completes (or immediately if nothing to flush). */
|
||||
* that resolves once the IPC call completes (or immediately if nothing to flush).
|
||||
*
|
||||
* Flushes are serialized through `flushChainRef`: the batch is drained
|
||||
* synchronously here (so each flush owns a distinct set of samples in FIFO
|
||||
* order), but the actual IPC send is chained after any prior in-flight
|
||||
* flush. This keeps chunks strictly ordered and lets `stopRecording`/
|
||||
* `cleanup` await the chain tail so an earlier timer-tick flush can't still
|
||||
* be enqueuing chunks when `stop_dictation` fires. */
|
||||
const flushAudioBatch = useCallback((): Promise<void> => {
|
||||
const batch = audioBatchRef.current;
|
||||
if (batch.length === 0) return Promise.resolve();
|
||||
if (batch.length === 0) return flushChainRef.current;
|
||||
|
||||
// Tag this flush with the session that owns the buffered audio. Captured
|
||||
// once here so every chunk of this batch carries the same session; native
|
||||
@@ -148,7 +159,10 @@ export function useLocalDictation({
|
||||
// flush from a just-stopped session can't leak into a newer session.
|
||||
const session = nativeSessionRef.current;
|
||||
|
||||
// Calculate total byte length and merge into a single buffer.
|
||||
// Calculate total byte length and merge into a single buffer. Drain the
|
||||
// batch synchronously so a concurrent timer tick can't grab the same
|
||||
// samples — the merged buffer captured here is this flush's exclusive
|
||||
// payload.
|
||||
let totalSamples = 0;
|
||||
for (const chunk of batch) {
|
||||
totalSamples += chunk.length;
|
||||
@@ -193,7 +207,12 @@ export function useLocalDictation({
|
||||
await invokeRawBinary("push_dictation_audio", payload).catch(() => {});
|
||||
}
|
||||
};
|
||||
return sendChunks();
|
||||
|
||||
// Chain this flush's IPC work after any prior in-flight flush so chunks
|
||||
// stay strictly ordered and awaiting the tail drains everything.
|
||||
const chained = flushChainRef.current.then(sendChunks);
|
||||
flushChainRef.current = chained;
|
||||
return chained;
|
||||
}, []);
|
||||
|
||||
const cleanup = useCallback(() => {
|
||||
@@ -267,6 +286,9 @@ export function useLocalDictation({
|
||||
// Reset any leftover audio buffer so a stale batch from a prior session
|
||||
// can't be flushed into this new session/draft.
|
||||
audioBatchRef.current = [];
|
||||
// Reset the flush chain so this session's flushes don't chain behind a
|
||||
// prior session's (already-settled) tail.
|
||||
flushChainRef.current = Promise.resolve();
|
||||
|
||||
setIsStarting(true);
|
||||
onRecordingStartRef.current?.();
|
||||
|
||||
Reference in New Issue
Block a user