Have the ordering sync fetch its own inputs

The instrumentation finally said it plainly:

    inputs {versionId: '29', versionFound: false, bucket_order: null,
            logMetaCount: 0, seqs: []}

It read the `versions` prop and the `logMeta` state, both populated
asynchronously, while running from initViewer — which finishes well before
them on a large load. So it was called with nothing every time, could never
build an expression, and the effect meant to re-run it once the data landed
never fired. Two small queries beat depending on that timing, the same
correction the master-data effect needed for the same reason.

Also explains why the ordering half-worked: `existing: Array(1)` with
Segment already applied. An earlier session had built it and it has been
riding in the saved layout since, so segment ordering appeared to work while
Bucket never existed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Paul Trowbridge 2026-09-17 14:43:30 -04:00
parent 6c9d0eef11
commit f7f4fbb4c6

View File

@ -644,7 +644,9 @@ export default function Forecast({ sources = [], sourceId, versions = [], versio
useEffect(() => { refreshLogMeta(versionId) }, [versionId]) useEffect(() => { refreshLogMeta(versionId) }, [versionId])
// Reordering on the Baseline page takes effect here without a reload: the // Reordering on the Baseline page takes effect here without a reload: the
// expressions are rebuilt and the pivot re-renders. // expressions are rebuilt and the pivot re-renders.
useEffect(() => { if (tableRef.current) syncOrderExpressions() }, [logMeta, versions, versionId]) // Re-run when the ordering could have changed on the Baseline page. logMeta is
// the signal, not the source: the function reads the ordering itself.
useEffect(() => { if (tableRef.current) syncOrderExpressions() }, [logMeta, versionId])
useEffect(() => { refreshTags(sourceId) }, [sourceId]) useEffect(() => { refreshTags(sourceId) }, [sourceId])
// Stream an Arrow IPC endpoint into one buffer, reporting download progress. // Stream an Arrow IPC endpoint into one buffer, reporting download progress.
@ -932,20 +934,38 @@ export default function Forecast({ sources = [], sourceId, versions = [], versio
console.log(`[pf-order] ${msg}`, extra) console.log(`[pf-order] ${msg}`, extra)
} }
// Fetches its own inputs rather than reading the `versions` prop and the
// `logMeta` state.
//
// Those are populated asynchronously, and this runs from initViewer which
// finishes well before them on a large load. It was therefore called with
// versionFound: false and logMetaCount: 0 every time, could never build
// anything, and the effect meant to re-run it once the data arrived never
// fired. Two small queries are worth more than the right timing.
async function syncOrderExpressions() { async function syncOrderExpressions() {
dbgOrder('enter')
const viewer = viewerRef.current const viewer = viewerRef.current
if (!viewer) { dbgOrder('exit: no viewer'); return } if (!viewer) return
const version = versions.find(v => String(v.id) === String(versionId))
const wanted = buildOrderExpressions(version?.bucket_order, logMeta) let bucketOrder = null
let entries = []
try {
const [vers, log] = await Promise.all([
fetch(`/api/sources/${sourceId}/versions`).then(r => r.ok ? r.json() : []),
fetch(`/api/versions/${versionId}/log`).then(r => r.ok ? r.json() : []),
])
bucketOrder = (Array.isArray(vers) ? vers : [])
.find(v => String(v.id) === String(versionId))?.bucket_order ?? null
entries = Array.isArray(log) ? log : []
} catch (err) {
console.error('[pf-order] could not read the ordering', err)
return
}
const byId = Object.fromEntries(entries.map(e => [e.id, e]))
const wanted = buildOrderExpressions(bucketOrder, byId)
dbgOrder('inputs', { dbgOrder('inputs', {
versionId, bucket_order: bucketOrder,
versionFound: !!version, seqs: entries.filter(e => e.seq != null).map(e => `${e.tag || e.note}=${e.seq}`),
bucket_order: version?.bucket_order ?? null,
logMetaCount: Object.keys(logMeta || {}).length,
seqs: Object.entries(logMeta || {})
.filter(([, m]) => m.seq != null)
.map(([id, m]) => `${id}=${m.seq}`),
wanted: Object.keys(wanted), wanted: Object.keys(wanted),
}) })