@@ -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 */
209211async 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