Skip to content

Commit e5b4149

Browse files
ralyodioclaude
andcommitted
newtab: per-feed fetch timeout so one dead feed doesn't hang the grid (v0.2.14)
RSS feeds were stuck on 'Loading feeds…' because fetchFeed had no timeout — a single slow/dead feed hangs Promise.all and renderFeeds never replaces the placeholder. Add an 8s AbortController timeout per feed, render an error tile on failure, make renderFeeds resilient if loading throws, and bump CACHE_V to clear any stale feed cache. No manifest key / extension-id change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 1ca8be4 commit e5b4149

2 files changed

Lines changed: 24 additions & 8 deletions

File tree

‎apps/desktop/extensions/ai-sidebar/manifest.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"manifest_version": 3,
33
"name": "TronBrowser",
4-
"version": "0.2.13",
4+
"version": "0.2.14",
55
"description": "TronBrowser — privacy-first, AI-native. Branded new tab, private search, CoinPay login, and a bring-your-own-keys AI sidebar.",
66
"icons": {
77
"16": "icons/icon-16.png",

‎apps/desktop/extensions/ai-sidebar/newtab.js‎

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -88,13 +88,23 @@ renderAccount();
8888
const TTL = 15 * 60 * 1000;
8989

9090
async function fetchFeed(feed) {
91-
const res = await fetch(feed.xmlUrl, { redirect: 'follow' });
92-
if (!res.ok) throw new Error('HTTP ' + res.status);
93-
const items = parseFeed(await res.text()).slice(0, 6);
94-
return { ...feed, items };
91+
// Per-feed timeout — without it, one dead/slow feed hangs Promise.all and the
92+
// whole grid is stuck on "Loading feeds…" forever.
93+
const ctrl = new AbortController();
94+
const t = setTimeout(() => ctrl.abort(), 8000);
95+
try {
96+
const res = await fetch(feed.xmlUrl, { redirect: 'follow', signal: ctrl.signal });
97+
if (!res.ok) throw new Error('HTTP ' + res.status);
98+
const items = parseFeed(await res.text()).slice(0, 6);
99+
return { ...feed, items };
100+
} catch (e) {
101+
throw new Error(e.name === 'AbortError' ? 'timed out' : e.message);
102+
} finally {
103+
clearTimeout(t);
104+
}
95105
}
96106

97-
const CACHE_V = 2; // bump to invalidate caches when item shape changes (thumbnails)
107+
const CACHE_V = 3; // bump to invalidate caches when item shape changes / clear stale
98108
async function getFeedData(feeds) {
99109
const { feedCache } = await chrome.storage.local.get('feedCache');
100110
if (feedCache && feedCache.v === CACHE_V && Date.now() - feedCache.at < TTL && feedCache.count === feeds.length) {
@@ -114,9 +124,15 @@ function fmtDate(d) {
114124
}
115125

116126
async function renderFeeds() {
117-
const feeds = await loadFeeds();
118-
const data = await getFeedData(feeds);
119127
const grid = el('feeds');
128+
let feeds, data;
129+
try {
130+
feeds = await loadFeeds();
131+
data = await getFeedData(feeds);
132+
} catch (e) {
133+
grid.innerHTML = `<p class="err">Couldn't load feeds: ${escapeHtml(e.message)}</p>`;
134+
return;
135+
}
120136
grid.innerHTML = '';
121137
for (const f of data) {
122138
const card = document.createElement('div');

0 commit comments

Comments
 (0)