Files
VoiceStudio/frontend/src/utils/format.js
T
b1bf00a257 fix: prevent probeAudioDuration from hanging on unresolved Promise (#1162)
* fix: prevent probeAudioDuration from hanging on unresolved Promise

The original Promise constructor only accepted `resolve` — no `reject`
callback. The `error` event handler called `resolve(null)` instead of
rejecting. If the Audio element never emits `loadedmetadata` or
`error` (rare browser conditions, GC races, invalid blob URLs), the
Promise hangs forever with no settlement path.

Added:
- 10-second timeout that rejects if neither event fires
- Proper rejection on error event
- `{ once: true }` on listeners to prevent double-invocation
- Dedicated cleanup function

* fix: settle probeAudioDuration with null on error/timeout instead of rejecting

The 10s timeout stays (a media element that never fires any event
genuinely hung this promise forever — verified). But both failure paths
now resolve(null) rather than reject: the only caller, ingestRefAudio,
awaits without a try/catch, and a clip this webview can't decode must
still be accepted — the backend decodes it with ffmpeg (Tauri WebKit
lacks several codecs). Regression test covers all four behaviors,
fail-before verified against the reject() version.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* style: oxfmt pass on format.js

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: mergetest <nizam4103@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 18:28:46 +05:30

42 lines
1.2 KiB
JavaScript

export function formatTime(s) {
const m = Math.floor(s / 60);
const sec = (s % 60).toFixed(1);
return `${m}:${sec.padStart(4, '0')}`;
}
// Contract: settles with a number or null, NEVER rejects. null means
// "duration unknown — keep the file": callers (ingestRefAudio) have no
// try/catch and must still accept clips this webview can't decode, because
// the backend decodes them with ffmpeg (Tauri WebKit lacks several codecs).
// The timeout guards against media elements that never fire any event.
export async function probeAudioDuration(file) {
return new Promise((resolve) => {
const url = URL.createObjectURL(file);
const a = new Audio();
const cleanup = () => URL.revokeObjectURL(url);
const timeout = setTimeout(() => {
cleanup();
resolve(null);
}, 10000);
a.addEventListener(
'loadedmetadata',
() => {
clearTimeout(timeout);
cleanup();
resolve(isFinite(a.duration) ? a.duration : null);
},
{ once: true },
);
a.addEventListener(
'error',
() => {
clearTimeout(timeout);
cleanup();
resolve(null);
},
{ once: true },
);
a.src = url;
});
}