Skip to content

bug(result-grid): editing a large-value cell triggers infinite recursion and crashes the tab #2379

Description

@Aias00

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    needs/infoNeeds more information from the reporter

    Type

    No type

    Projects

    Status
    Done

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions