Spaces:
Sleeping
Sleeping
| // @flow | |
| import {OverscaledTileID} from './tile_id'; | |
| import type Tile from './tile'; | |
| /** | |
| * A [least-recently-used cache](http://en.wikipedia.org/wiki/Cache_algorithms) | |
| * with hash lookup made possible by keeping a list of keys in parallel to | |
| * an array of dictionary of values | |
| * | |
| * @private | |
| */ | |
| class TileCache { | |
| max: number; | |
| data: {[key: string]: Array<{ value: Tile, timeout: ?TimeoutID}>}; | |
| order: Array<string>; | |
| onRemove: (element: Tile) => void; | |
| /** | |
| * @param {number} max number of permitted values | |
| * @param {Function} onRemove callback called with items when they expire | |
| */ | |
| constructor(max: number, onRemove: (element: Tile) => void) { | |
| this.max = max; | |
| this.onRemove = onRemove; | |
| this.reset(); | |
| } | |
| /** | |
| * Clear the cache | |
| * | |
| * @returns {TileCache} this cache | |
| * @private | |
| */ | |
| reset() { | |
| for (const key in this.data) { | |
| for (const removedData of this.data[key]) { | |
| if (removedData.timeout) clearTimeout(removedData.timeout); | |
| this.onRemove(removedData.value); | |
| } | |
| } | |
| this.data = {}; | |
| this.order = []; | |
| return this; | |
| } | |
| /** | |
| * Add a key, value combination to the cache, trimming its size if this pushes | |
| * it over max length. | |
| * | |
| * @param {OverscaledTileID} tileID lookup key for the item | |
| * @param {*} data any value | |
| * | |
| * @returns {TileCache} this cache | |
| * @private | |
| */ | |
| add(tileID: OverscaledTileID, data: Tile, expiryTimeout: number | void) { | |
| const key = tileID.wrapped().key; | |
| if (this.data[key] === undefined) { | |
| this.data[key] = []; | |
| } | |
| const dataWrapper = { | |
| value: data, | |
| timeout: undefined | |
| }; | |
| if (expiryTimeout !== undefined) { | |
| dataWrapper.timeout = setTimeout(() => { | |
| this.remove(tileID, dataWrapper); | |
| }, expiryTimeout); | |
| } | |
| this.data[key].push(dataWrapper); | |
| this.order.push(key); | |
| if (this.order.length > this.max) { | |
| const removedData = this._getAndRemoveByKey(this.order[0]); | |
| if (removedData) this.onRemove(removedData); | |
| } | |
| return this; | |
| } | |
| /** | |
| * Determine whether the value attached to `key` is present | |
| * | |
| * @param {OverscaledTileID} tileID the key to be looked-up | |
| * @returns {boolean} whether the cache has this value | |
| * @private | |
| */ | |
| has(tileID: OverscaledTileID): boolean { | |
| return tileID.wrapped().key in this.data; | |
| } | |
| /** | |
| * Get the value attached to a specific key and remove data from cache. | |
| * If the key is not found, returns `null` | |
| * | |
| * @param {OverscaledTileID} tileID the key to look up | |
| * @returns {*} the data, or null if it isn't found | |
| * @private | |
| */ | |
| getAndRemove(tileID: OverscaledTileID): ?Tile { | |
| if (!this.has(tileID)) { return null; } | |
| return this._getAndRemoveByKey(tileID.wrapped().key); | |
| } | |
| /* | |
| * Get and remove the value with the specified key. | |
| */ | |
| _getAndRemoveByKey(key: string): ?Tile { | |
| const data = this.data[key].shift(); | |
| if (data.timeout) clearTimeout(data.timeout); | |
| if (this.data[key].length === 0) { | |
| delete this.data[key]; | |
| } | |
| this.order.splice(this.order.indexOf(key), 1); | |
| return data.value; | |
| } | |
| /* | |
| * Get the value with the specified (wrapped tile) key. | |
| */ | |
| getByKey(key: string): ?Tile { | |
| const data = this.data[key]; | |
| return data ? data[0].value : null; | |
| } | |
| /** | |
| * Get the value attached to a specific key without removing data | |
| * from the cache. If the key is not found, returns `null` | |
| * | |
| * @param {OverscaledTileID} tileID the key to look up | |
| * @returns {*} the data, or null if it isn't found | |
| * @private | |
| */ | |
| get(tileID: OverscaledTileID): ?Tile { | |
| if (!this.has(tileID)) { return null; } | |
| const data = this.data[tileID.wrapped().key][0]; | |
| return data.value; | |
| } | |
| /** | |
| * Remove a key/value combination from the cache. | |
| * | |
| * @param {OverscaledTileID} tileID the key for the pair to delete | |
| * @param {Tile} value If a value is provided, remove that exact version of the value. | |
| * @returns {TileCache} this cache | |
| * @private | |
| */ | |
| remove(tileID: OverscaledTileID, value: ?{ value: Tile, timeout: ?TimeoutID}) { | |
| if (!this.has(tileID)) { return this; } | |
| const key = tileID.wrapped().key; | |
| const dataIndex = value === undefined ? 0 : this.data[key].indexOf(value); | |
| const data = this.data[key][dataIndex]; | |
| this.data[key].splice(dataIndex, 1); | |
| if (data.timeout) clearTimeout(data.timeout); | |
| if (this.data[key].length === 0) { | |
| delete this.data[key]; | |
| } | |
| this.onRemove(data.value); | |
| this.order.splice(this.order.indexOf(key), 1); | |
| return this; | |
| } | |
| /** | |
| * Change the max size of the cache. | |
| * | |
| * @param {number} max the max size of the cache | |
| * @returns {TileCache} this cache | |
| * @private | |
| */ | |
| setMaxSize(max: number): TileCache { | |
| this.max = max; | |
| while (this.order.length > this.max) { | |
| const removedData = this._getAndRemoveByKey(this.order[0]); | |
| if (removedData) this.onRemove(removedData); | |
| } | |
| return this; | |
| } | |
| /** | |
| * Remove entries that do not pass a filter function. Used for removing | |
| * stale tiles from the cache. | |
| * | |
| * @param {function} filterFn Determines whether the tile is filtered. If the supplied function returns false, the tile will be filtered out. | |
| */ | |
| filter(filterFn: (tile: Tile) => boolean) { | |
| const removed = []; | |
| for (const key in this.data) { | |
| for (const entry of this.data[key]) { | |
| if (!filterFn(entry.value)) { | |
| removed.push(entry); | |
| } | |
| } | |
| } | |
| for (const r of removed) { | |
| this.remove(r.value.tileID, r); | |
| } | |
| } | |
| } | |
| export default TileCache; | |