From 78704e40f2b6da708b06ce4a52a53afcf00ed59c Mon Sep 17 00:00:00 2001 From: rays Date: Wed, 14 Oct 2026 15:51:41 +0000 Subject: [PATCH] Page type-check: strict's flags that cost nothing, and catch variables as unknown (#235) why(e) gives a caught error's message; each catch read e.message as any. Co-Authored-By: Claude Opus 5.5 --- tsconfig.json | 6 ++++++ web/src/admin.ts | 16 ++++++++-------- web/src/dialogs.ts | 26 +++++++++++++------------- web/src/feedpage.ts | 4 ++-- web/src/items.ts | 6 +++--- web/src/player.ts | 2 +- web/src/util.ts | 4 +++- 7 files changed, 36 insertions(+), 28 deletions(-) diff --git a/tsconfig.json b/tsconfig.json index 9506878..aa46f77 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,7 +5,13 @@ "target": "es2022", "lib": ["es2022", "dom", "dom.iterable"], "noEmit": true, + // strict, a flag at a time (#235); noImplicitAny and strictNullChecks still to come. "strict": false, + "strictFunctionTypes": true, + "strictBindCallApply": true, + "noImplicitThis": true, + "alwaysStrict": true, + "useUnknownInCatchVariables": true, "skipLibCheck": true }, "include": ["web/src/*.ts"] diff --git a/web/src/admin.ts b/web/src/admin.ts index d725a01..bebb4a0 100644 --- a/web/src/admin.ts +++ b/web/src/admin.ts @@ -66,7 +66,7 @@ async function drawServer(){ max_age_days: Number($('#gage').value) || 0, art_cache_mb: Math.max(0, Number($('#gart').value) || 0)})}); toast('Settings saved'); - }catch(e){ toast(e.message, true); } + }catch(e){ toast(why(e), true); } }; } @@ -99,7 +99,7 @@ async function drawDirectorySettings(){ await api('/api/settings', {method: 'PATCH', body: JSON.stringify(patch)}); toast(said); drawDirectorySettings(); - }catch(e){ toast(e.message, true); } + }catch(e){ toast(why(e), true); } }; $('#dsave').onclick = () => { const patch: Record = {directory_url: $('#durl').value.trim(), directory_share: $('#dshare').checked}; @@ -115,7 +115,7 @@ async function drawDirectorySettings(){ async function drawDirectoryStatus(){ const el = $('#dstatus'); let s; - try{ s = await api('/api/directory-status'); }catch(e){ el.textContent = e.message; return; } + try{ s = await api('/api/directory-status'); }catch(e){ el.textContent = why(e); return; } if(!el.isConnected) return; const lines: string[] = []; if(!s.configured) lines.push('Not set up: it needs an address and a token.'); @@ -138,7 +138,7 @@ async function drawDirectoryStatus(){ async function drawDirectoryAdmin(){ const box = $('#dadmin'); let a; - try{ a = await api('/api/directory-admin'); }catch(e){ box.innerHTML = `

${esc(e.message)}

`; return; } + try{ a = await api('/api/directory-admin'); }catch(e){ box.innerHTML = `

${esc(why(e))}

`; return; } if(!box.isConnected) return; box.innerHTML = `

Servers

Each server sharing with the directory, by its token's name. Dropping one removes its @@ -170,7 +170,7 @@ async function drawDirectoryAdmin(){ toast('Unblocked. It comes back with the next snapshot that has it'); } drawDirectoryAdmin(); - }catch(e){ toast(e.message, true); } + }catch(e){ toast(why(e), true); } }; } @@ -201,7 +201,7 @@ async function drawAccounts(){ // Demoting yourself takes this page away; go back to the app rather than stay on a page // the server no longer answers. Only on success: a refusal's toast has to stay readable. if(u.name === me?.name){ location.href = '/'; return; } - }catch(e){ toast(e.message, true); } + }catch(e){ toast(why(e), true); } drawAccounts(); // on a refusal, this puts the checkbox back where the server left it }; for(const row of $$('#accounts [data-id]')){ @@ -218,7 +218,7 @@ async function drawAccounts(){ await api('/api/users', {method: 'POST', body: JSON.stringify({ name: $('#uname').value, password: $('#upass').value, admin: $('#uadmin').checked})}); toast('Added'); drawAccounts(); - }catch(e){ toast(e.message, true); } // keep what was typed + }catch(e){ toast(why(e), true); } // keep what was typed }; } @@ -281,7 +281,7 @@ async function pollLog(){ }else if(!logLines.length){ drawLog(); } }catch(e){ const box = $('#logbox'); - if(box) box.innerHTML = `

Lost contact with the daemon: ${esc(e.message)}

`; + if(box) box.innerHTML = `

Lost contact with the daemon: ${esc(why(e))}

