From c35cdd9a557f88870498889cd88592ca4ed193e1 Mon Sep 17 00:00:00 2001 From: AntoinePrv Date: Wed, 24 Dec 2025 14:57:45 +0100 Subject: [PATCH 1/6] Store data in structured Chunk --- src/model.ts | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/src/model.ts b/src/model.ts index 9a7367c..92adc1d 100644 --- a/src/model.ts +++ b/src/model.ts @@ -120,9 +120,9 @@ export class ArrowModel extends DataModel { } // We have data - const row_idx_in_chunk = row % this._loadingParams.rowChunkSize; - const col_idx_in_chunk = col % this._loadingParams.colChunkSize; - const val = chunk.getChildAt(col_idx_in_chunk)?.get(row_idx_in_chunk); + const rowIdxInChunk = row - chunk.startRow; + const colIdxInChunk = col - chunk.startCol; + const val = chunk.data.getChildAt(colIdxInChunk)?.get(rowIdxInChunk); const out = val?.toString() || this._loadingParams.nullRepr; // Prefetch next chunks only once we have data for the current chunk. @@ -138,8 +138,8 @@ export class ArrowModel extends DataModel { // Fetch data, however we cannot await it due to the interface required by the DataGrid. // Instead, we fire the request, and notify of change upon completion. - const promise = this.fetchChunk(chunkIdx).then((table) => { - this._chunks.set(chunkIdx, table); + const promise = this.fetchChunk(chunkIdx).then((chnk) => { + this._chunks.set(chunkIdx, chnk); this.emitChangedChunk(chunkIdx); }); this._chunks.set(chunkIdx, promise); @@ -147,9 +147,9 @@ export class ArrowModel extends DataModel { return this._loadingParams.loadingRepr; } - private async fetchChunk(chunkIdx: [number, number]) { + private async fetchChunk(chunkIdx: [number, number]): Promise { const [rowChunk, colChunk] = chunkIdx; - return await fetchTable({ + const table = await fetchTable({ path: this._loadingParams.path, row_chunk_size: this._loadingParams.rowChunkSize, row_chunk: rowChunk, @@ -157,6 +157,11 @@ export class ArrowModel extends DataModel { col_chunk: colChunk, ...this._fileOptions, }); + return { + data: table, + startRow: rowChunk * this._loadingParams.rowChunkSize, + startCol: colChunk * this._loadingParams.colChunkSize, + }; } private emitChangedChunk(chunkIdx: [number, number]) { @@ -209,6 +214,12 @@ export class ArrowModel extends DataModel { private _numRows: number = 0; private _numCols: number = 0; private _schema!: Arrow.Schema; - private _chunks: PairMap> = new PairMap(); + private _chunks: PairMap> = new PairMap(); private _ready: Promise; } + +interface Chunk { + data: Arrow.Table; + startRow: number; + startCol: number; +} From 0dedd1c2cdbee663720f5f68909b33b5a088e132 Mon Sep 17 00:00:00 2001 From: AntoinePrv Date: Wed, 24 Dec 2025 16:19:14 +0100 Subject: [PATCH 2/6] Refactor chunks into ChunkMap --- src/model.ts | 178 ++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 128 insertions(+), 50 deletions(-) diff --git a/src/model.ts b/src/model.ts index 92adc1d..611e03c 100644 --- a/src/model.ts +++ b/src/model.ts @@ -45,16 +45,20 @@ export class ArrowModel extends DataModel { } protected async initialize(): Promise { - const [stats, chunk00] = await Promise.all([ - fetchStats({ path: this._loadingParams.path, ...this._fileOptions }), - this.fetchChunk([0, 0]), - ]); - + const stats = await fetchStats({ path: this._loadingParams.path, ...this._fileOptions }); this._schema = stats.schema; this._numCols = stats.num_cols; this._numRows = stats.num_rows; - this._chunks = new PairMap(); - this._chunks.set([0, 0], chunk00); + this._chunks = new ChunkMap({ + rowChunkSize: this._loadingParams.rowChunkSize, + numRows: this._numRows, + colChunkSize: this._loadingParams.colChunkSize, + numCols: this._numCols, + }); + + const chunkIdx00 = this._chunks.getChunkIdx({ rowIdx: 0, colIdx: 0 }); + const chunk00 = await this.fetchChunk(chunkIdx00); + this._chunks.set(chunkIdx00, chunk00); } get fileInfo(): Readonly { @@ -110,7 +114,7 @@ export class ArrowModel extends DataModel { } private dataBody(row: number, col: number): string { - const chunkIdx = this.chunkIdx(row, col); + const chunkIdx = this._chunks.getChunkIdx({ rowIdx: row, colIdx: col }); if (this._chunks.has(chunkIdx)) { const chunk = this._chunks.get(chunkIdx)!; @@ -128,9 +132,9 @@ export class ArrowModel extends DataModel { // Prefetch next chunks only once we have data for the current chunk. // We chain the Promise because this can be considered a low priority operation so we want // to reduce load on the server - const [rowChunk, colChunk] = chunkIdx; - this.prefetchChunkIfNeeded([rowChunk + 1, colChunk]).then((_) => { - this.prefetchChunkIfNeeded([rowChunk, colChunk + 1]); + const { chunkRowIdx, chunkColIdx } = chunkIdx; + this.prefetchChunkIfNeeded({ chunkRowIdx: chunkRowIdx + 1, chunkColIdx }).then((_) => { + this.prefetchChunkIfNeeded({ chunkRowIdx, chunkColIdx: chunkColIdx + 1 }); }); return out; @@ -147,44 +151,37 @@ export class ArrowModel extends DataModel { return this._loadingParams.loadingRepr; } - private async fetchChunk(chunkIdx: [number, number]): Promise { - const [rowChunk, colChunk] = chunkIdx; + private async fetchChunk(chunkIdx: ChunkMap.ChunkIdx): Promise { + const { chunkRowIdx, chunkColIdx } = chunkIdx; const table = await fetchTable({ path: this._loadingParams.path, row_chunk_size: this._loadingParams.rowChunkSize, - row_chunk: rowChunk, + row_chunk: chunkRowIdx, col_chunk_size: this._loadingParams.colChunkSize, - col_chunk: colChunk, + col_chunk: chunkColIdx, ...this._fileOptions, }); return { data: table, - startRow: rowChunk * this._loadingParams.rowChunkSize, - startCol: colChunk * this._loadingParams.colChunkSize, + startRow: chunkRowIdx * this._loadingParams.rowChunkSize, + startCol: chunkColIdx * this._loadingParams.colChunkSize, }; } - private emitChangedChunk(chunkIdx: [number, number]) { - const [rowChunk, colChunk] = chunkIdx; - - // We must ensure the range is within the bounds - const rowStart = rowChunk * this._loadingParams.rowChunkSize; - const rowEnd = Math.min(rowStart + this._loadingParams.rowChunkSize, this._numRows); - const colStart = colChunk * this._loadingParams.colChunkSize; - const colEnd = Math.min(colStart + this._loadingParams.colChunkSize, this._numCols); - + private emitChangedChunk(chunkIdx: ChunkMap.ChunkIdx) { + const { chunkRowIdx, chunkColIdx } = chunkIdx; this.emitChanged({ type: "cells-changed", region: "body", - row: rowStart, - rowSpan: rowEnd - rowStart, - column: colStart, - columnSpan: colEnd - colStart, + row: chunkRowIdx * this._loadingParams.rowChunkSize, + rowSpan: this._loadingParams.rowChunkSize, + column: chunkColIdx * this._loadingParams.colChunkSize, + columnSpan: this._loadingParams.colChunkSize, }); } - private async prefetchChunkIfNeeded(chunkIdx: [number, number]) { - if (this._chunks.has(chunkIdx) || !this.chunkIsValid(chunkIdx)) { + private async prefetchChunkIfNeeded(chunkIdx: ChunkMap.ChunkIdx) { + if (this._chunks.has(chunkIdx) || !this._chunks.chunkIsValid(chunkIdx)) { return; } @@ -194,19 +191,6 @@ export class ArrowModel extends DataModel { this._chunks.set(chunkIdx, promise); } - private chunkIdx(row: number, col: number): [number, number] { - return [ - Math.floor(row / this._loadingParams.rowChunkSize), - Math.floor(col / this._loadingParams.colChunkSize), - ]; - } - - private chunkIsValid(chunkIdx: [number, number]): boolean { - const [rowChunk, colChunk] = chunkIdx; - const [max_rowChunk, max_colChunk] = this.chunkIdx(this._numRows - 1, this._numCols - 1); - return rowChunk >= 0 && rowChunk <= max_rowChunk && colChunk >= 0 && colChunk <= max_colChunk; - } - private readonly _loadingParams: Required; private readonly _fileInfo: FileInfo; private _fileOptions: FileReadOptions; @@ -214,12 +198,106 @@ export class ArrowModel extends DataModel { private _numRows: number = 0; private _numCols: number = 0; private _schema!: Arrow.Schema; - private _chunks: PairMap> = new PairMap(); + private _chunks!: ChunkMap; private _ready: Promise; } -interface Chunk { - data: Arrow.Table; - startRow: number; - startCol: number; +export namespace ChunkMap { + export interface Parameters { + rowChunkSize: number; + numRows: number; + colChunkSize: number; + numCols: number; + } + + export interface ChunkIdx { + chunkRowIdx: number; + chunkColIdx: number; + } + + export interface Chunk { + data: Arrow.Table; + startRow: number; + startCol: number; + } + + export type ChunkData = Chunk | Promise; + + export interface CellIdx { + rowIdx: number; + colIdx: number; + } +} + +class ChunkMap { + constructor(parameters: ChunkMap.Parameters) { + this._parameters = parameters; + } + + getChunkIdx(cellIdx: ChunkMap.CellIdx): ChunkMap.ChunkIdx { + return { + chunkRowIdx: Math.floor(cellIdx.rowIdx / this._parameters.rowChunkSize), + chunkColIdx: Math.floor(cellIdx.colIdx / this._parameters.colChunkSize), + }; + } + + chunkIsValid(chunkIdx: ChunkMap.ChunkIdx): boolean { + const { chunkRowIdx, chunkColIdx } = chunkIdx; + const { chunkRowIdx: maxChunkRowIdx, chunkColIdx: maxChunkColIdx } = this.getChunkIdx({ + rowIdx: this._parameters.numRows - 1, + colIdx: this._parameters.numCols - 1, + }); + return ( + chunkRowIdx >= 0 && + chunkRowIdx <= maxChunkRowIdx && + chunkColIdx >= 0 && + chunkColIdx <= maxChunkColIdx + ); + } + + set(chunkIdx: ChunkMap.ChunkIdx, value: ChunkMap.ChunkData): this { + this._map.set(ChunkMap._chunkIdxToKey(chunkIdx), value); + return this; + } + + get(chunkIdx: ChunkMap.ChunkIdx): ChunkMap.ChunkData | undefined { + return this._map.get(ChunkMap._chunkIdxToKey(chunkIdx)); + } + + clear(): void { + this._map.clear(); + } + + delete(chunkIdx: ChunkMap.ChunkIdx): boolean { + return this._map.delete(ChunkMap._chunkIdxToKey(chunkIdx)); + } + + has(chunkIdx: ChunkMap.ChunkIdx): boolean { + return this._map.has(ChunkMap._chunkIdxToKey(chunkIdx)); + } + + get size(): number { + return this._map.size; + } + + forEach( + callbackfn: (value: ChunkMap.ChunkData, key: ChunkMap.ChunkIdx, map: ChunkMap) => void, + // biome-ignore lint/suspicious/noExplicitAny: This is in the Map signature + thisArg?: any, + ): void { + this._map.forEach((value, key) => { + callbackfn.call(thisArg, value, ChunkMap._keyToChunkIdx(key), this); + }); + } + + private static _chunkIdxToKey(chunkIdx: ChunkMap.ChunkIdx): [number, number] { + return [chunkIdx.chunkRowIdx, chunkIdx.chunkColIdx]; + } + + private static _keyToChunkIdx(key: [number, number]): ChunkMap.ChunkIdx { + return { chunkRowIdx: key[0], chunkColIdx: key[1] }; + } + + private _map = new PairMap(); + private _parameters: Required; } From 3de081716ac3e6acefc5f6ea68465dd34588bd24 Mon Sep 17 00:00:00 2001 From: AntoinePrv Date: Wed, 24 Dec 2025 16:24:17 +0100 Subject: [PATCH 3/6] Restore main --- src/model.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/model.ts b/src/model.ts index 611e03c..1c69b6e 100644 --- a/src/model.ts +++ b/src/model.ts @@ -170,13 +170,20 @@ export class ArrowModel extends DataModel { private emitChangedChunk(chunkIdx: ChunkMap.ChunkIdx) { const { chunkRowIdx, chunkColIdx } = chunkIdx; + + // We must ensure the range is within the bounds + const rowStart = chunkRowIdx * this._loadingParams.rowChunkSize; + const rowEnd = Math.min(rowStart + this._loadingParams.rowChunkSize, this._numRows); + const colStart = chunkColIdx * this._loadingParams.colChunkSize; + const colEnd = Math.min(colStart + this._loadingParams.colChunkSize, this._numCols); + this.emitChanged({ type: "cells-changed", region: "body", - row: chunkRowIdx * this._loadingParams.rowChunkSize, - rowSpan: this._loadingParams.rowChunkSize, - column: chunkColIdx * this._loadingParams.colChunkSize, - columnSpan: this._loadingParams.colChunkSize, + row: rowStart, + rowSpan: rowEnd - rowStart, + column: colStart, + columnSpan: colEnd - colStart, }); } From 28aca7dc22b3e7556f90229df49cc3e4274304be Mon Sep 17 00:00:00 2001 From: AntoinePrv Date: Wed, 24 Dec 2025 17:38:35 +0100 Subject: [PATCH 4/6] Fix prefetch not emiting changes --- src/model.ts | 43 ++++++++++++++++++++++++++++--------------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/src/model.ts b/src/model.ts index 1c69b6e..2351d11 100644 --- a/src/model.ts +++ b/src/model.ts @@ -118,8 +118,13 @@ export class ArrowModel extends DataModel { if (this._chunks.has(chunkIdx)) { const chunk = this._chunks.get(chunkIdx)!; - if (chunk instanceof Promise) { - // Wait for Promise to complete and mark data as modified + if (chunk.type === "pending") { + // Wait for Promise to complete, and let it will mark data as modified. + // If it was created through a prefetch, it does emit a change so we add it. + if (chunk.reason === "prefetch") { + const promise = chunk.promise.then((_) => this.emitChangedChunk(chunkIdx)); + this._chunks.set(chunkIdx, { promise, reason: "query", type: "pending" }); + } return this._loadingParams.loadingRepr; } @@ -146,7 +151,7 @@ export class ArrowModel extends DataModel { this._chunks.set(chunkIdx, chnk); this.emitChangedChunk(chunkIdx); }); - this._chunks.set(chunkIdx, promise); + this._chunks.set(chunkIdx, { promise, reason: "query", type: "pending" }); return this._loadingParams.loadingRepr; } @@ -165,6 +170,7 @@ export class ArrowModel extends DataModel { data: table, startRow: chunkRowIdx * this._loadingParams.rowChunkSize, startCol: chunkColIdx * this._loadingParams.colChunkSize, + type: "chunk", }; } @@ -195,7 +201,7 @@ export class ArrowModel extends DataModel { const promise = this.fetchChunk(chunkIdx).then((table) => { this._chunks.set(chunkIdx, table); }); - this._chunks.set(chunkIdx, promise); + this._chunks.set(chunkIdx, { promise, reason: "prefetch", type: "pending" }); } private readonly _loadingParams: Required; @@ -210,30 +216,37 @@ export class ArrowModel extends DataModel { } export namespace ChunkMap { - export interface Parameters { + export type Parameters = { rowChunkSize: number; numRows: number; colChunkSize: number; numCols: number; - } + }; - export interface ChunkIdx { + export type ChunkIdx = { chunkRowIdx: number; chunkColIdx: number; - } + }; + + export type CellIdx = { + rowIdx: number; + colIdx: number; + }; - export interface Chunk { + export type Chunk = { data: Arrow.Table; startRow: number; startCol: number; - } + type: "chunk"; + }; - export type ChunkData = Chunk | Promise; + export type PendingChunk = { + promise: Promise; + reason: "query" | "prefetch"; + type: "pending"; + }; - export interface CellIdx { - rowIdx: number; - colIdx: number; - } + export type ChunkData = Chunk | PendingChunk; } class ChunkMap { From 43d6e8856fa59a9db1bfcb7ec339b1651f3f0762 Mon Sep 17 00:00:00 2001 From: AntoinePrv Date: Mon, 29 Dec 2025 10:29:23 +0100 Subject: [PATCH 5/6] Refactor fetch saving --- src/model.ts | 110 ++++++++++++++++++++++++++++----------------------- 1 file changed, 60 insertions(+), 50 deletions(-) diff --git a/src/model.ts b/src/model.ts index 2351d11..3eaf259 100644 --- a/src/model.ts +++ b/src/model.ts @@ -57,8 +57,7 @@ export class ArrowModel extends DataModel { }); const chunkIdx00 = this._chunks.getChunkIdx({ rowIdx: 0, colIdx: 0 }); - const chunk00 = await this.fetchChunk(chunkIdx00); - this._chunks.set(chunkIdx00, chunk00); + await this.fetchThenStoreChunk(chunkIdx00); } get fileInfo(): Readonly { @@ -123,7 +122,7 @@ export class ArrowModel extends DataModel { // If it was created through a prefetch, it does emit a change so we add it. if (chunk.reason === "prefetch") { const promise = chunk.promise.then((_) => this.emitChangedChunk(chunkIdx)); - this._chunks.set(chunkIdx, { promise, reason: "query", type: "pending" }); + this._chunks.set(chunkIdx, ChunkMap.makePendingChunk({ promise, reason: "query" })); } return this._loadingParams.loadingRepr; } @@ -147,17 +146,15 @@ export class ArrowModel extends DataModel { // Fetch data, however we cannot await it due to the interface required by the DataGrid. // Instead, we fire the request, and notify of change upon completion. - const promise = this.fetchChunk(chunkIdx).then((chnk) => { - this._chunks.set(chunkIdx, chnk); - this.emitChangedChunk(chunkIdx); - }); - this._chunks.set(chunkIdx, { promise, reason: "query", type: "pending" }); + const promise = this.fetchThenStoreChunk(chunkIdx).then((_) => this.emitChangedChunk(chunkIdx)); + this._chunks.set(chunkIdx, ChunkMap.makePendingChunk({ promise, reason: "query" })); return this._loadingParams.loadingRepr; } - private async fetchChunk(chunkIdx: ChunkMap.ChunkIdx): Promise { + private async fetchThenStoreChunk(chunkIdx: ChunkMap.ChunkIdx): Promise { const { chunkRowIdx, chunkColIdx } = chunkIdx; + const table = await fetchTable({ path: this._loadingParams.path, row_chunk_size: this._loadingParams.rowChunkSize, @@ -166,12 +163,13 @@ export class ArrowModel extends DataModel { col_chunk: chunkColIdx, ...this._fileOptions, }); - return { + const chunk: ChunkMap.Chunk = ChunkMap.makeChunk({ data: table, startRow: chunkRowIdx * this._loadingParams.rowChunkSize, startCol: chunkColIdx * this._loadingParams.colChunkSize, - type: "chunk", - }; + }); + + this._chunks.set(chunkIdx, chunk); } private emitChangedChunk(chunkIdx: ChunkMap.ChunkIdx) { @@ -198,10 +196,8 @@ export class ArrowModel extends DataModel { return; } - const promise = this.fetchChunk(chunkIdx).then((table) => { - this._chunks.set(chunkIdx, table); - }); - this._chunks.set(chunkIdx, { promise, reason: "prefetch", type: "pending" }); + const promise = this.fetchThenStoreChunk(chunkIdx); + this._chunks.set(chunkIdx, ChunkMap.makePendingChunk({ promise, reason: "prefetch" })); } private readonly _loadingParams: Required; @@ -215,40 +211,6 @@ export class ArrowModel extends DataModel { private _ready: Promise; } -export namespace ChunkMap { - export type Parameters = { - rowChunkSize: number; - numRows: number; - colChunkSize: number; - numCols: number; - }; - - export type ChunkIdx = { - chunkRowIdx: number; - chunkColIdx: number; - }; - - export type CellIdx = { - rowIdx: number; - colIdx: number; - }; - - export type Chunk = { - data: Arrow.Table; - startRow: number; - startCol: number; - type: "chunk"; - }; - - export type PendingChunk = { - promise: Promise; - reason: "query" | "prefetch"; - type: "pending"; - }; - - export type ChunkData = Chunk | PendingChunk; -} - class ChunkMap { constructor(parameters: ChunkMap.Parameters) { this._parameters = parameters; @@ -321,3 +283,51 @@ class ChunkMap { private _map = new PairMap(); private _parameters: Required; } + +namespace ChunkMap { + export type Parameters = { + rowChunkSize: number; + numRows: number; + colChunkSize: number; + numCols: number; + }; + + export type ChunkIdx = { + chunkRowIdx: number; + chunkColIdx: number; + }; + + export type CellIdx = { + rowIdx: number; + colIdx: number; + }; + + export type Chunk = { + data: Arrow.Table; + startRow: number; + startCol: number; + readonly type: "chunk"; + }; + + export function makeChunk(chunk: Omit): Chunk { + return { + ...chunk, + type: "chunk", + }; + } + + export type PendingChunk = { + promise: Promise; + reason: "query" | "prefetch"; + readonly type: "pending"; + }; + + export function makePendingChunk(chunk: Omit): PendingChunk { + return { + ...chunk, + type: "pending", + }; + } + + export type ChunkData = Chunk | PendingChunk; +} From f7111227f308bbfee35d4502eb36e9ca1f6ddaf5 Mon Sep 17 00:00:00 2001 From: AntoinePrv Date: Mon, 29 Dec 2025 10:51:13 +0100 Subject: [PATCH 6/6] Refactor prefetch --- src/model.ts | 50 +++++++++++++++++++++++++++++++++++--------------- 1 file changed, 35 insertions(+), 15 deletions(-) diff --git a/src/model.ts b/src/model.ts index 3eaf259..32d6a01 100644 --- a/src/model.ts +++ b/src/model.ts @@ -122,7 +122,7 @@ export class ArrowModel extends DataModel { // If it was created through a prefetch, it does emit a change so we add it. if (chunk.reason === "prefetch") { const promise = chunk.promise.then((_) => this.emitChangedChunk(chunkIdx)); - this._chunks.set(chunkIdx, ChunkMap.makePendingChunk({ promise, reason: "query" })); + this.storeChunkData(chunkIdx, ChunkMap.makePendingChunk({ promise, reason: "query" })); } return this._loadingParams.loadingRepr; } @@ -133,13 +133,8 @@ export class ArrowModel extends DataModel { const val = chunk.data.getChildAt(colIdxInChunk)?.get(rowIdxInChunk); const out = val?.toString() || this._loadingParams.nullRepr; - // Prefetch next chunks only once we have data for the current chunk. - // We chain the Promise because this can be considered a low priority operation so we want - // to reduce load on the server - const { chunkRowIdx, chunkColIdx } = chunkIdx; - this.prefetchChunkIfNeeded({ chunkRowIdx: chunkRowIdx + 1, chunkColIdx }).then((_) => { - this.prefetchChunkIfNeeded({ chunkRowIdx, chunkColIdx: chunkColIdx + 1 }); - }); + // Prefetch next chunks only once we have the current data to prioritize current view + this.prefetchAsNeededForChunk(chunkIdx); return out; } @@ -147,7 +142,7 @@ export class ArrowModel extends DataModel { // Fetch data, however we cannot await it due to the interface required by the DataGrid. // Instead, we fire the request, and notify of change upon completion. const promise = this.fetchThenStoreChunk(chunkIdx).then((_) => this.emitChangedChunk(chunkIdx)); - this._chunks.set(chunkIdx, ChunkMap.makePendingChunk({ promise, reason: "query" })); + this.storeChunkData(chunkIdx, ChunkMap.makePendingChunk({ promise, reason: "query" })); return this._loadingParams.loadingRepr; } @@ -169,7 +164,11 @@ export class ArrowModel extends DataModel { startCol: chunkColIdx * this._loadingParams.colChunkSize, }); - this._chunks.set(chunkIdx, chunk); + this.storeChunkData(chunkIdx, chunk); + } + + private storeChunkData(chunkIdx: ChunkMap.ChunkIdx, data: ChunkMap.ChunkData) { + this._chunks.set(chunkIdx, data); } private emitChangedChunk(chunkIdx: ChunkMap.ChunkIdx) { @@ -191,13 +190,34 @@ export class ArrowModel extends DataModel { }); } - private async prefetchChunkIfNeeded(chunkIdx: ChunkMap.ChunkIdx) { - if (this._chunks.has(chunkIdx) || !this._chunks.chunkIsValid(chunkIdx)) { - return; + /** + * Prefetch next chunks if available. + * + * We chain the Promise because this can be considered a low priority operation so we want + * to reduce load on the server. + */ + private prefetchAsNeededForChunk(chunkIdx: ChunkMap.ChunkIdx) { + const { chunkRowIdx, chunkColIdx } = chunkIdx; + + let promise = Promise.resolve(); + + const nextRowsChunkIdx: ChunkMap.ChunkIdx = { chunkRowIdx: chunkRowIdx + 1, chunkColIdx }; + if (!this._chunks.has(nextRowsChunkIdx) && this._chunks.chunkIsValid(nextRowsChunkIdx)) { + promise = promise.then((_) => this.fetchThenStoreChunk(nextRowsChunkIdx)); + this.storeChunkData( + nextRowsChunkIdx, + ChunkMap.makePendingChunk({ promise, reason: "prefetch" }), + ); } - const promise = this.fetchThenStoreChunk(chunkIdx); - this._chunks.set(chunkIdx, ChunkMap.makePendingChunk({ promise, reason: "prefetch" })); + const nextColsChunkIdx: ChunkMap.ChunkIdx = { chunkRowIdx, chunkColIdx: chunkColIdx + 1 }; + if (!this._chunks.has(nextColsChunkIdx) && this._chunks.chunkIsValid(nextColsChunkIdx)) { + promise = promise.then((_) => this.fetchThenStoreChunk(nextColsChunkIdx)); + this.storeChunkData( + nextColsChunkIdx, + ChunkMap.makePendingChunk({ promise, reason: "prefetch" }), + ); + } } private readonly _loadingParams: Required;