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 <noreply@anthropic.com>
This commit is contained in:
2026-10-14 15:51:41 +00:00
parent f20660a7d5
commit 78704e40f2
7 changed files with 36 additions and 28 deletions

View File

@@ -5,7 +5,13 @@
"target": "es2022", "target": "es2022",
"lib": ["es2022", "dom", "dom.iterable"], "lib": ["es2022", "dom", "dom.iterable"],
"noEmit": true, "noEmit": true,
// strict, a flag at a time (#235); noImplicitAny and strictNullChecks still to come.
"strict": false, "strict": false,
"strictFunctionTypes": true,
"strictBindCallApply": true,
"noImplicitThis": true,
"alwaysStrict": true,
"useUnknownInCatchVariables": true,
"skipLibCheck": true "skipLibCheck": true
}, },
"include": ["web/src/*.ts"] "include": ["web/src/*.ts"]

View File

@@ -66,7 +66,7 @@ async function drawServer(){
max_age_days: Number($('#gage').value) || 0, max_age_days: Number($('#gage').value) || 0,
art_cache_mb: Math.max(0, Number($('#gart').value) || 0)})}); art_cache_mb: Math.max(0, Number($('#gart').value) || 0)})});
toast('Settings saved'); 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)}); await api('/api/settings', {method: 'PATCH', body: JSON.stringify(patch)});
toast(said); toast(said);
drawDirectorySettings(); drawDirectorySettings();
}catch(e){ toast(e.message, true); } }catch(e){ toast(why(e), true); }
}; };
$('#dsave').onclick = () => { $('#dsave').onclick = () => {
const patch: Record<string, unknown> = {directory_url: $('#durl').value.trim(), directory_share: $('#dshare').checked}; const patch: Record<string, unknown> = {directory_url: $('#durl').value.trim(), directory_share: $('#dshare').checked};
@@ -115,7 +115,7 @@ async function drawDirectorySettings(){
async function drawDirectoryStatus(){ async function drawDirectoryStatus(){
const el = $('#dstatus'); const el = $('#dstatus');
let s; 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; if(!el.isConnected) return;
const lines: string[] = []; const lines: string[] = [];
if(!s.configured) lines.push('Not set up: it needs an address and a token.'); 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(){ async function drawDirectoryAdmin(){
const box = $('#dadmin'); const box = $('#dadmin');
let a; let a;
try{ a = await api('/api/directory-admin'); }catch(e){ box.innerHTML = `<p class="hint">${esc(e.message)}</p>`; return; } try{ a = await api('/api/directory-admin'); }catch(e){ box.innerHTML = `<p class="hint">${esc(why(e))}</p>`; return; }
if(!box.isConnected) return; if(!box.isConnected) return;
box.innerHTML = `<h2>Servers</h2> box.innerHTML = `<h2>Servers</h2>
<p class="hint">Each server sharing with the directory, by its token's name. Dropping one removes its <p class="hint">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'); toast('Unblocked. It comes back with the next snapshot that has it');
} }
drawDirectoryAdmin(); 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 // 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. // the server no longer answers. Only on success: a refusal's toast has to stay readable.
if(u.name === me?.name){ location.href = '/'; return; } 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 drawAccounts(); // on a refusal, this puts the checkbox back where the server left it
}; };
for(const row of $$('#accounts [data-id]')){ for(const row of $$('#accounts [data-id]')){
@@ -218,7 +218,7 @@ async function drawAccounts(){
await api('/api/users', {method: 'POST', body: JSON.stringify({ await api('/api/users', {method: 'POST', body: JSON.stringify({
name: $('#uname').value, password: $('#upass').value, admin: $('#uadmin').checked})}); name: $('#uname').value, password: $('#upass').value, admin: $('#uadmin').checked})});
toast('Added'); drawAccounts(); 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(); } }else if(!logLines.length){ drawLog(); }
}catch(e){ }catch(e){
const box = $('#logbox'); const box = $('#logbox');
if(box) box.innerHTML = `<p class="empty">Lost contact with the daemon: ${esc(e.message)}</p>`; if(box) box.innerHTML = `<p class="empty">Lost contact with the daemon: ${esc(why(e))}</p>`;
} }
} }
function visibleLog(){ function visibleLog(){

View File

@@ -30,7 +30,7 @@ $('#addFeed').onclick=()=>{
keywords:$('#nkw').value.split(',').map(s=>s.trim()).filter(Boolean)})}); keywords:$('#nkw').value.split(',').map(s=>s.trim()).filter(Boolean)})});
closeModal(); toast(r.existing?`Already subscribed as ${r.id}`:`Added ${r.id}`); closeModal(); toast(r.existing?`Already subscribed as ${r.id}`:`Added ${r.id}`);
await loadFeeds(true); selectFeed(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'}); const r=await api(`/api/popular/${encodeURIComponent(p.id)}`,{method:'POST'});
toast(`Subscribed to ${NAME(p)}`); toast(`Subscribed to ${NAME(p)}`);
await loadFeeds(true); selectFeed(r.id); 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 // 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. // Each visit starts at the front, as going back to a tab does.
dirCat=null; dirSub=null; dirShow=null; dirCat=null; dirSub=null; dirShow=null;
dirError=''; 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); drawDirectory(box);
return dirRows.length; return dirRows.length;
} }
@@ -208,7 +208,7 @@ async function fillShow(box,p){
const list=$('#dshow',box), about=$('#dabout',box); const list=$('#dshow',box), about=$('#dabout',box);
let got; let got;
try{ got=await api(`/api/directory/${encodeURIComponent(p.id)}`); } try{ got=await api(`/api/directory/${encodeURIComponent(p.id)}`); }
catch(e){ list.innerHTML=`<p class="hint">${esc(e.message)}</p>`; return; } catch(e){ list.innerHTML=`<p class="hint">${esc(why(e))}</p>`; return; }
// Gone back, or on to another, while it loaded. // Gone back, or on to another, while it loaded.
if(dirShow!==p.id||!list.isConnected) return; if(dirShow!==p.id||!list.isConnected) return;
// Some feeds give their title again as their description, which says nothing new. // Some feeds give their title again as their description, which says nothing new.
@@ -233,12 +233,12 @@ async function fillShow(box,p){
+`<button class="btn ico danger" data-a="dremove" title="Remove from the directory" aria-label="Remove from the directory">${ICON.trash}</button>`); +`<button class="btn ico danger" data-a="dremove" title="Remove from the directory" aria-label="Remove from the directory">${ICON.trash}</button>`);
$('[data-a="drefresh"]',acts).onclick=async()=>{ $('[data-a="drefresh"]',acts).onclick=async()=>{
try{ await api(`/api/directory/${encodeURIComponent(p.id)}/refresh`,{method:'POST'}); toast('Checked'); fillShow(box,p); } 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()=>{ $('[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; 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'}); } 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`); toast(`Removed ${NAME(p)} from the directory`);
dirRows=dirRows.filter(r=>r.id!==p.id); dirShow=null; dirRows=dirRows.filter(r=>r.id!==p.id); dirShow=null;
drawDirectory(box); drawDirectory(box);
@@ -347,7 +347,7 @@ async function forget(e){
try{ try{
await api(`/api/entries/${encodeURIComponent(e.feed_id)}/${encodeURIComponent(e.guid)}/position`, await api(`/api/entries/${encodeURIComponent(e.feed_id)}/${encodeURIComponent(e.guid)}/position`,
{method:'POST',body:JSON.stringify({secs:0})}); {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']); if(S.feed===':listening') renderListed(VIEWS[':listening']);
} }
@@ -446,7 +446,7 @@ async function prefsModal(){
<button class="btn ico" id="tokcopy" title="Copy" aria-label="Copy">${ICON.copy}</button></div></div>`); <button class="btn ico" id="tokcopy" title="Copy" aria-label="Copy">${ICON.copy}</button></div></div>`);
$('#tokcopy').onclick=()=>copyText(t.token,$('#tokcopy')); $('#tokcopy').onclick=()=>copyText(t.token,$('#tokcopy'));
$('#tokval').select(); $('#tokval').select();
}catch(err){ toast(err.message,true); } }catch(err){ toast(why(err),true); }
}; };
$('#sblock').onchange=async e=>{ $('#sblock').onchange=async e=>{
const blocked=splitWords(e.target.value); const blocked=splitWords(e.target.value);
@@ -454,7 +454,7 @@ async function prefsModal(){
await api('/api/me',{method:'PATCH',body:JSON.stringify({blocked})}); await api('/api/me',{method:'PATCH',body:JSON.stringify({blocked})});
if(S.me) S.me.blocked=blocked; if(S.me) S.me.blocked=blocked;
toast('Saved'); await loadFeeds(true); loadEntries(); 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(){
<button class="btn ico" data-tok="${t.id}" title="Revoke ${esc(t.name)}" aria-label="Revoke ${esc(t.name)}">${ICON.trash}</button></div>`).join(''); <button class="btn ico" data-tok="${t.id}" title="Revoke ${esc(t.name)}" aria-label="Revoke ${esc(t.name)}">${ICON.trash}</button></div>`).join('');
for(const b of $$('[data-tok]',box)) b.onclick=async()=>{ for(const b of $$('[data-tok]',box)) b.onclick=async()=>{
try{ await api(`/api/tokens/${b.dataset.tok}`,{method:'DELETE'}); toast('Revoked'); drawTokens(); } 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)}); await api(`/api/feeds/${encodeURIComponent(f.id)}`,{method:'PATCH',body:JSON.stringify(patch)});
closeModal(); toast('Saved. It applies from the next check.'); closeModal(); toast('Saved. It applies from the next check.');
await loadFeeds(true); renderFeed(); loadEntries(); 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`, const r=await api(`/api/feeds/${encodeURIComponent(f.id)}/download-latest`,
{method:'POST',body:JSON.stringify({count:n})}); {method:'POST',body:JSON.stringify({count:n})});
closeModal(); toast(`Queued ${r.queued} item${r.queued===1?'':'s'}`); 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{ try{
const r=await api('/api/opml',{method:'POST',body:JSON.stringify({xml})}); 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); 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); }
}; };
} }