`; } } function visibleLog(){ diff --git a/web/src/dialogs.ts b/web/src/dialogs.ts index 305cd39..e4bb214 100644 --- a/web/src/dialogs.ts +++ b/web/src/dialogs.ts @@ -30,7 +30,7 @@ $('#addFeed').onclick=()=>{ keywords:$('#nkw').value.split(',').map(s=>s.trim()).filter(Boolean)})}); closeModal(); toast(r.existing?`Already subscribed as ${r.id}`:`Added ${r.id}`); await loadFeeds(true); selectFeed(r.id); - }catch(e){ toast(e.message,true); $('#nsave').title='Add feed'; $('#nsave').disabled=false; } + }catch(e){ toast(why(e),true); $('#nsave').title='Add feed'; $('#nsave').disabled=false; } }; }; @@ -73,7 +73,7 @@ async function subscribeListed(p){ const r=await api(`/api/popular/${encodeURIComponent(p.id)}`,{method:'POST'}); toast(`Subscribed to ${NAME(p)}`); await loadFeeds(true); selectFeed(r.id); - }catch(e){ toast(e.message,true); } + }catch(e){ toast(why(e),true); } } // Where you are in the Directory: what kind of feed, which category and subcategory, in what @@ -104,7 +104,7 @@ async function renderDirectory(url,box){ // Each visit starts at the front, as going back to a tab does. dirCat=null; dirSub=null; dirShow=null; dirError=''; - try{ dirRows=await api(url)||[]; }catch(e){ dirRows=[]; dirError=e.message; } + try{ dirRows=await api(url)||[]; }catch(e){ dirRows=[]; dirError=why(e); } drawDirectory(box); return dirRows.length; } @@ -208,7 +208,7 @@ async function fillShow(box,p){ const list=$('#dshow',box), about=$('#dabout',box); let got; try{ got=await api(`/api/directory/${encodeURIComponent(p.id)}`); } - catch(e){ list.innerHTML=`

${esc(e.message)}

`; return; } + catch(e){ list.innerHTML=`

${esc(why(e))}

