Name each loaded table uniquely

Refresh Data aborted with Table "fc_29" already exists. The name was fixed
per version and the cleanup before it is best-effort: the viewer is still
holding the previous table when the new one is built, so deleting it does
not free the registry entry, and creating a second under the same name
fails.

Nothing reads the name -- the viewer is loaded by reference, and cleanLayout
strips it out of saved configs -- so it only has to be unique. The previous
name is remembered and freed on the next load, when the viewer has let go of
it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Paul Trowbridge 2026-09-18 13:49:44 -04:00
parent 0d05dc3baa
commit 469e4cc957

View File

@ -246,6 +246,9 @@ export default function Forecast({ sources = [], sourceId, versions = [], versio
// the datagrid plugin element, for reading cell coordinates and driving its
// own selection highlight (see the selection-highlight effect below)
const gridRef = useRef(null)
// the worker-registry name of the table currently loaded, so the next load can
// try to free it
const tableNameRef = useRef(null)
// { x, y } of the body cell the last mousedown landed on, or null
const downCellRef = useRef(null)
// the region a drag is building, as a Perspective ViewWindow; committed on mouseup
@ -765,7 +768,13 @@ export default function Forecast({ sources = [], sourceId, versions = [], versio
...meta.filter(c => ['dimension','value','units','date'].includes(c.role)).map(c => c.cname),
'pf_id', 'pf_iter', 'pf_logid', 'pf_user', 'created_at', 'pf_segment', 'pf_bucket', 'pf_note', 'pf_op',
])
const tableName = `fc_${vid}`
// Unique per load, not per version. A fixed name collides on refresh:
// the viewer still holds the previous table, so deleting it does not
// actually free the registry entry, and creating the new one aborts with
// "Table `fc_29` already exists". Nothing reads the name -- the viewer is
// loaded by reference and cleanLayout strips the name out of saved
// configs -- so it only has to be unique.
const tableName = `fc_${vid}_${myId}`
if (rowCount >= 500000) setLargeDataset(true)
@ -774,16 +783,21 @@ export default function Forecast({ sources = [], sourceId, versions = [], versio
if (!workerRef.current) workerRef.current = await perspective.worker()
const worker = workerRef.current
// Clean up the previous table by JS reference first, then by name in the
// worker registry (covers the case where the ref was lost or delete failed).
// Clean up the previous table by JS reference first, then by the name we
// gave it (covers the case where the ref was lost or the delete failed).
// Best-effort: a table the viewer is still holding will refuse, which is
// why the new one no longer reuses its name.
if (tableRef.current) {
try { await tableRef.current.delete() } catch {}
tableRef.current = null
}
try {
const stale = await worker.open_table(tableName)
if (stale) await stale.delete()
} catch {}
if (tableNameRef.current && tableNameRef.current !== tableName) {
try {
const stale = await worker.open_table(tableNameRef.current)
if (stale) await stale.delete()
} catch {}
}
tableNameRef.current = tableName
// An empty result gets no index. `[]` carries no columns, so naming one
// aborts the worker outright -- "Specified index `pf_gkey` does not exist