View File

@@ -170,7 +170,7 @@ async function feedAction(a,f){
try{ try{
await api(`/api/feeds/${encodeURIComponent(f.id)}`,{method:'PATCH',body:JSON.stringify({pinned:!f.pinned})}); await api(`/api/feeds/${encodeURIComponent(f.id)}`,{method:'PATCH',body:JSON.stringify({pinned:!f.pinned})});
await loadFeeds(true); renderFeed(); await loadFeeds(true); renderFeed();
}catch(e){ toast(e.message,true); } }catch(e){ toast(why(e),true); }
} }
if(a==='settings') settingsModal(f); if(a==='settings') settingsModal(f);
if(a==='share') share(f.title||f.id, f.url, $('#content .acts [data-a="share"]')); if(a==='share') share(f.title||f.id, f.url, $('#content .acts [data-a="share"]'));
@@ -187,7 +187,7 @@ async function allAction(a){
try{ try{
const r=await api('/api/read-all',{method:'POST'}); const r=await api('/api/read-all',{method:'POST'});
toast(`Marked ${r.marked} read`); await loadFeeds(true); renderFeed(); loadEntries(); toast(`Marked ${r.marked} read`); await loadFeeds(true); renderFeed(); loadEntries();
}catch(e){ toast(e.message,true); } }catch(e){ toast(why(e),true); }
} }
} }

