Description
Editing a "large value" cell in the result grid triggers an unbounded synchronous recursion that exhausts the call stack and crashes the result tab.
When __CHAT2DB_CELL_META__[col].largeValue is true, the change_cell_value listener calls tableInstance.changeCellValue(col, row, event.currentValue) to restore the display to the old value. VTable's listTableChangeCellValue re-fires change_cell_value synchronously whenever oldValue !== changedValue, which re-enters the listener, which writes the value back again — oscillating between two distinct values indefinitely. The only guard (if (event.currentValue === event.changedValue) return) can never match because every programmatic write flips the two values.
Large-value cells still get an inline editor (editor is set independently of largeValue; customRender only changes the display and does not disable editing), so a double-click edit, Enter, or paste on a large-value cell reaches the branch.
Location
chat2db-community-client/src/blocks/SearchResult/components/ResultSetTable/event/onChangeCellValue/index.tsx:4-14
const id = tableInstance.on('change_cell_value', (event) => {
if (event.currentValue === event.changedValue) {
return;
}
const { row, col } = event;
const originData = tableInstance.getRecordByCell(col, row);
const cellMeta = originData?.__CHAT2DB_CELL_META__?.[col];
if (cellMeta?.largeValue) {
tableInstance.changeCellValue(col, row, event.currentValue); // ← re-fires this listener
return;
}
...
});
VTable re-fire: @visactor/vtable record-helper.js listTableChangeCellValue calls table.fireListeners(CHANGE_CELL_VALUE, { currentValue: oldValue, changedValue }) synchronously after dataSource.changeFieldValue when oldValue !== changedValue.
Impact
RangeError: Maximum call stack size exceeded → result tab freezes/crashes.
- Reachable by editing any large-value cell in an editable result set (double-click edit, or paste).
Suggested fix
Add a reentry guard so the programmatic restore does not re-enter the same branch. Because VTable fires the listener synchronously inside changeCellValue, a closure flag set before the call and checked at the top of the listener breaks the cycle:
const onChangeCellValue = (tableInstance: VTable.ListTable, handleCellValueChange) => {
let isRestoring = false;
const id = tableInstance.on('change_cell_value', (event) => {
if (isRestoring || event.currentValue === event.changedValue) {
return;
}
const { row, col } = event;
const originData = tableInstance.getRecordByCell(col, row);
const cellMeta = originData?.__CHAT2DB_CELL_META__?.[col];
if (cellMeta?.largeValue) {
isRestoring = true;
try {
tableInstance.changeCellValue(col, row, event.currentValue);
} finally {
isRestoring = false;
}
return;
}
const headerField = tableInstance.getHeaderField(col, row);
const rowId = originData.CHAT2DB_ROW_NUMBER;
handleCellValueChange({ ...event, rowId, field: headerField });
});
return id;
};
The re-entered event is purely an artifact of the programmatic restore, so suppressing it loses no user intent.
Related existing
None. #2296 / #2298 concern change-tracking flags after a no-op undo; this is a separate large-value recursion path those fixes do not touch.
Description
Editing a "large value" cell in the result grid triggers an unbounded synchronous recursion that exhausts the call stack and crashes the result tab.
When
__CHAT2DB_CELL_META__[col].largeValueis true, thechange_cell_valuelistener callstableInstance.changeCellValue(col, row, event.currentValue)to restore the display to the old value. VTable'slistTableChangeCellValuere-fireschange_cell_valuesynchronously wheneveroldValue !== changedValue, which re-enters the listener, which writes the value back again — oscillating between two distinct values indefinitely. The only guard (if (event.currentValue === event.changedValue) return) can never match because every programmatic write flips the two values.Large-value cells still get an inline editor (
editoris set independently oflargeValue;customRenderonly changes the display and does not disable editing), so a double-click edit, Enter, or paste on a large-value cell reaches the branch.Location
chat2db-community-client/src/blocks/SearchResult/components/ResultSetTable/event/onChangeCellValue/index.tsx:4-14VTable re-fire:
@visactor/vtablerecord-helper.jslistTableChangeCellValuecallstable.fireListeners(CHANGE_CELL_VALUE, { currentValue: oldValue, changedValue })synchronously afterdataSource.changeFieldValuewhenoldValue !== changedValue.Impact
RangeError: Maximum call stack size exceeded→ result tab freezes/crashes.Suggested fix
Add a reentry guard so the programmatic restore does not re-enter the same branch. Because VTable fires the listener synchronously inside
changeCellValue, a closure flag set before the call and checked at the top of the listener breaks the cycle:The re-entered event is purely an artifact of the programmatic restore, so suppressing it loses no user intent.
Related existing
None. #2296 / #2298 concern change-tracking flags after a no-op undo; this is a separate large-value recursion path those fixes do not touch.