Files
ipx/web/src/events.ts
rays d6c0e3e7a6 Show that DeadAir is still working on an episode (#176)
A new download in a show with DeadAir on plays at full length until its
copy is made, and nothing said so. Each file now carries
deadair_pending while DeadAir is on for its show and the copy is still
to come (not for one heard through, which it passes over), and the
episode's line and details say "Cutting dead air…". The maker sends a
deadair_done event when it finishes a file, and the page takes the list
again, so the line becomes how much shorter the episode is without a
reload.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 19:05:18 +00:00

74 lines
4.1 KiB
TypeScript

/* ---------------- live events ---------------- */
let sse;
/// The feed list and the open list, taken again from the server. The events keep both current,
/// and one missed is a count wrong until a reload: reading an item sends none to your other
/// pages, so one read on another device stayed unread here, and an iPhone stops the stream while
/// the app is in the background, when everything is missed (#168).
const catchUp=(()=>{ let t; return ()=>{ clearTimeout(t); t=setTimeout(()=>{
loadFeeds(true).catch(()=>{});
if(S.feed) loadEntries().catch(()=>{});
},300); }; })();
document.addEventListener('visibilitychange',()=>{ if(!document.hidden) catchUp(); });
let dropped=false;
function connect(){
sse=new EventSource('/api/events');
// Back after a drop: whatever was sent meanwhile went to nobody.
sse.onopen=()=>{ if(dropped){ dropped=false; catchUp(); } };
const soon=(fn,ms=500)=>{ let t; return ()=>{ clearTimeout(t); t=setTimeout(fn,ms); }; };
const refreshEntries=soon(()=>{ if(S.feed) loadEntries(); });
// Every scan's events reach everyone; only this person's feeds are theirs to show or refresh.
const mine=id=>S.feeds.some(f=>f.id===id);
sse.onmessage=m=>{
let ev; try{ ev=JSON.parse(m.data) }catch{ return }
if(ev.ev==='progress'){
const pct=ev.total?ev.done/ev.total*100:0;
// Only the row actually downloading. Without the enclosure id this used to paint
// every pending bar at once, so adding a feed looked like it was fetching the lot.
const bar=document.querySelector<HTMLElement>(`.dlbar[data-bar="${ev.enclosure}"] i`);
if(bar){ bar.style.width=pct+'%'; bar.parentElement.classList.add('live'); }
$('#count') && ($('#count').textContent=`downloading ${ev.file} — ${pct.toFixed(0)}%`);
}
else if(ev.ev==='download_done'){
const bar=document.querySelector(`.dlbar[data-bar="${ev.enclosure}"]`);
// Said only for a file on screen, as one downloaded by hand is: the scheduled downloads of
// everyone's feeds used to announce themselves to everyone.
if(bar){ bar.classList.remove('live'); toast('Downloaded '+ev.path.split('/').pop()); }
refreshEntries();
}
// A copy finished: the episode's "Cutting dead air…" becomes how much shorter it is.
else if(ev.ev==='deadair_done'){
if(!mine(ev.feed)) return;
if(ev.saved_s>0&&document.querySelector('.dapend')) toast(`DeadAir is done: ${shorter(ev.saved_s)}`);
refreshEntries();
if(S.sel) loadEntries().then(()=>{ const e=cur(); if(e) showDetail(e); }).catch(()=>{});
}
else if(ev.ev==='download_error'){
const bar=document.querySelector(`.dlbar[data-bar="${ev.enclosure}"]`);
if(bar) bar.classList.remove('live');
toast('Download failed: '+ev.msg,true); refreshEntries();
}
// A spinner on the feed's row while it is checked, in place of a toast per feed (issue #37).
else if(ev.ev==='feed_start') setScanning(ev.feed,true);
else if(ev.ev==='feed_skip') setScanning(ev.feed,false);
else if(ev.ev==='feed_done'){
setScanning(ev.feed,false);
if(!mine(ev.feed)) return;
if(ev.feed===S.feed||S.feed===':all') refreshEntries();
}
// No toast: a scan of every feed raised one per failure, to everyone. The feed list's
// red ! marks the feed instead, and its page says why.
else if(ev.ev==='feed_error') setScanning(ev.feed,false);
// The server sends a feed's new row after anything changes it (counts, error, last check),
// only to those who subscribe; the page used to reload the whole list after each event.
else if(ev.ev==='feed_row') patchFeed(ev.row);
// Only a scan that checked something: the scheduler scans every minute, due or not, and
// every open page reloaded the whole list each time (#103).
// Rows came as feed_row events. ponytail: a feed an OPML drops stays in the list until the
// page reloads; reload the list here when a scan synced an OPML if that ever matters.
else if(ev.ev==='scan_done'){ scanning.clear(); paintScanning(); if(ev.feeds) refreshEntries(); }
};
sse.onerror=()=>{ sse.close(); dropped=true; setTimeout(connect,4000); };
}
connect();
loadFeeds();