View File

@@ -396,12 +396,12 @@ async function epAction(a: string, e, el, encId?: number){
await api(`/api/enclosures/${enc.id}`,{method:'DELETE'}); await api(`/api/enclosures/${enc.id}`,{method:'DELETE'});
}catch(err){ }catch(err){
// 409: somebody else has it starred or unplayed. Their reason, their words. // 409: somebody else has it starred or unplayed. Their reason, their words.
if(!/one copy of this file/.test(err.message)) throw err; if(!/one copy of this file/.test(why(err))) throw err;
if(!confirm(err.message+'\n\nDelete it anyway?')) return; if(!confirm(why(err)+'\n\nDelete it anyway?')) return;
await api(`/api/enclosures/${enc.id}?force=true`,{method:'DELETE'}); await api(`/api/enclosures/${enc.id}?force=true`,{method:'DELETE'});
} }
toast('Deleted'); loadEntries(); loadFeeds(true); toast('Deleted'); loadEntries(); loadFeeds(true);
} }
}catch(err){ toast(err.message,true); } }catch(err){ toast(why(err),true); }
} }

View File

@@ -177,7 +177,7 @@ $('#pdead').onclick=async()=>{
const f=S.feeds.find(x=>x.id===player.feed); if(!f) return; const f=S.feeds.find(x=>x.id===player.feed); if(!f) return;
const on=!f.deadair; const on=!f.deadair;
try{ await api(`/api/feeds/${encodeURIComponent(f.id)}`,{method:'PATCH',body:JSON.stringify({deadair:on})}); } 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(); f.deadair=on; paintDead();
const switched=await switchVersion(); const switched=await switchVersion();
toast(!on?'DeadAir is off for this show':switched?'DeadAir is cutting the dead air from this show' toast(!on?'DeadAir is off for this show':switched?'DeadAir is cutting the dead air from this show'

View File

@@ -3,6 +3,8 @@
// smoke test, not the type checker, is what makes sure a selector exists. // 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.querySelector(s);
const $$ = (s: string, r: ParentNode = document): any[] => [...r.querySelectorAll(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=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c])); const esc = s => (s??'').replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
// Icons: Font Awesome Free 7.3.1 by @fontawesome - https://fontawesome.com // 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 // 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){ if(navigator.share){
// Cancelling the sheet rejects too, and is not a failure worth a word. // Cancelling the sheet rejects too, and is not a failure worth a word.
try{ await navigator.share({title, url}); } 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; return;
} }
await copyText(url,btn); await copyText(url,btn);