Page: catch background fetches, no HTML in errors, a trace ends after a quiet minute (#219)
The page reported NetworkError rejections from the live update's list refresh and the first feed load, which had no catch, whenever the network was gone (a laptop asleep). A Cloudflare 502 page became a request's error message, since api() used whatever body came back. And the trace id lived until the next tap, so background requests nine hours apart shared one trace. The id now ends after a minute without a request; a play's position saves, every ten seconds, keep it going. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
|
||||
- A live update or a list refresh that fails because the network is gone (a laptop asleep) is no longer reported as an error, an error page from Cloudflare no longer becomes a request's message, and requests a minute or more after a tap start a trace of their own instead of joining that tap's (#219).
|
||||
|
||||
## [1.0.0] - 2026-10-12
|
||||
|
||||
### Added
|
||||
|
||||
@@ -15,7 +15,8 @@ function connect(){
|
||||
// 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(); });
|
||||
// Caught: a network gone when one fires (a laptop asleep) was an unhandled rejection (#219).
|
||||
const refreshEntries=soon(()=>{ if(S.feed) loadEntries().catch(()=>{}); });
|
||||
// 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=>{
|
||||
@@ -70,4 +71,4 @@ function connect(){
|
||||
sse.onerror=()=>{ sse.close(); dropped=true; setTimeout(connect,4000); };
|
||||
}
|
||||
connect();
|
||||
loadFeeds();
|
||||
loadFeeds().catch(()=>{});
|
||||
|
||||
@@ -72,11 +72,18 @@ function spanText(secs){
|
||||
}
|
||||
/// One trace for each thing you do (#190): the requests a tap or a key sets off share a trace id,
|
||||
/// sent as traceparent, so Tempo shows them together. getRandomValues, not randomUUID, which a
|
||||
/// LAN address over plain HTTP does not have.
|
||||
let traceId='';
|
||||
/// LAN address over plain HTTP does not have. A trace ends after a minute with no request in it:
|
||||
/// the requests a live update or a woken laptop sets off hours later used to join the last tap's
|
||||
/// trace, which then lasted nine hours (#219). A play's position saves, every ten seconds, keep it.
|
||||
let traceId='', traceAt=0;
|
||||
const hex=(n: number)=>Array.from(crypto.getRandomValues(new Uint8Array(n)),b=>b.toString(16).padStart(2,'0')).join('');
|
||||
for(const t of ['pointerdown','keydown']) window.addEventListener(t,()=>{ traceId=hex(16); },{capture:true,passive:true});
|
||||
function traceparent(){ return `00-${traceId||=hex(16)}-${hex(8)}-01`; }
|
||||
for(const t of ['pointerdown','keydown']) window.addEventListener(t,()=>{ traceId=hex(16); traceAt=Date.now(); },{capture:true,passive:true});
|
||||
function traceparent(){
|
||||
const now=Date.now();
|
||||
if(!traceId||now-traceAt>60_000) traceId=hex(16);
|
||||
traceAt=now;
|
||||
return `00-${traceId}-${hex(8)}-01`;
|
||||
}
|
||||
|
||||
/// Something that went wrong, or how long something took, in the browser, for the server's log
|
||||
/// (#190). The same thing at most once a minute: a failure in a loop would otherwise send a line
|
||||
@@ -95,7 +102,9 @@ window.addEventListener('unhandledrejection',e=>report('rejection',{msg:String(e
|
||||
async function api(url: string, opts?: RequestInit): Promise<any>{
|
||||
const r = await fetch(url,{headers:{'Content-Type':'application/json',traceparent:traceparent()},...opts});
|
||||
if(r.status===401){ location.href='/login'; throw new Error('signed out'); }
|
||||
if(!r.ok) throw new Error(await r.text().catch(()=>'')||String(r.status));
|
||||
// A page in place of ipx's words is Cloudflare or Caddy answering for it: the message became a
|
||||
// whole 502 page (#219).
|
||||
if(!r.ok) throw new Error((!/html/.test(r.headers.get('Content-Type')||'')&&await r.text().catch(()=>''))||`${r.status} ${r.statusText}`.trim());
|
||||
return r.status===204?null:r.json().catch(()=>null);
|
||||
}
|
||||
// navigator.clipboard only exists in a secure context. Served over plain HTTP on a LAN
|
||||
|
||||
Reference in New Issue
Block a user