Skip to content

Commit 4620a07

Browse files
fix(tables): stop bulk cut chunks on first failure, reconcile grid on error
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent e9434cb commit 4620a07

1 file changed

Lines changed: 30 additions & 7 deletions

File tree

  • apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid

apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/table-grid.tsx

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -203,8 +203,10 @@ function cellToText(value: unknown): string {
203203

204204
/**
205205
* Split updates into chunks bounded by the server batch-size limit, dispatching
206-
* up to 3 chunks concurrently. Throws on first failure — `Promise.all` rejects
207-
* immediately, so partial success cannot leave the table in an ambiguous state.
206+
* up to 3 chunks concurrently. On the first chunk failure the remaining chunks
207+
* are not dispatched and the error is rethrown. There is no cross-chunk
208+
* transaction, so chunks already committed (or in flight when the failure
209+
* occurs) are not rolled back — callers must reconcile on failure (e.g. refetch).
208210
*/
209211
async function chunkBatchUpdates(
210212
updates: Array<{ rowId: string; data: Record<string, unknown> }>,
@@ -218,10 +220,17 @@ async function chunkBatchUpdates(
218220
chunks.push(updates.slice(i, i + size))
219221
}
220222
let cursor = 0
223+
let failed = false
221224
await Promise.all(
222225
Array.from({ length: Math.min(3, chunks.length) }, async () => {
223-
while (cursor < chunks.length) {
224-
await mutateAsync({ updates: chunks[cursor++]! })
226+
while (cursor < chunks.length && !failed) {
227+
const chunk = chunks[cursor++]!
228+
try {
229+
await mutateAsync({ updates: chunk })
230+
} catch (error) {
231+
failed = true
232+
throw error
233+
}
225234
}
226235
})
227236
)
@@ -310,6 +319,7 @@ export function TableGrid({
310319
columnSourceInfo,
311320
ensureAllRowsLoaded,
312321
ensureRowsLoadedUpTo,
322+
refetchRows,
313323
} = useTable({ workspaceId, tableId, queryOptions })
314324

315325
const { data: tableRunState } = useTableRunState(tableId)
@@ -330,6 +340,8 @@ export function TableGrid({
330340
ensureAllRowsLoadedRef.current = ensureAllRowsLoaded
331341
const ensureRowsLoadedUpToRef = useRef(ensureRowsLoadedUpTo)
332342
ensureRowsLoadedUpToRef.current = ensureRowsLoadedUpTo
343+
const refetchRowsRef = useRef(refetchRows)
344+
refetchRowsRef.current = refetchRows
333345
const isAppendingRowRef = useRef(false)
334346

335347
/**
@@ -2074,7 +2086,12 @@ export function TableGrid({
20742086
})()
20752087
}
20762088

2077-
/** Clears `colNames` on `rowsToClear` (the cut tail), recording undo only after the update lands. */
2089+
/**
2090+
* Clears `colNames` on `rowsToClear` (the cut tail). Undo is recorded only
2091+
* after the whole clear succeeds — a large cut spans multiple non-atomic
2092+
* chunks, so on failure we drop the (now-unreliable) undo and refetch to
2093+
* reconcile the grid with whatever the server actually committed.
2094+
*/
20782095
const clearCutRows = async (rowsToClear: TableRowType[], colNames: string[]) => {
20792096
const undo: Array<{ rowId: string; data: Record<string, unknown> }> = []
20802097
const updates: Array<{ rowId: string; data: Record<string, unknown> }> = []
@@ -2088,8 +2105,14 @@ export function TableGrid({
20882105
undo.push({ rowId: row.id, data: previousData })
20892106
updates.push({ rowId: row.id, data: nextData })
20902107
}
2091-
if (updates.length > 0) await chunkBatchUpdates(updates, batchUpdateAsyncRef.current)
2092-
if (undo.length > 0) pushUndoRef.current({ type: 'clear-cells', cells: undo })
2108+
if (updates.length === 0) return
2109+
try {
2110+
await chunkBatchUpdates(updates, batchUpdateAsyncRef.current)
2111+
} catch (error) {
2112+
refetchRowsRef.current()
2113+
throw error
2114+
}
2115+
pushUndoRef.current({ type: 'clear-cells', cells: undo })
20932116
}
20942117

20952118
const handleCopy = (e: ClipboardEvent) => {

0 commit comments

Comments
 (0)