Skip to content

Commit 23e4498

Browse files
fix(tables): ignore superseded-run import events in the detail SSE cache
applyImport applied every replayed import payload to the detail cache. The SSE buffer can replay a prior import's terminal event for the same table, stomping a newer in-flight import's UI. Lock to the active run's importId (and ignore a replayed terminal before the id is known), matching the guard the header tracker used to have. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent de7df1f commit 23e4498

1 file changed

Lines changed: 21 additions & 12 deletions

File tree

apps/sim/app/workspace/[workspaceId]/tables/[tableId]/hooks/use-table-event-stream.ts

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -225,23 +225,32 @@ export function useTableEventStream({
225225
}
226226

227227
const applyImport = (event: Extract<TableEvent, { kind: 'import' }>): void => {
228-
const { status, progress, error } = event
229-
queryClient.setQueryData<TableDefinition>(tableKeys.detail(tableId), (prev) =>
230-
prev
228+
const { status, progress, error, importId } = event
229+
const isTerminal = status === 'ready' || status === 'failed' || status === 'canceled'
230+
231+
// The SSE buffer replays on (re)connect and can hold a *prior* import's events for this
232+
// table. Ignore anything from a superseded run, and don't trust a replayed terminal before
233+
// we know the active run's id.
234+
const prev = queryClient.getQueryData<TableDefinition>(tableKeys.detail(tableId))
235+
const lockedId = prev?.importId
236+
if (lockedId && importId && importId !== lockedId) return
237+
if (!lockedId && isTerminal) return
238+
239+
queryClient.setQueryData<TableDefinition>(tableKeys.detail(tableId), (p) =>
240+
p
231241
? {
232-
...prev,
242+
...p,
233243
importStatus: status,
234-
importRowsProcessed: progress ?? prev.importRowsProcessed,
244+
importId: importId ?? p.importId,
245+
importRowsProcessed: progress ?? p.importRowsProcessed,
235246
importError: error ?? null,
236247
}
237-
: prev
248+
: p
238249
)
239-
// The header tray + completion toast are owned by `useImportTrayPoll` (mounted on every
240-
// page). Here we only keep the detail cache + grid in sync.
241-
// Live-fill: rows are real as each batch commits. Coalesce the per-tick row
242-
// refetches via a debounce; on the terminal event refetch rows + the
243-
// definition immediately (the worker may have rewritten the schema).
244-
if (status === 'ready' || status === 'failed' || status === 'canceled') {
250+
// The header tray + completion toast are owned by `useImportTrayPoll`. Here we only keep the
251+
// detail cache + grid in sync: live-fill rows per batch (debounced), and on the terminal
252+
// event refetch rows + the definition (the worker may have rewritten the schema).
253+
if (isTerminal) {
245254
if (importInvalidateTimer !== null) clearTimeout(importInvalidateTimer)
246255
void queryClient.invalidateQueries({ queryKey: tableKeys.rowsRoot(tableId) })
247256
void queryClient.invalidateQueries({ queryKey: tableKeys.detail(tableId) })

0 commit comments

Comments
 (0)