After a feed was checked, failed or downloaded a file, and after every item read, the page fetched /api/feeds whole, about 60 ms for 160 rows, though one row had changed. The live event stream now knows who is connected and, after an event that changes a feed, sends that person its row (feed_row), built by the same code as the list (feed_rows, with Db::feed_list asked for one feed). Marking an item read answers with the feed's row. The page puts the row in place and redraws once a frame. A routine skip of a feed not due sends nothing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
55 lines
3.0 KiB
TypeScript
55 lines
3.0 KiB
TypeScript
/* ---------------- live events ---------------- */
|
|
let sse;
|
|
function connect(){
|
|
sse=new EventSource('/api/events');
|
|
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();
|
|
}
|
|
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(); setTimeout(connect,4000); };
|
|
}
|
|
connect();
|
|
loadFeeds();
|