From ea6d554e3da0c71f73c2a54c965ae0289263575c Mon Sep 17 00:00:00 2001 From: Jared Knowles Date: Tue, 11 Aug 2026 09:22:01 -0400 Subject: [PATCH] fix: don't poison shard cache on failure; require real draws for ready status Two Important review findings on useDrawDistribution: - ensureShardRegistered cached the rejected promise on fetch/registration failure, permanently stuck for that (model,year,state) key until a full page reload. Now deletes the cache entry on failure so the next caller retries fresh. - status could become 'ready' even when the LEAID-bound query returned zero usable rows (district not present in the draws shard), silently mislabeling a fitSkewedInterval-only render as real posterior draws. Now requires at least one group with actual draws before reporting 'ready'. --- src/hooks/useDrawDistribution.js | 39 ++++++++++++++++++++++++-------- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/src/hooks/useDrawDistribution.js b/src/hooks/useDrawDistribution.js index 7ca817d..fd8966a 100644 --- a/src/hooks/useDrawDistribution.js +++ b/src/hooks/useDrawDistribution.js @@ -19,13 +19,21 @@ function ensureShardRegistered(db, model, year, state) { shardCache.set( key, (async () => { - const url = `${HF_BASE}/model_id=${model}/YEAR=${year}/LEA_STATE=${state}/data_0.parquet` - const res = await fetch(url) - if (!res.ok) throw new Error(`Failed to fetch draw shard: HTTP ${res.status}`) - const buffer = new Uint8Array(await res.arrayBuffer()) - const fileName = `${key}.parquet` - await db.registerFileBuffer(fileName, buffer) - return fileName + try { + const url = `${HF_BASE}/model_id=${model}/YEAR=${year}/LEA_STATE=${state}/data_0.parquet` + const res = await fetch(url) + if (!res.ok) throw new Error(`Failed to fetch draw shard: HTTP ${res.status}`) + const buffer = new Uint8Array(await res.arrayBuffer()) + const fileName = `${key}.parquet` + await db.registerFileBuffer(fileName, buffer) + return fileName + } catch (err) { + // Don't let a transient failure (network blip, HF outage) poison the + // cache forever — remove the rejected entry so the next caller for + // this shard gets a fresh attempt instead of the same dead promise. + shardCache.delete(key) + throw err + } })(), ) } @@ -79,9 +87,22 @@ export function useDrawDistribution({ leaid, state, model, year, groups }) { ;(byGroup[key] ??= []).push(rate) } + // A successful query with zero matching rows (this district isn't in + // the draws shard, or none of its rows matched a known group) is not + // "ready" — there's no real data to show, so treat it like a failure + // and let the caller fall back, rather than silently claiming real + // draws while every group actually uses the fitted approximation. + const hasDraws = Object.values(byGroup).some((draws) => draws.length > 0) + if (!cancelled) { - setDrawsByGroup(byGroup) - setStatus('ready') + if (hasDraws) { + setDrawsByGroup(byGroup) + setStatus('ready') + } else { + console.warn('useDrawDistribution: query succeeded but returned no usable draws for', { leaid, model, year, state }) + setDrawsByGroup(null) + setStatus('error') + } } } catch (err) { console.error('useDrawDistribution failed:', err)