mirror of
https://github.com/koala73/worldmonitor.git
synced 2026-04-25 17:14:57 +02:00
fix(economic): fix national debt panel hang + add lazy pagination (#1928)
- Remove persistCache from nationalDebtBreaker: IndexedDB hydration on first call can deadlock in some browsers, causing the panel to hang indefinitely showing "Loading debt data from IMF..." with no timeout - Add 20s hard deadline via Promise.race so loading always resolves - Extract _fetchNationalDebt() to avoid top-level await in the race - Lazy-load 20 countries initially with "Load more" button (+20 each tap) instead of rendering all 187 rows at once; resets on sort/search change - Ticker only updates visible rows (not all 187)
This commit is contained in:
@@ -101,6 +101,8 @@ function sortEntries(entries: NationalDebtEntry[], mode: SortMode): NationalDebt
|
||||
return sorted;
|
||||
}
|
||||
|
||||
const PAGE_SIZE = 20;
|
||||
|
||||
export class NationalDebtPanel extends Panel {
|
||||
private entries: NationalDebtEntry[] = [];
|
||||
private filteredEntries: NationalDebtEntry[] = [];
|
||||
@@ -108,6 +110,7 @@ export class NationalDebtPanel extends Panel {
|
||||
private searchQuery = '';
|
||||
private loading = false;
|
||||
private lastFetch = 0;
|
||||
private visibleCount = PAGE_SIZE;
|
||||
private tickerInterval: ReturnType<typeof setInterval> | null = null;
|
||||
private readonly REFRESH_INTERVAL = 6 * 60 * 60 * 1000;
|
||||
|
||||
@@ -120,12 +123,20 @@ export class NationalDebtPanel extends Panel {
|
||||
});
|
||||
|
||||
this.content.addEventListener('click', (e) => {
|
||||
const tab = (e.target as HTMLElement).closest('[data-sort]') as HTMLElement | null;
|
||||
const target = e.target as HTMLElement;
|
||||
const tab = target.closest('[data-sort]') as HTMLElement | null;
|
||||
if (tab?.dataset.sort) {
|
||||
this.sortMode = tab.dataset.sort as SortMode;
|
||||
this.visibleCount = PAGE_SIZE;
|
||||
this.applyFilters();
|
||||
this.render();
|
||||
this.restartTicker();
|
||||
return;
|
||||
}
|
||||
if (target.closest('.debt-load-more')) {
|
||||
this.visibleCount += PAGE_SIZE;
|
||||
this.render();
|
||||
this.restartTicker();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -133,6 +144,7 @@ export class NationalDebtPanel extends Panel {
|
||||
const target = e.target as HTMLInputElement;
|
||||
if (target.classList.contains('debt-search')) {
|
||||
this.searchQuery = target.value;
|
||||
this.visibleCount = PAGE_SIZE;
|
||||
this.applyFilters();
|
||||
this.render();
|
||||
this.restartTicker();
|
||||
@@ -227,8 +239,13 @@ export class NationalDebtPanel extends Panel {
|
||||
<input class="debt-search" type="text" placeholder="Search country..." value="${escapeHtml(this.searchQuery)}">
|
||||
</div>
|
||||
<div class="debt-list">
|
||||
${this.filteredEntries.slice(0, 100).map((entry, idx) => this.renderRow(entry, idx + 1)).join('')}
|
||||
${this.filteredEntries.slice(0, this.visibleCount).map((entry, idx) => this.renderRow(entry, idx + 1)).join('')}
|
||||
</div>
|
||||
${this.visibleCount < this.filteredEntries.length ? `
|
||||
<button class="debt-load-more">
|
||||
Load ${Math.min(PAGE_SIZE, this.filteredEntries.length - this.visibleCount)} more
|
||||
<span class="debt-load-more-count">(${this.filteredEntries.length - this.visibleCount} remaining)</span>
|
||||
</button>` : ''}
|
||||
<div class="debt-footer">
|
||||
<span class="debt-source">Source: IMF WEO 2024 + US Treasury FiscalData</span>
|
||||
<span class="debt-updated">Updated: ${new Date(this.lastFetch).toLocaleDateString()}</span>
|
||||
@@ -279,7 +296,7 @@ export class NationalDebtPanel extends Panel {
|
||||
}
|
||||
const container = this.content.querySelector('.debt-list');
|
||||
if (!container) return;
|
||||
for (const entry of this.filteredEntries.slice(0, 100)) {
|
||||
for (const entry of this.filteredEntries.slice(0, this.visibleCount)) {
|
||||
const el = container.querySelector<HTMLElement>(`.debt-ticker[data-iso3="${entry.iso3}"]`);
|
||||
if (el) {
|
||||
el.textContent = formatDebt(getCurrentDebt(entry));
|
||||
|
||||
@@ -589,13 +589,25 @@ export type { NationalDebtEntry };
|
||||
// National Debt Clock
|
||||
// ========================================================================
|
||||
|
||||
const nationalDebtBreaker = createCircuitBreaker<GetNationalDebtResponse>({ name: 'National Debt', cacheTtlMs: 6 * 60 * 60 * 1000, persistCache: true });
|
||||
// No persistCache: IndexedDB hydration on first call can deadlock in some browsers,
|
||||
// causing the panel to hang indefinitely on "Loading debt data from IMF..."
|
||||
const nationalDebtBreaker = createCircuitBreaker<GetNationalDebtResponse>({ name: 'National Debt', cacheTtlMs: 6 * 60 * 60 * 1000 });
|
||||
const emptyNationalDebtFallback: GetNationalDebtResponse = { entries: [], seededAt: '', unavailable: true };
|
||||
|
||||
export async function getNationalDebtData(): Promise<GetNationalDebtResponse> {
|
||||
const hydrated = getHydratedData('nationalDebt') as GetNationalDebtResponse | undefined;
|
||||
if (hydrated?.entries?.length) return hydrated;
|
||||
|
||||
// Race all fetch paths against a hard 20s deadline so the panel never hangs.
|
||||
return Promise.race([
|
||||
_fetchNationalDebt(),
|
||||
new Promise<GetNationalDebtResponse>(resolve =>
|
||||
setTimeout(() => resolve(emptyNationalDebtFallback), 20_000),
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
async function _fetchNationalDebt(): Promise<GetNationalDebtResponse> {
|
||||
try {
|
||||
const resp = await fetch(toApiUrl('/api/bootstrap?keys=nationalDebt'), {
|
||||
signal: AbortSignal.timeout(5_000),
|
||||
@@ -608,7 +620,7 @@ export async function getNationalDebtData(): Promise<GetNationalDebtResponse> {
|
||||
|
||||
try {
|
||||
return await nationalDebtBreaker.execute(async () => {
|
||||
return client.getNationalDebt({}, { signal: AbortSignal.timeout(15_000) });
|
||||
return client.getNationalDebt({}, { signal: AbortSignal.timeout(12_000) });
|
||||
}, emptyNationalDebtFallback);
|
||||
} catch {
|
||||
return emptyNationalDebtFallback;
|
||||
|
||||
@@ -20264,6 +20264,30 @@ body.has-breaking-alert .panels-grid {
|
||||
min-width: 70px;
|
||||
}
|
||||
|
||||
.debt-load-more {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
margin: 4px 0;
|
||||
background: none;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
color: var(--text-dim);
|
||||
font-size: 11px;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
flex-shrink: 0;
|
||||
transition: border-color 0.15s, color 0.15s;
|
||||
}
|
||||
|
||||
.debt-load-more:hover {
|
||||
border-color: var(--accent);
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.debt-load-more-count {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.debt-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
Reference in New Issue
Block a user