From 8086627086580fb9d7d4846f4b3efd4a5dd4119e Mon Sep 17 00:00:00 2001 From: Russell Ballestrini Date: Fri, 5 Jun 2026 13:34:05 -0400 Subject: [PATCH] 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. --- web/zebra-spaces.html | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/web/zebra-spaces.html b/web/zebra-spaces.html index 6739acb..cee1daa 100644 --- a/web/zebra-spaces.html +++ b/web/zebra-spaces.html @@ -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');