Show the row count while the load is still waiting on it

X-Row-Count is exact but travels with the response headers, and in grain mode
the server aggregates the whole table before sending any -- so the number
appeared just as the fifteen-second wait ended, which is no use to anyone
watching it.

The forecast table's own count goes up first instead, from the same
table-info the status bar already reads, and the exact figure replaces it
when the headers arrive. The count query is deliberately not awaited: it
scans the whole table, and the load must not wait on a progress message.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Paul Trowbridge 2026-09-18 08:41:24 -04:00
parent 9753846d34
commit 1a0a9db8d0

View File

@ -57,10 +57,15 @@ export default function Forecast({ sources = [], sourceId, versions = [], versio
const [loading, setLoading] = useState(false)
const [largeDataset, setLargeDataset] = useState(false)
const [loadProgress, setLoadProgress] = useState(null) // { received, total }
// Rows the server says it is sending, from X-Row-Count. Known as soon as the
// headers land, well before the body has been read, so the wait can say what
// it is waiting for -- on this data the row count is the wait (see CLAUDE.md,
// "Load time is dominated by row count").
// Rows the load is waiting on, so the wait can say what it is waiting for --
// on this data the row count is the wait (see CLAUDE.md, "Load time is
// dominated by row count").
//
// Filled twice. X-Row-Count is exact but arrives with the response headers,
// and in grain mode the server aggregates before sending any: the number
// turned up just as the wait ended. So the forecast table's own count goes in
// first, from the same table-info the status bar reads, and the exact figure
// replaces it when the headers land.
const [loadRows, setLoadRows] = useState(null)
const [msg, setMsg] = useState(null)
@ -646,7 +651,7 @@ export default function Forecast({ sources = [], sourceId, versions = [], versio
if (!r.ok) { const { error } = await r.json(); throw new Error(error || 'Failed to load data') }
const rowCount = parseInt(r.headers.get('X-Row-Count') || '0')
const total = parseInt(r.headers.get('Content-Length') || '0') || null
setLoadRows(rowCount || null)
if (rowCount) setLoadRows(rowCount)
const reader = r.body.getReader()
const chunks = []
let received = 0
@ -684,6 +689,12 @@ export default function Forecast({ sources = [], sourceId, versions = [], versio
setLargeDataset(false)
setLoadProgress(null)
setLoadRows(null)
// deliberately not awaited: it is a count over the whole forecast table and
// the load must not wait on it
fetch(`/api/versions/${vid}/table-info`)
.then(r => r.ok ? r.json() : null)
.then(info => { if (info?.rows && initIdRef.current === myId) setLoadRows(n => n ?? info.rows) })
.catch(() => {})
setSlices([])
setExpandDepth(null)
adoptSplit([], 0)