diff --git a/src/reactviews/pages/QueryResult/table/asyncDataView.ts b/src/reactviews/pages/QueryResult/table/asyncDataView.ts index aaaa200e5c..84e4804525 100644 --- a/src/reactviews/pages/QueryResult/table/asyncDataView.ts +++ b/src/reactviews/pages/QueryResult/table/asyncDataView.ts @@ -3,8 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -// import * as vscode from 'vscode'; import { IDisposableDataProvider } from "./dataProvider"; +import { v4 as uuid } from "uuid"; export interface IObservableCollection { getLength(): number; @@ -22,6 +22,11 @@ class DataWindow { private _data: T[] | undefined; private _length: number = 0; private _offsetFromDataSource: number = -1; + private _currentRequestId: string = uuid(); + private _debounceTimeout: NodeJS.Timeout | undefined; + private readonly _getRowsDebounceDelayMs: number = 50; + private _lastPositionTime: number = 0; + private _consecutivePositionCount: number = 0; // private cancellationToken = new CancellationTokenSource(); @@ -33,6 +38,10 @@ class DataWindow { dispose() { this._data = undefined; + if (this._debounceTimeout) { + clearTimeout(this._debounceTimeout); + this._debounceTimeout = undefined; + } // this.cancellationToken.cancel(); } @@ -55,28 +64,71 @@ class DataWindow { return this._data[index - this._offsetFromDataSource]; } - public positionWindow(offset: number, length: number): void { + public positionWindow(offset: number, length: number, totalItems: number): void { + offset = Math.max(0, offset); // Ensure offset is never negative + offset = Math.min(offset, totalItems); // Ensure offset is within total items + + length = Math.max(0, length); // Ensure length is at least 0 + length = Math.min(length, totalItems - offset); // Ensure length doesn't exceed total items + this._offsetFromDataSource = offset; this._length = length; this._data = undefined; - // this.cancellationToken.cancel(); - // this.cancellationToken = new CancellationTokenSource(); - // const currentCancellation = this.cancellationToken; + // Increment request ID to invalidate any pending requests + this._currentRequestId = uuid(); + const currentRequestId = this._currentRequestId; if (length === 0) { return; } - this.loadFunction(offset, length).then((data) => { - // if (!currentCancellation.token.isCancellationRequested) { - this._data = data; - this.loadCompleteCallback( - this._offsetFromDataSource, - this._offsetFromDataSource + this._length, - ); - // } - }); + // Detect if this is rapid continuous scrolling or a jump to position + const now = Date.now(); + const timeSinceLastPosition = now - this._lastPositionTime; + this._lastPositionTime = now; + + // If positions are happening very rapidly (< 100ms apart), it's continuous scrolling + if (timeSinceLastPosition < 100) { + this._consecutivePositionCount++; + } else { + this._consecutivePositionCount = 0; + } + + // Clear any pending debounced requests + if (this._debounceTimeout) { + clearTimeout(this._debounceTimeout); + } + + const executeLoad = () => { + // Double-check that this request is still current + if (currentRequestId !== this._currentRequestId) { + return; // Window was repositioned again, skip this request + } + + this.loadFunction(offset, length).then((data) => { + // Only apply data if this request is still current (window hasn't been repositioned) + if (currentRequestId === this._currentRequestId) { + this._data = data; + this.loadCompleteCallback( + this._offsetFromDataSource, + this._offsetFromDataSource + this._length, + ); + } + // Otherwise, ignore this outdated response to prevent flickering + }); + }; + + // If rapid continuous scrolling (3+ rapid events), debounce to reduce load + if (this._consecutivePositionCount >= 3) { + this._debounceTimeout = setTimeout(() => { + this._debounceTimeout = undefined; + executeLoad(); + }, this._getRowsDebounceDelayMs); + } else { + // Otherwise, load immediately (scrollbar drag, single scroll, or first few scrolls) + executeLoad(); + } } } @@ -158,6 +210,7 @@ export class VirtualizedCollection implements IObserv this._bufferWindowBefore.positionWindow( newWindowOffset, this._window.getStartIndex() - newWindowOffset, + this.length, ); } else if (start >= this._bufferWindowAfter.getStartIndex()) { // scroll down, shift down @@ -171,7 +224,7 @@ export class VirtualizedCollection implements IObserv ); let newWindowLength = Math.min(this.length - newWindowOffset, this.windowSize); - this._bufferWindowAfter.positionWindow(newWindowOffset, newWindowLength); + this._bufferWindowAfter.positionWindow(newWindowOffset, newWindowLength, this.length); } return currentData; @@ -204,17 +257,19 @@ export class VirtualizedCollection implements IObserv this._bufferWindowBefore.positionWindow( bufferWindowBeforeStart, bufferWindowBeforeEnd - bufferWindowBeforeStart, + this.length, ); let mainWindowStart = bufferWindowBeforeEnd; let mainWindowEnd = Math.min(mainWindowStart + this.windowSize, this.length); - this._window.positionWindow(mainWindowStart, mainWindowEnd - mainWindowStart); + this._window.positionWindow(mainWindowStart, mainWindowEnd - mainWindowStart, this.length); let bufferWindowAfterStart = mainWindowEnd; let bufferWindowAfterEnd = Math.min(bufferWindowAfterStart + this.windowSize, this.length); this._bufferWindowAfter.positionWindow( bufferWindowAfterStart, bufferWindowAfterEnd - bufferWindowAfterStart, + this.length, ); } }