Currently Listening and the Directory open at once, or with a spinner (#248)

Both fetched on every visit and drew only when the answer came: Currently Listening took 1.3 s.
fetchPage keeps their answers by address, as the item lists are kept, sharing a request already
on its way; both draw from what is kept at once and again only if the refresh changed them, and
show the spinner with nothing kept. They are fetched ahead with the lists when the page loads,
and when the pointer or a finger reaches their row. Removing an episode from Currently Listening
forgets its kept copy, which would otherwise show it again until the refresh. The preload loop
takes a failed fetch as done instead of leaving an unhandled rejection.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-15 15:24:25 +00:00
parent 6640ca9612
commit cdf2c98f9f
4 changed files with 83 additions and 20 deletions

View File

@@ -21,14 +21,30 @@ function keepList(key: string, r: ListPage){
/// Fetches a list's first page ahead of a click: when the page has loaded, and when the pointer
/// reaches a feed or a filter. Not one kept already or on its way.
function prefetch(feed: string, filter=S.filter){
// A folder's page lists its feeds, not items.
if(VIEWS[feed]?.url||S.feeds.some(c=>c.group===feed)) return;
// Currently Listening and the Directory draw from their own address; a folder's page lists its
// feeds, not items.
const url=VIEWS[feed]?.url;
if(url) return prefetchPage(url);
if(S.feeds.some(c=>c.group===feed)) return;
const key=listKey(feed,filter);
if(lists.has(key)||fetching.has(key)) return;
const got: Promise<ListPage>=api(listUrl(feed,filter));
fetching.set(key,got);
got.then(r=>keepList(key,r),()=>{}).finally(()=>fetching.delete(key));
}
/// Other answers a view draws from, kept by address as the lists are (#248): Currently Listening
/// and the Directory's catalogue. A request already on its way is shared, not sent twice.
const pages=new Map<string,any>();
const pageFetching=new Map<string,Promise<any>>();
function fetchPage(url: string): Promise<any>{
const on=pageFetching.get(url); if(on) return on;
const got=api(url).then(r=>{ pages.delete(url); pages.set(url,r); if(pages.size>LISTS_KEPT) pages.delete(pages.keys().next().value!); return r; });
pageFetching.set(url,got);
got.finally(()=>pageFetching.delete(url)).catch(()=>{});
return got;
}
const prefetchPage=(url: string)=>{ if(!pages.has(url)) fetchPage(url).catch(()=>{}); };
/// A feed changed (a scan, a read elsewhere): its kept lists, and every one of All Subscriptions,
/// are fetched again when next wanted.
function forgetLists(feed: string){
@@ -37,8 +53,14 @@ function forgetLists(feed: string){
/// What is likely to be opened next, fetched two at a time once the page is idle: All Subscriptions
/// and the feeds with something unread, most first.
function preloadLists(){
const want=[':all',...[...S.feeds].filter(f=>f.unread>0&&!f.group).sort((a,b)=>b.unread-a.unread).slice(0,20).map(f=>f.id)];
const next=()=>{ const f=want.shift(); if(!f) return; prefetch(f); (fetching.get(listKey(f))||Promise.resolve()).finally(next); };
const want=[':all',':listening',':directory',...[...S.feeds].filter(f=>f.unread>0&&!f.group).sort((a,b)=>b.unread-a.unread).slice(0,20).map(f=>f.id)];
const next=()=>{
const f=want.shift(); if(!f) return;
prefetch(f);
const url=VIEWS[f]?.url, on=url ? pageFetching.get(url) : fetching.get(listKey(f));
// On to the next when this one is in, failed or not: a failure here is nobody's to see.
if(on) on.then(next,next); else next();
};
const go=()=>{ next(); next(); };
'requestIdleCallback' in window ? (window as any).requestIdleCallback(go,{timeout:2000}) : setTimeout(go,300);
}
@@ -92,8 +114,8 @@ function showList(r: ListPage, append: boolean, asked: number){
renderEntries();
}
/// A list being waited for: said with a spinner, so the click is seen to have been heard.
function listLoading(){
const box=$('#eps'); if(!box) return;
function listLoading(box=$('#eps')){
if(!box) return;
box.innerHTML='<p class="listload" role="status">Loading…</p>';
}
function renderEntries(){