UNPKG

ag-grid-enterprise

Version:

ag-Grid Enterprise Features

523 lines (410 loc) 19 kB
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 { @Autowired('eventService') private eventService: EventService; @Autowired('context') private context: Context; @Autowired('gridOptionsWrapper') 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(@Qualifier('loggerFactory') loggerFactory: LoggerFactory) { this.logger = loggerFactory.create('ServerSideCache'); } @PostConstruct 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()); } }); } } }