/** * The **Infinite Row Model** — the strategy that maps a continuous scroll * position onto fixed-size pages, fetching them on demand and rendering * not-yet-loaded rows as skeletons. * * Where the Server-Side Row Model refetches the whole viewport on every state * change and keeps one request in flight, this model keeps a *window* of pages * resident, fetches several concurrently, and never re-requests a page it * already holds. The scrollbar spans the full dataset from the first response, * so any row is reachable immediately. * * Responsibilities are split into focused collaborators, mirroring the SSRM * layout: * - {@link ServerRequestBuilder} — snapshots grid state into a typed request. * - {@link InfinitePageCache} — generation-scoped LRU with viewport pinning. * - {@link InfiniteRequestQueue} — dedup, concurrency, per-page abort, retry. * - {@link computePageWindow} — pure row-range → page-range geometry. * * This orchestrator owns only sequencing, the sparse row array and lifecycle * events; it never sorts, filters or paginates locally. * * ### Memory * Row nodes exist only for cached pages and the rendered window; every other * index in the row array is a hole. Memory therefore scales with * `maxCachedPages × pageSize`, not with the dataset — ten million rows cost the * same as ten thousand. This is safe because the renderer, told that the model * has a uniform row height, derives its window arithmetically and never * iterates the array. * * @packageDocumentation */ import type { GridContext } from '../../core/grid-context'; import type { InfiniteScrollConfig, InfiniteStats } from '../../types/infinite.types'; import type { ServerSideDatasource } from '../../types/server-side.types'; import type { RowModelStrategy } from '../row-model-strategy'; /** Loads pages on demand as the user scrolls, caching them under an LRU bound. */ export declare class InfiniteRowModel implements RowModelStrategy { private readonly ctx; readonly type: "infinite"; /** * Every row is the configured height, so the renderer may compute the total * content height arithmetically instead of summing the array — which is both * faster and what makes the sparse array safe to publish. */ readonly uniformRowHeight = true; private readonly cfg; private readonly builder; private readonly cache; private readonly queue; private datasource; /** * The published row array. Sparse by design: only indices inside a cached * page or the rendered window hold a node. */ private rows; /** Rows the dataset reports, or the configured assumption before we know. */ private totalRows; /** `true` once a response has told us the real total. */ private totalKnown; /** Last row range the renderer asked for, so refreshes can re-target it. */ private renderStart; private renderEnd; private requestSeq; private syncTimer; private destroyed; private firstLoadPending; private pagesLoaded; private pagesFailed; constructor(ctx: GridContext, config?: InfiniteScrollConfig, datasource?: ServerSideDatasource); /** * Invoked by `applyPipeline()` on every refresh. * * A sort, filter or search change rewrites which rows live at which index, so * the cached pages describe a dataset that no longer exists: the signature * check drops them, cancels in-flight work and re-fetches from the top. */ buildDisplayedRows(): void; /** Initial kick-off after grid initialisation. */ start(): void; /** * The renderer's report of which rows it is about to paint. * * This, rather than a scroll listener, is what drives loading: the renderer * already owns virtualisation, so taking its row range keeps one copy of that * maths in the codebase and guarantees the model serves exactly what is being * painted. * * The window is filled **synchronously** — from cache where possible, with * skeletons otherwise — because the renderer slices the row array immediately * after this returns and a sparse array must have no holes inside that slice. * Fetching what is still missing is debounced separately. * * @param startRow - First row index to be painted, inclusive. * @param endRow - Row index painting stops at, exclusive. */ onRenderWindow(startRow: number, endRow: number): void; /** * Guarantees every index in a range holds a node. * * Cached rows are materialised; anything still missing becomes a skeleton. * O(window), and it allocates only for indices that are actually empty, so a * stationary viewport does no work at all after the first pass. */ private ensureWindow; /** Aborts in-flight work, cancels timers and releases the datasource. */ destroy(): void; /** Replaces the datasource and reloads from the current position. */ setDatasource(datasource: ServerSideDatasource | null): void; /** * Reloads the resident pages. `purge` also empties the cache, so previously * loaded pages are fetched fresh rather than served from memory. */ refresh(params?: { purge?: boolean; }): void; /** * Drops a range of cached pages so they reload on next sight. * * @param from - First page index, inclusive. Omit for the whole cache. * @param to - Last page index, inclusive. Omit to run to the end. */ invalidatePages(from?: number, to?: number): void; /** A snapshot of cache and request state, for diagnostics. */ getStats(): InfiniteStats; /** * Coalesces bursts of window changes into one pass. * * During a fast scroll the render window changes every frame; without this a * flick through a million rows would request every page it passed over. Only * the range the user actually settles near survives the debounce. */ private scheduleSync; /** Materialises what is cached and requests what is missing. */ private sync; /** Issues one page request and applies its result. */ private loadPage; /** * Reconciles the dataset size with what the response reported. * * A datasource that omits `totalRows` still terminates the list correctly: a * short page means the end has been reached, so the total is pinned to that * page's last row. * * @returns `true` when the row array's length changed. */ private adoptTotalRows; /** * Writes a cached page's rows into the row array. * * Nodes are only created where one is missing or its data changed, so a * re-materialised page reuses its existing nodes and the renderer's row cache * (and any selection on those rows) survives. * * @returns `true` when anything changed. */ private materialisePage; /** * Puts placeholder nodes in a page's slots so unloaded rows render as * skeletons rather than as holes the renderer would trip over. * * @returns `true` when anything changed. */ private fillSkeletons; /** * Publishes the row array to the store. * * The array is mutated in place while its length is stable, because the * renderer keys total-height and scrollbar recomputation off the array * *reference* — reusing it means a page arriving repaints the window without * touching layout, which is what keeps the scroll position rock-steady. Only * a length change hands over a fresh reference. * * @param lengthChanged - Whether the dataset size changed. */ private publishRows; /** Builds a data node for a loaded row. */ private createNode; /** Builds a placeholder node for a row that has not loaded yet. */ private createSkeleton; /** * Stable identity for a row. * * Prefers the application's own id field so a row keeps its identity across * refetches — selection, and the renderer's DOM reuse, both key off it. Falls * back to the absolute row index, which is stable for as long as the query is. */ private nodeIdFor; /** Builds the request for one page, overriding the pager-derived row range. */ private buildPageRequest; /** Clears the initial loading overlay once the first page settles. */ private finishFirstLoad; /** * Writes the shared loading flag. `GridCore` watches this store key and is * the single emitter of `LOADING_STARTED` / `LOADING_STOPPED`, so this must * not emit them itself — doing so would double-fire for every page load. */ private setLoading; } //# sourceMappingURL=infinite-row-model.d.ts.map