ag-grid
Version:
Advanced Data Grid / Data Table supporting Javascript / React / AngularJS / Web Components
309 lines (249 loc) • 11.3 kB
text/typescript
import {NumberSequence, Utils as _} from "../../utils";
import {RowNode} from "../../entities/rowNode";
import {BeanStub} from "../../context/beanStub";
import {RowNodeBlock} from "./rowNodeBlock";
import {Logger} from "../../logger";
import {RowNodeBlockLoader} from "./rowNodeBlockLoader";
import {AgEvent} from "../../events";
export interface RowNodeCacheParams {
initialRowCount: number;
blockSize: number;
overflowSize: number;
sortModel: any;
filterModel: any;
maxBlocksInCache: number;
rowHeight: number;
lastAccessedSequence: NumberSequence;
maxConcurrentRequests: number;
rowNodeBlockLoader: RowNodeBlockLoader;
dynamicRowHeight: boolean;
}
export interface CacheUpdatedEvent extends AgEvent {
}
export abstract class RowNodeCache<T extends RowNodeBlock, P extends RowNodeCacheParams> extends BeanStub {
public static EVENT_CACHE_UPDATED = 'cacheUpdated';
private virtualRowCount: number;
private maxRowFound = false;
protected cacheParams: P;
private active: boolean;
public blocks: {[blockNumber: string]: T} = {};
private blockCount = 0;
protected logger: Logger;
public abstract getRow(rowIndex: number): RowNode;
protected constructor(cacheParams: P) {
super();
this.virtualRowCount = cacheParams.initialRowCount;
this.cacheParams = cacheParams;
}
public destroy(): void {
super.destroy();
this.forEachBlockInOrder( block => this.destroyBlock(block) );
}
protected init(): void {
this.active = true;
this.addDestroyFunc( ()=> this.active = false );
}
public isActive(): boolean {
return this.active;
}
public getVirtualRowCount(): number {
return this.virtualRowCount;
}
public hack_setVirtualRowCount(virtualRowCount: number): void {
this.virtualRowCount = virtualRowCount;
}
public isMaxRowFound(): boolean {
return this.maxRowFound;
}
// listener on EVENT_LOAD_COMPLETE
protected onPageLoaded(event: any): void {
this.cacheParams.rowNodeBlockLoader.loadComplete();
this.checkBlockToLoad();
// if we are not active, then we ignore all events, otherwise we could end up getting the
// grid to refresh even though we are no longer the active cache
if (!this.isActive()) {
return;
}
this.logger.log(`onPageLoaded: page = ${event.page.getBlockNumber()}, lastRow = ${event.lastRow}`);
if (event.success) {
this.checkVirtualRowCount(event.page, event.lastRow);
}
}
private purgeBlocksIfNeeded(blockToExclude: T): void {
// no purge if user didn't give maxBlocksInCache
if (_.missing(this.cacheParams.maxBlocksInCache)) { return; }
// no purge if block count is less than max allowed
if (this.blockCount <= this.cacheParams.maxBlocksInCache) { return; }
// put all candidate blocks into a list for sorting
let blocksForPurging: T[] = [];
this.forEachBlockInOrder( (block: T)=> {
// we exclude checking for the page just created, as this has yet to be accessed and hence
// the lastAccessed stamp will not be updated for the first time yet
if (block === blockToExclude) {
return;
}
blocksForPurging.push(block);
});
// note: need to verify that this sorts items in the right order
blocksForPurging.sort( (a: T, b: T) => b.getLastAccessed() - a.getLastAccessed());
// we remove (maxBlocksInCache - 1) as we already excluded the 'just created' page.
// in other words, after the splice operation below, we have taken out the blocks
// we want to keep, which means we are left with blocks that we can potentially purge
let blocksToKeep = this.cacheParams.maxBlocksInCache - 1;
blocksForPurging.splice(0, blocksToKeep);
// try and purge each block
blocksForPurging.forEach( block => {
// we never purge blocks if they are open, as purging them would mess up with
// our indexes, it would be very messy to restore the purged block to it's
// previous state if it had open children (and what if open children of open
// children, jeeeesus, just thinking about it freaks me out) so best is have a
// rule, if block is open, we never purge.
if (block.isAnyNodeOpen(this.virtualRowCount)) { return; }
// at this point, block is not needed, and no open nodes, so burn baby burn
this.removeBlockFromCache(block);
});
}
protected postCreateBlock(newBlock: T): void {
newBlock.addEventListener(RowNodeBlock.EVENT_LOAD_COMPLETE, this.onPageLoaded.bind(this));
this.setBlock(newBlock.getBlockNumber(), newBlock);
this.purgeBlocksIfNeeded(newBlock);
this.checkBlockToLoad();
}
protected removeBlockFromCache(blockToRemove: T): void {
if (!blockToRemove) {
return;
}
this.destroyBlock(blockToRemove);
// we do not want to remove the 'loaded' event listener, as the
// concurrent loads count needs to be updated when the load is complete
// if the purged page is in loading state
}
// gets called after: 1) block loaded 2) block created 3) cache refresh
protected checkBlockToLoad() {
this.cacheParams.rowNodeBlockLoader.checkBlockToLoad();
}
protected checkVirtualRowCount(block: T, lastRow: any): void {
// if client provided a last row, we always use it, as it could change between server calls
// if user deleted data and then called refresh on the grid.
if (typeof lastRow === 'number' && lastRow >= 0) {
this.virtualRowCount = lastRow;
this.maxRowFound = true;
this.onCacheUpdated();
} else if (!this.maxRowFound) {
// otherwise, see if we need to add some virtual rows
let lastRowIndex = (block.getBlockNumber() + 1) * this.cacheParams.blockSize;
let lastRowIndexPlusOverflow = lastRowIndex + this.cacheParams.overflowSize;
if (this.virtualRowCount < lastRowIndexPlusOverflow) {
this.virtualRowCount = lastRowIndexPlusOverflow;
this.onCacheUpdated();
} else if (this.cacheParams.dynamicRowHeight) {
// the only other time is if dynamic row height, as loading rows
// will change the height of the block, given the height of the rows
// is only known after the row is loaded.
this.onCacheUpdated();
}
}
}
public setVirtualRowCount(rowCount: number, maxRowFound?: boolean): void {
this.virtualRowCount = rowCount;
// if undefined is passed, we do not set this value, if one of {true,false}
// is passed, we do set the value.
if (_.exists(maxRowFound)) {
this.maxRowFound = maxRowFound;
}
// if we are still searching, then the row count must not end at the end
// of a particular page, otherwise the searching will not pop into the
// next page
if (!this.maxRowFound) {
if (this.virtualRowCount % this.cacheParams.blockSize === 0) {
this.virtualRowCount++;
}
}
this.onCacheUpdated();
}
public forEachNodeDeep(callback: (rowNode: RowNode, index: number)=> void, sequence: NumberSequence): void {
this.forEachBlockInOrder(block => {
block.forEachNodeDeep(callback, sequence, this.virtualRowCount);
});
}
public forEachBlockInOrder(callback: (block: T, id: number)=>void): void {
let ids = this.getBlockIdsSorted();
this.forEachBlockId(ids, callback);
}
protected forEachBlockInReverseOrder(callback: (block: T, id: number)=>void): void {
let ids = this.getBlockIdsSorted().reverse();
this.forEachBlockId(ids, callback);
}
private forEachBlockId(ids: number[], callback: (block: T, id: number)=>void): void {
ids.forEach( id => {
let block = this.blocks[id];
callback(block, id);
});
}
protected getBlockIdsSorted(): number[] {
// get all page id's as NUMBERS (not strings, as we need to sort as numbers) and in descending order
let numberComparator = (a: number, b: number) => a - b; // default comparator for array is string comparison
let blockIds = Object.keys(this.blocks).map(idStr => parseInt(idStr)).sort(numberComparator);
return blockIds;
}
protected getBlock(blockId: string|number): T {
return this.blocks[blockId];
}
protected setBlock(id: number, block: T): void {
this.blocks[id] = block;
this.blockCount++;
this.cacheParams.rowNodeBlockLoader.addBlock(block);
}
protected destroyBlock(block: T): void {
delete this.blocks[block.getBlockNumber()];
block.destroy();
this.blockCount--;
this.cacheParams.rowNodeBlockLoader.removeBlock(block);
}
// gets called 1) row count changed 2) cache purged 3) items inserted
protected onCacheUpdated(): void {
if (this.isActive()) {
// this results in both row models (infinite and server side) firing ModelUpdated,
// however server side row model also updates the row indexes first
let event: CacheUpdatedEvent = {
type: RowNodeCache.EVENT_CACHE_UPDATED
};
this.dispatchEvent(event);
}
}
public purgeCache(): void {
this.forEachBlockInOrder(block => this.removeBlockFromCache(block));
this.onCacheUpdated();
}
public getRowNodesInRange(firstInRange: RowNode, lastInRange: RowNode): RowNode[] {
let result: RowNode[] = [];
let lastBlockId = -1;
let inActiveRange = false;
let numberSequence: NumberSequence = new NumberSequence();
// if only one node passed, we start the selection at the top
if (_.missing(firstInRange)) {
inActiveRange = true;
}
let foundGapInSelection = false;
this.forEachBlockInOrder((block: RowNodeBlock, id: number) => {
if (foundGapInSelection) return;
if (inActiveRange && (lastBlockId + 1 !== id)) {
foundGapInSelection = true;
return;
}
lastBlockId = id;
block.forEachNodeShallow(rowNode => {
let hitFirstOrLast = rowNode === firstInRange || rowNode === lastInRange;
if (inActiveRange || hitFirstOrLast) {
result.push(rowNode);
}
if (hitFirstOrLast) {
inActiveRange = !inActiveRange;
}
}, numberSequence, this.virtualRowCount);
});
// inActiveRange will be still true if we never hit the second rowNode
let invalidRange = foundGapInSelection || inActiveRange;
return invalidRange ? [] : result;
}
}