(jobId)
| 629 | |
| 630 | // SSE with a REST-poll fallback (mirrors the desktop's resilience, simplified). |
| 631 | function followExtraction(jobId) { |
| 632 | if (extractES) { extractES.close(); extractES = null; } |
| 633 | if (extractPoll) { clearInterval(extractPoll); extractPoll = null; } |
| 634 | |
| 635 | const startPolling = () => { |
| 636 | if (extractPoll) return; |
| 637 | extractPoll = setInterval(async () => { |
| 638 | try { |
| 639 | const r = await fetch(`/api/jobs/${jobId}`, { cache: "no-store" }); |
| 640 | if (!r.ok) throw new Error(String(r.status)); |
| 641 | if (_onExtractState(jobId, await r.json())) { clearInterval(extractPoll); extractPoll = null; } |
| 642 | } catch { /* keep polling */ } |
| 643 | }, 2500); |
| 644 | }; |
| 645 | |
| 646 | try { |
| 647 | const es = new EventSource(`/api/jobs/${jobId}/events`); |
| 648 | extractES = es; |
| 649 | es.onmessage = (ev) => { |
| 650 | let s; |
| 651 | try { s = JSON.parse(ev.data); } catch { return; } |
| 652 | if (_onExtractState(jobId, s)) { es.close(); extractES = null; } |
| 653 | }; |
| 654 | es.onerror = () => { es.close(); extractES = null; startPolling(); }; |
| 655 | } catch { |
| 656 | startPolling(); |
| 657 | } |
| 658 | } |
| 659 | |
| 660 | function miniPlayer() { |
| 661 | if (state.tab === "mixer" || !state.current) return ""; |
no test coverage detected