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'.
This commit is contained in:
2026-08-11 09:22:01 -04:00
parent d5bf18482a
commit ea6d554e3d
+21
View File
@@ -19,6 +19,7 @@ function ensureShardRegistered(db, model, year, state) {
shardCache.set( shardCache.set(
key, key,
(async () => { (async () => {
try {
const url = `${HF_BASE}/model_id=${model}/YEAR=${year}/LEA_STATE=${state}/data_0.parquet` const url = `${HF_BASE}/model_id=${model}/YEAR=${year}/LEA_STATE=${state}/data_0.parquet`
const res = await fetch(url) const res = await fetch(url)
if (!res.ok) throw new Error(`Failed to fetch draw shard: HTTP ${res.status}`) if (!res.ok) throw new Error(`Failed to fetch draw shard: HTTP ${res.status}`)
@@ -26,6 +27,13 @@ function ensureShardRegistered(db, model, year, state) {
const fileName = `${key}.parquet` const fileName = `${key}.parquet`
await db.registerFileBuffer(fileName, buffer) await db.registerFileBuffer(fileName, buffer)
return fileName 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) ;(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) { if (!cancelled) {
if (hasDraws) {
setDrawsByGroup(byGroup) setDrawsByGroup(byGroup)
setStatus('ready') setStatus('ready')
} else {
console.warn('useDrawDistribution: query succeeded but returned no usable draws for', { leaid, model, year, state })
setDrawsByGroup(null)
setStatus('error')
}
} }
} catch (err) { } catch (err) {
console.error('useDrawDistribution failed:', err) console.error('useDrawDistribution failed:', err)