zebra-spaces: fix worklet-load race that killed self-capture

Fox 2026-06-05 log: "self capture: worklet module load failed" at the
same instant a remote capture for the same publisher succeeded.

Root cause was a check in loadWhisperCaptureWorklet:
  if (whisperCaptureWorkletReady || whisperCaptureWorkletLoading)
    return Promise.resolve(whisperCaptureWorkletReady);

When TWO calls happen near-simultaneously (one from startSelfCapture,
one from startCaptureForUuid on toggle-on), the second caller sees
whisperCaptureWorkletLoading === true and gets back
Promise.resolve(false). Caller bails. By the time the actual load
finishes, the second caller already gave up.

Fix: cache the loading promise itself and return the same one to
every concurrent caller. They all await the same resolution; both
succeed when the module is ready. On error, the cached promise is
cleared so a later toggle can retry.

After this lands, self-capture should engage on every host that has
transcribe on. Telemetry's selfCap=1 will flip to 1.
This commit is contained in:
Russell Ballestrini 2026-06-05 13:34:05 -04:00
parent af6de11540
commit 8086627086
No known key found for this signature in database

View file

@ -2102,23 +2102,29 @@ registerProcessor('whisper-capture', WhisperCaptureProcessor);
`;
let whisperCaptureWorkletReady = false;
let whisperCaptureWorkletLoading = false;
let _whisperCaptureLoadPromise = null;
/* RACE FIX: previous version returned Promise.resolve(false) for any
* SECOND concurrent caller — startSelfCapture and startCaptureForUuid
* both fire on toggle-on and the loser got "not ready" instead of
* awaiting the in-flight load. Fox 2026-06-05 surfaced this as
* "self capture: worklet module load failed." Cache the loading
* promise so every caller awaits the same one. */
function loadWhisperCaptureWorklet(ctx){
if (whisperCaptureWorkletReady || whisperCaptureWorkletLoading) return Promise.resolve(whisperCaptureWorkletReady);
whisperCaptureWorkletLoading = true;
if (whisperCaptureWorkletReady) return Promise.resolve(true);
if (_whisperCaptureLoadPromise) return _whisperCaptureLoadPromise;
const blob = new Blob([WHISPER_CAPTURE_WORKLET_CODE], { type: 'application/javascript' });
const url = URL.createObjectURL(blob);
return ctx.audioWorklet.addModule(url).then(() => {
_whisperCaptureLoadPromise = ctx.audioWorklet.addModule(url).then(() => {
URL.revokeObjectURL(url);
whisperCaptureWorkletReady = true;
whisperCaptureWorkletLoading = false;
return true;
}).catch(e => {
URL.revokeObjectURL(url);
whisperCaptureWorkletLoading = false;
_whisperCaptureLoadPromise = null; /* allow retry on next call */
logLine('err', 'whisper-capture worklet load: '+e.message);
return false;
});
return _whisperCaptureLoadPromise;
}
/* Whisper inference runs in a dedicated Web Worker so ONNX Runtime
@ -7420,8 +7426,8 @@ logLine('', 'ready — pick a handle, type a rendezvous code, enter the space');
<footer style="margin:2.2rem auto 0;font-size:0.65rem;color:#999;line-height:1.7;word-break:break-all;font-family:monospace">
<span id="pi-seal" style="color:#777;cursor:default;user-select:none" title="">page integrity</span> &nbsp;·&nbsp; built <span class="stamp-date">2026-06-05</span><br>
md5 <span class="stamp-md5">9aa06e957f35ac7f764fb8c97bb3c634</span><br>
sha256 <span class="stamp-sha">46c487137a3aabfca6d136a0164676f4f07d3a727223fce109b42b339dae3eb5</span><br>
md5 <span class="stamp-md5">9870220482eb18365ec4f5c8f556d976</span><br>
sha256 <span class="stamp-sha">853b11dcfc8b3612fbe452d53df6cec037a125654ff858f79d891855f6396fad</span><br>
<span style="color:#bbb">hashes are of this page with these two fields zeroed — to verify, blank them and re-hash</span><br>
<span style="color:#bbb">one self-contained file — <strong>save a copy</strong> and verify against these hashes; point at your own servers with ?signal= and ?turncred=, or <a href="host-your-own.html" style="color:#999">host your own community</a></span>
</footer>