`; return; } // Gone back, or on to another, while it loaded. if(dirShow!==p.id||!list.isConnected) return; // Some feeds give their title again as their description, which says nothing new. @@ -233,12 +233,12 @@ async function fillShow(box,p){ +``); $('[data-a="drefresh"]',acts).onclick=async()=>{ try{ await api(`/api/directory/${encodeURIComponent(p.id)}/refresh`,{method:'POST'}); toast('Checked'); fillShow(box,p); } - catch(e){ toast(e.message,true); } + catch(e){ toast(why(e),true); } }; $('[data-a="dremove"]',acts).onclick=async()=>{ if(!confirm(`Remove ${NAME(p)} from the directory? It is blocked, so no server's snapshot brings it back. Admin, Directory can unblock it.`)) return; try{ await api(`/api/directory/${encodeURIComponent(p.id)}`,{method:'DELETE'}); } - catch(e){ return toast(e.message,true); } + catch(e){ return toast(why(e),true); } toast(`Removed ${NAME(p)} from the directory`); dirRows=dirRows.filter(r=>r.id!==p.id); dirShow=null; drawDirectory(box); @@ -347,7 +347,7 @@ async function forget(e){ try{ await api(`/api/entries/${encodeURIComponent(e.feed_id)}/${encodeURIComponent(e.guid)}/position`, {method:'POST',body:JSON.stringify({secs:0})}); - }catch(err){ toast(err.message,true); } + }catch(err){ toast(why(err),true); } if(S.feed===':listening') renderListed(VIEWS[':listening']); } @@ -446,7 +446,7 @@ async function prefsModal(){ `); $('#tokcopy').onclick=()=>copyText(t.token,$('#tokcopy')); $('#tokval').select(); - }catch(err){ toast(err.message,true); } + }catch(err){ toast(why(err),true); } }; $('#sblock').onchange=async e=>{ const blocked=splitWords(e.target.value); @@ -454,7 +454,7 @@ async function prefsModal(){ await api('/api/me',{method:'PATCH',body:JSON.stringify({blocked})}); if(S.me) S.me.blocked=blocked; toast('Saved'); await loadFeeds(true); loadEntries(); - }catch(err){ toast(err.message,true); } + }catch(err){ toast(why(err),true); } }; } @@ -467,7 +467,7 @@ async function drawTokens(){ `).join(''); for(const b of $$('[data-tok]',box)) b.onclick=async()=>{ try{ await api(`/api/tokens/${b.dataset.tok}`,{method:'DELETE'}); toast('Revoked'); drawTokens(); } - catch(err){ toast(err.message,true); } + catch(err){ toast(why(err),true); } }; } @@ -664,7 +664,7 @@ function settingsModal(f, newUrl?: string){ await api(`/api/feeds/${encodeURIComponent(f.id)}`,{method:'PATCH',body:JSON.stringify(patch)}); closeModal(); toast('Saved. It applies from the next check.'); await loadFeeds(true); renderFeed(); loadEntries(); - }catch(e){ toast(e.message,true); } + }catch(e){ toast(why(e),true); } }; } @@ -681,7 +681,7 @@ function downloadLatestModal(f){ const r=await api(`/api/feeds/${encodeURIComponent(f.id)}/download-latest`, {method:'POST',body:JSON.stringify({count:n})}); closeModal(); toast(`Queued ${r.queued} item${r.queued===1?'':'s'}`); - }catch(e){ toast(e.message,true); } + }catch(e){ toast(why(e),true); } }; } @@ -724,7 +724,7 @@ function opmlModal(){ try{ const r=await api('/api/opml',{method:'POST',body:JSON.stringify({xml})}); letGo(); closeModal(); toast(`Subscribed to ${r.added} feed(s)`+(r.already?`, ${r.already} you already had`:'')); loadFeeds(true); - }catch(e){ letGo(); toast(e.message,true); } + }catch(e){ letGo(); toast(why(e),true); } }; } diff --git a/web/src/feedpage.ts b/web/src/feedpage.ts index 6990a86..386b8a3 100644 --- a/web/src/feedpage.ts +++ b/web/src/feedpage.ts @@ -170,7 +170,7 @@ async function feedAction(a,f){ try{ await api(`/api/feeds/${encodeURIComponent(f.id)}`,{method:'PATCH',body:JSON.stringify({pinned:!f.pinned})}); await loadFeeds(true); renderFeed(); - }catch(e){ toast(e.message,true); } + }catch(e){ toast(why(e),true); } } if(a==='settings') settingsModal(f); if(a==='share') share(f.title||f.id, f.url, $('#content .acts [data-a="share"]')); @@ -187,7 +187,7 @@ async function allAction(a){ try{ const r=await api('/api/read-all',{method:'POST'}); toast(`Marked ${r.marked} read`); await loadFeeds(true); renderFeed(); loadEntries(); - }catch(e){ toast(e.message,true); } + }catch(e){ toast(why(e),true); } } } diff --git a/web/src/items.ts b/web/src/items.ts index 05b37da..cd5e650 100644 --- a/web/src/items.ts +++ b/web/src/items.ts @@ -396,12 +396,12 @@ async function epAction(a: string, e, el, encId?: number){ await api(`/api/enclosures/${enc.id}`,{method:'DELETE'}); }catch(err){ // 409: somebody else has it starred or unplayed. Their reason, their words. - if(!/one copy of this file/.test(err.message)) throw err; - if(!confirm(err.message+'\n\nDelete it anyway?')) return; + if(!/one copy of this file/.test(why(err))) throw err; + if(!confirm(why(err)+'\n\nDelete it anyway?')) return; await api(`/api/enclosures/${enc.id}?force=true`,{method:'DELETE'}); } toast('Deleted'); loadEntries(); loadFeeds(true); } - }catch(err){ toast(err.message,true); } + }catch(err){ toast(why(err),true); } } diff --git a/web/src/player.ts b/web/src/player.ts index 9902773..8fe7a9c 100644 --- a/web/src/player.ts +++ b/web/src/player.ts @@ -177,7 +177,7 @@ $('#pdead').onclick=async()=>{ const f=S.feeds.find(x=>x.id===player.feed); if(!f) return; const on=!f.deadair; try{ await api(`/api/feeds/${encodeURIComponent(f.id)}`,{method:'PATCH',body:JSON.stringify({deadair:on})}); } - catch(err){ toast(err.message,true); return; } + catch(err){ toast(why(err),true); return; } f.deadair=on; paintDead(); const switched=await switchVersion(); toast(!on?'DeadAir is off for this show':switched?'DeadAir is cutting the dead air from this show' diff --git a/web/src/util.ts b/web/src/util.ts index 37416df..017aaff 100644 --- a/web/src/util.ts +++ b/web/src/util.ts @@ -3,6 +3,8 @@ // smoke test, not the type checker, is what makes sure a selector exists. const $ = (s: string, r: ParentNode = document): any => r.querySelector(s); const $$ = (s: string, r: ParentNode = document): any[] => [...r.querySelectorAll(s)]; +/// What went wrong, from whatever a catch caught: the page throws Errors, but a catch cannot know. +const why = (e: unknown): string => e instanceof Error ? e.message : String(e); const esc = s => (s??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); // Icons: Font Awesome Free 7.3.1 by @fontawesome - https://fontawesome.com // License - https://fontawesome.com/license/free (Icons: CC BY 4.0). Embedded as SVG, only the @@ -151,7 +153,7 @@ async function share(title: string, url: string, btn?){ if(navigator.share){ // Cancelling the sheet rejects too, and is not a failure worth a word. try{ await navigator.share({title, url}); } - catch(e){ if(e.name!=='AbortError') toast(e.message,true); } + catch(e){ if((e as Error)?.name!=='AbortError') toast(why(e),true); } return; } await copyText(url,btn);