ag-grid-enterprise
Version:
ag-Grid Enterprise Features
523 lines (410 loc) • 19 kB
text/typescript
import {
_,
Autowired,
ColumnVO,
Context,
EventService,
IServerSideCache,
IServerSideDatasource,
LoggerFactory,
NumberSequence,
PostConstruct,
Qualifier,
RowNode,
RowNodeCache,
RowNodeCacheParams,
RowBounds,
GridOptionsWrapper,
RowDataUpdatedEvent,
Events
} from "ag-grid-community";
import {ServerSideBlock} from "./serverSideBlock";
export interface ServerSideCacheParams extends RowNodeCacheParams {
rowGroupCols: ColumnVO[];
valueCols: ColumnVO[];
pivotCols: ColumnVO[];
pivotMode: boolean;
datasource: IServerSideDatasource;
lastAccessedSequence: NumberSequence;
}
export class ServerSideCache extends RowNodeCache<ServerSideBlock, ServerSideCacheParams> implements IServerSideCache {
private eventService: EventService;
private context: Context;
private gridOptionsWrapper: GridOptionsWrapper;
// this will always be zero for the top level cache only,
// all the other ones change as the groups open and close
private displayIndexStart = 0;
private displayIndexEnd = 0; // not sure if setting this one to zero is necessary
private readonly parentRowNode: RowNode;
private cacheTop = 0;
private cacheHeight: number;
private blockHeights: {[blockId: number]: number} = {};
constructor(cacheParams: ServerSideCacheParams, parentRowNode: RowNode) {
super(cacheParams);
this.parentRowNode = parentRowNode;
}
private setBeans( loggerFactory: LoggerFactory) {
this.logger = loggerFactory.create('ServerSideCache');
}
protected init(): void {
super.init();
}
public getRowBounds(index: number): RowBounds {
this.logger.log(`getRowBounds(${index})`);
// we return null if row not found
let result: RowBounds;
let blockFound = false;
let lastBlock: ServerSideBlock;
this.forEachBlockInOrder( block => {
if (blockFound) return;
if (block.isDisplayIndexInBlock(index)) {
result = block.getRowBounds(index, this.getVirtualRowCount());
blockFound = true;
} else if (block.isBlockBefore(index)) {
lastBlock = block;
}
});
if (!blockFound) {
let nextRowTop: number;
let nextRowIndex : number;
if (lastBlock) {
nextRowTop = lastBlock.getBlockTop() + lastBlock.getBlockHeight();
nextRowIndex = lastBlock.getDisplayIndexEnd();
} else {
nextRowTop = this.cacheTop;
nextRowIndex = this.displayIndexStart;
}
let rowsBetween = index - nextRowIndex;
result = {
rowHeight: this.cacheParams.rowHeight,
rowTop: nextRowTop + rowsBetween * this.cacheParams.rowHeight
}
}
// NOTE: what about purged blocks
this.logger.log(`getRowBounds(${index}), result = ${result}`);
return result;
}
protected destroyBlock(block: ServerSideBlock): void {
super.destroyBlock(block);
}
public getRowIndexAtPixel(pixel: number): number {
this.logger.log(`getRowIndexAtPixel(${pixel})`);
// we return null if row not found
let result: number;
let blockFound = false;
let lastBlock: ServerSideBlock;
this.forEachBlockInOrder( block => {
if (blockFound) return;
if (block.isPixelInRange(pixel)) {
result = block.getRowIndexAtPixel(pixel, this.getVirtualRowCount());
blockFound = true;
} else if (block.getBlockTop() < pixel) {
lastBlock = block;
}
});
if (!blockFound) {
let nextRowTop: number;
let nextRowIndex : number;
if (lastBlock) {
nextRowTop = lastBlock.getBlockTop() + lastBlock.getBlockHeight();
nextRowIndex = lastBlock.getDisplayIndexEnd();
} else {
nextRowTop = this.cacheTop;
nextRowIndex = this.displayIndexStart;
}
let pixelsBetween = pixel - nextRowTop;
let rowsBetween = (pixelsBetween / this.cacheParams.rowHeight) | 0;
result = nextRowIndex + rowsBetween;
}
let lastAllowedIndex = this.getDisplayIndexEnd() - 1;
if (result > lastAllowedIndex) {
result = lastAllowedIndex;
}
//NOTE: purged
this.logger.log(`getRowIndexAtPixel(${pixel}) result = ${result}`);
return result;
}
public clearRowTops(): void {
this.forEachBlockInOrder( block => block.clearRowTops(this.getVirtualRowCount()));
}
public setDisplayIndexes(displayIndexSeq: NumberSequence,
nextRowTop: {value: number}): void {
this.displayIndexStart = displayIndexSeq.peek();
this.cacheTop = nextRowTop.value;
let lastBlockId = -1;
this.forEachBlockInOrder( (currentBlock: ServerSideBlock, blockId: number)=> {
// if we skipped blocks, then we need to skip the row indexes. we assume that all missing
// blocks are made up of closed RowNodes only (if they were groups), as we never expire from
// the cache if any row nodes are open.
let blocksSkippedCount = blockId - lastBlockId - 1;
let rowsSkippedCount = blocksSkippedCount * this.cacheParams.blockSize;
if (rowsSkippedCount>0) {
displayIndexSeq.skip(rowsSkippedCount);
}
for (let i = 1; i <= blocksSkippedCount; i++) {
let blockToAddId = blockId - i;
if (_.exists(this.blockHeights[blockToAddId])) {
nextRowTop.value += this.blockHeights[blockToAddId];
} else {
nextRowTop.value += this.cacheParams.blockSize * this.cacheParams.rowHeight;
}
}
lastBlockId = blockId;
currentBlock.setDisplayIndexes(displayIndexSeq, this.getVirtualRowCount(), nextRowTop);
this.blockHeights[blockId] = currentBlock.getBlockHeight();
});
// if any blocks missing at the end, need to increase the row index for them also
// eg if block size = 10, we have total rows of 25 (indexes 0 .. 24), but first 2 blocks loaded (because
// last row was ejected from cache), then:
// lastVisitedRow = 19, virtualRowCount = 25, rows not accounted for = 5 (24 - 19)
let lastVisitedRow = ((lastBlockId + 1) * this.cacheParams.blockSize) -1;
let rowCount = this.getVirtualRowCount();
let rowsNotAccountedFor = rowCount - lastVisitedRow - 1;
if (rowsNotAccountedFor > 0) {
displayIndexSeq.skip(rowsNotAccountedFor);
nextRowTop.value += rowsNotAccountedFor * this.cacheParams.rowHeight;
}
this.displayIndexEnd = displayIndexSeq.peek();
this.cacheHeight = nextRowTop.value - this.cacheTop;
}
// gets called in a) init() above and b) by the grid
public getRow(displayRowIndex: number, dontCreateBlock = false): RowNode {
// this can happen if asking for a row that doesn't exist in the model,
// eg if a cell range is selected, and the user filters so rows no longer
// exist
if (!this.isDisplayIndexInCache(displayRowIndex)) { return null; }
// if we have the block, then this is the block
let block: ServerSideBlock = null;
// this is the last block that we have BEFORE the right block
let beforeBlock: ServerSideBlock = null;
this.forEachBlockInOrder( currentBlock => {
if (currentBlock.isDisplayIndexInBlock(displayRowIndex)) {
block = currentBlock;
} else if (currentBlock.isBlockBefore(displayRowIndex)) {
// this will get assigned many times, but the last time will
// be the closest block to the required block that is BEFORE
beforeBlock = currentBlock;
}
});
// when we are moving rows around, we don't want to trigger loads
if (_.missing(block) && dontCreateBlock) {
return null;
}
// if block not found, we need to load it
if (_.missing(block)) {
let blockNumber: number;
let displayIndexStart: number;
let nextRowTop: number;
// because missing blocks are always fully closed, we can work out
// the start index of the block we want by hopping from the closest block,
// as we know the row count in closed blocks is equal to the page size
if (beforeBlock) {
blockNumber = beforeBlock.getBlockNumber() + 1;
displayIndexStart = beforeBlock.getDisplayIndexEnd();
nextRowTop = beforeBlock.getBlockHeight() + beforeBlock.getBlockTop();
let isInRange = (): boolean => {
return displayRowIndex >= displayIndexStart && displayRowIndex < (displayIndexStart + this.cacheParams.blockSize);
};
while (!isInRange()) {
displayIndexStart += this.cacheParams.blockSize;
let cachedBlockHeight = this.blockHeights[blockNumber];
if (_.exists(cachedBlockHeight)) {
nextRowTop += cachedBlockHeight;
} else {
nextRowTop += this.cacheParams.rowHeight * this.cacheParams.blockSize;
}
blockNumber++;
}
} else {
let localIndex = displayRowIndex - this.displayIndexStart;
blockNumber = Math.floor(localIndex / this.cacheParams.blockSize);
displayIndexStart = this.displayIndexStart + (blockNumber * this.cacheParams.blockSize);
nextRowTop = this.cacheTop + (blockNumber * this.cacheParams.blockSize * this.cacheParams.rowHeight);
}
block = this.createBlock(blockNumber, displayIndexStart, {value: nextRowTop});
this.logger.log(`block missing, rowIndex = ${displayRowIndex}, creating #${blockNumber}, displayIndexStart = ${displayIndexStart}`);
}
let rowNode = block.getRow(displayRowIndex);
return rowNode;
}
private createBlock(blockNumber: number, displayIndex: number, nextRowTop: {value: number}): ServerSideBlock {
let newBlock = new ServerSideBlock(blockNumber, this.parentRowNode, this.cacheParams, this);
this.context.wireBean(newBlock);
let displayIndexSequence = new NumberSequence(displayIndex);
newBlock.setDisplayIndexes(displayIndexSequence, this.getVirtualRowCount(), nextRowTop);
this.postCreateBlock(newBlock);
return newBlock;
}
public getDisplayIndexEnd(): number {
return this.displayIndexEnd;
}
public isDisplayIndexInCache(displayIndex: number): boolean {
if (this.getVirtualRowCount()===0) { return false; }
return displayIndex >= this.displayIndexStart && displayIndex < this.displayIndexEnd;
}
public getChildCache(keys: string[]): ServerSideCache {
if (_.missingOrEmpty(keys)) { return this; }
let nextKey = keys[0];
let nextServerSideCache: ServerSideCache = null;
this.forEachBlockInOrder(block => {
// callback: (rowNode: RowNode, index: number) => void, sequence: NumberSequence, rowCount: number
block.forEachNodeShallow( rowNode => {
if (rowNode.key === nextKey) {
nextServerSideCache = <ServerSideCache> rowNode.childrenCache;
}
}, new NumberSequence(), this.getVirtualRowCount());
});
if (nextServerSideCache) {
let keyListForNextLevel = keys.slice(1, keys.length);
return nextServerSideCache.getChildCache(keyListForNextLevel);
} else {
return null;
}
}
public isPixelInRange(pixel: number): boolean {
if (this.getVirtualRowCount()===0) { return false; }
return pixel >= this.cacheTop && pixel < (this.cacheTop + this.cacheHeight);
}
public removeFromCache(items: any[]): void {
// create map of id's for quick lookup
let itemsToDeleteById: {[id: string]: any} = {};
let idForNodeFunc = this.gridOptionsWrapper.getRowNodeIdFunc();
items.forEach( item => {
let id = idForNodeFunc(item);
itemsToDeleteById[id] = item;
});
let deletedCount = 0;
this.forEachBlockInOrder( block => {
let startRow = block.getStartRow();
let endRow = block.getEndRow();
let deletedCountFromThisBlock = 0;
for (let rowIndex = startRow; rowIndex<endRow; rowIndex++) {
let rowNode = block.getRowUsingLocalIndex(rowIndex, true);
if (!rowNode) { continue; }
let deleteThisRow = !!itemsToDeleteById[rowNode.id];
if (deleteThisRow) {
deletedCountFromThisBlock++;
deletedCount++;
block.setDirty();
rowNode.clearRowTop();
continue;
}
// if rows were deleted, then we need to move this row node to
// it's new location
if (deletedCount>0) {
block.setDirty();
let newIndex = rowIndex - deletedCount;
let blockId = Math.floor(newIndex / this.cacheParams.blockSize);
let blockToInsert = this.getBlock(blockId);
if (blockToInsert) {
blockToInsert.setRowNode(newIndex, rowNode);
}
}
}
if (deletedCountFromThisBlock>0) {
for (let i = deletedCountFromThisBlock; i>0; i--) {
block.setBlankRowNode(endRow-i);
}
}
});
if (this.isMaxRowFound()) {
this.hack_setVirtualRowCount(this.getVirtualRowCount() - deletedCount);
}
this.onCacheUpdated();
let event: RowDataUpdatedEvent = {
type: Events.EVENT_ROW_DATA_UPDATED,
api: this.gridOptionsWrapper.getApi(),
columnApi: this.gridOptionsWrapper.getColumnApi()
};
this.eventService.dispatchEvent(event);
}
public addToCache(items: any[], indexToInsert: number): void {
let newNodes: RowNode[] = [];
this.forEachBlockInReverseOrder( block => {
let pageEndRow = block.getEndRow();
// if the insertion is after this page, then this page is not impacted
if (pageEndRow <= indexToInsert) {
return;
}
this.moveItemsDown(block, indexToInsert, items.length);
let newNodesThisPage = this.insertItems(block, indexToInsert, items);
newNodesThisPage.forEach(rowNode => newNodes.push(rowNode));
});
if (this.isMaxRowFound()) {
this.hack_setVirtualRowCount(this.getVirtualRowCount() + items.length);
}
this.onCacheUpdated();
let event: RowDataUpdatedEvent = {
type: Events.EVENT_ROW_DATA_UPDATED,
api: this.gridOptionsWrapper.getApi(),
columnApi: this.gridOptionsWrapper.getColumnApi()
};
this.eventService.dispatchEvent(event);
}
private moveItemsDown(block: ServerSideBlock, moveFromIndex: number, moveCount: number): void {
let startRow = block.getStartRow();
let endRow = block.getEndRow();
let indexOfLastRowToMove = moveFromIndex + moveCount;
// all rows need to be moved down below the insertion index
for (let currentRowIndex = endRow - 1; currentRowIndex >= startRow; currentRowIndex--) {
// don't move rows at or before the insertion index
if (currentRowIndex < indexOfLastRowToMove) {
continue;
}
let indexOfNodeWeWant = currentRowIndex - moveCount;
let nodeForThisIndex = this.getRow(indexOfNodeWeWant, true);
if (nodeForThisIndex) {
block.setRowNode(currentRowIndex, nodeForThisIndex);
} else {
block.setBlankRowNode(currentRowIndex);
block.setDirty();
}
}
}
private insertItems(block: ServerSideBlock, indexToInsert: number, items: any[]): RowNode[] {
let pageStartRow = block.getStartRow();
let pageEndRow = block.getEndRow();
let newRowNodes: RowNode[] = [];
// next stage is insert the rows into this page, if applicable
for (let index = 0; index < items.length; index++) {
let rowIndex = indexToInsert + index;
let currentRowInThisPage = rowIndex >= pageStartRow && rowIndex < pageEndRow;
if (currentRowInThisPage) {
let dataItem = items[index];
let newRowNode = block.setNewData(rowIndex, dataItem);
newRowNodes.push(newRowNode);
}
}
return newRowNodes;
}
public refreshCacheAfterSort(changedColumnsInSort: string[], rowGroupColIds: string[]): void {
let level = this.parentRowNode.level + 1;
let grouping = level < this.cacheParams.rowGroupCols.length;
let shouldPurgeCache: boolean;
if (grouping) {
let groupColVo = this.cacheParams.rowGroupCols[level];
let groupField = groupColVo.field;
let rowGroupBlock = rowGroupColIds.indexOf(groupField) > -1;
let sortingByGroup = changedColumnsInSort.indexOf(groupField) > -1;
shouldPurgeCache = rowGroupBlock && sortingByGroup;
} else {
shouldPurgeCache = true;
}
if (shouldPurgeCache) {
this.purgeCache();
} else {
this.forEachBlockInOrder(block => {
if (block.isGroupLevel()) {
let callback = (rowNode: RowNode) => {
let nextCache = (<ServerSideCache> rowNode.childrenCache);
if (nextCache) {
nextCache.refreshCacheAfterSort(changedColumnsInSort, rowGroupColIds);
}
};
block.forEachNodeShallow(callback, new NumberSequence(), this.getVirtualRowCount());
}
});
}
}
}