kibana-123
Version:
Kibana is an open source (Apache Licensed), browser based analytics and search dashboard for Elasticsearch. Kibana is a snap to setup and start using. Kibana strives to be easy to get started with, while also being flexible and powerful, just like Elastic
152 lines (129 loc) • 5.94 kB
JavaScript
/**
* The HashedItemStore associates JSON objects with states in browser history and persists these
* objects in sessionStorage. We persist them so that when a tab is closed and re-opened, we can
* retain access to the state objects referenced by the browser history.
*
* Because there is a limit on how much data we can put into sessionStorage, the HashedItemStore
* will attempt to remove old items from storage once that limit is reached.
*
* -------------------------------------------------------------------------------------------------
*
* Consideration 1: We can't (easily) mirror the browser history
*
* If we use letters to indicate a unique state object, and numbers to represent the same state
* occurring again (due to action by the user), a history could look like this:
*
* Old < - - - - - - - - > New
* A1 | B1 | C1 | A2 | D1 | E1
*
* If the user navigates back to C1 and starts to create new states, persisted history states will
* become inaccessible:
*
* Old < - - - - - - - - - - -> New
* A1 | B1 | C1 | F1 | G1 | H1 | I1 (new history states)
* A2 | D1 | E1 (inaccessible persisted history states)
*
* Theoretically, we could build a mirror of the browser history. When the onpopstate event is
* dispatched, we could determine whether we have gone back or forward in history. Then, when
* a new state is persisted, we could delete all of the persisted items which are no longer
* accessible. (Note that this would require reference-counting so that A isn't removed while D and
* E are, since A would still have a remaining reference from A1).
*
* However, the History API doesn't allow us to read from the history beyond the current state. This
* means that if a session is restored, we can't rebuild this browser history mirror.
*
* Due to this imperfect implementation, HashedItemStore ignores the possibility of inaccessible
* history states. In the future, we could implement this history mirror and persist it in
* sessionStorage too. Then, when restoring a session, we can just retrieve it from sessionStorage.
*
* -------------------------------------------------------------------------------------------------
*
* Consideration 2: We can't tell when we've hit the browser history limit
*
* Because some of our persisted history states may no longer be referenced by the browser history,
* and we have no way of knowing which ones, we have no way of knowing whether we've persisted a
* number of accessible states beyond the browser history length limit.
*
* More fundamentally, the browser history length limit is a browser implementation detail, so it
* can change from browser to browser, or over time. Respecting this limit would introduce a lot of
* (unnecessary?) complexity.
*
* For these reasons, HashedItemStore doesn't concern itself with this constraint.
*/
import { pull, sortBy } from 'lodash';
export default class HashedItemStore {
/**
* HashedItemStore uses objects called indexed items to refer to items that have been persisted
* in sessionStorage. An indexed item is shaped {hash, touched}. The touched date is when the item
* was last referenced by the browser history.
*/
constructor(sessionStorage) {
this._sessionStorage = sessionStorage;
// Store indexed items in descending order by touched (oldest first, newest last). We'll use
// this to remove older items when we run out of storage space.
this._indexedItems = [];
// Potentially restore a previously persisted index. This happens when
// we re-open a closed tab.
const persistedItemIndex = this._sessionStorage.getItem(HashedItemStore.PERSISTED_INDEX_KEY);
if (persistedItemIndex) {
this._indexedItems = sortBy(JSON.parse(persistedItemIndex) || [], 'touched');
}
}
setItem(hash, item) {
const isItemPersisted = this._persistItem(hash, item);
if (isItemPersisted) {
this._touchHash(hash);
}
return isItemPersisted;
}
getItem(hash) {
const item = this._sessionStorage.getItem(hash);
if (item !== null) {
this._touchHash(hash);
}
return item;
}
_getIndexedItem(hash) {
return this._indexedItems.find(indexedItem => indexedItem.hash === hash);
}
_persistItem(hash, item) {
try {
this._sessionStorage.setItem(hash, item);
return true;
} catch (e) {
// If there was an error then we need to make some space for the item.
if (this._indexedItems.length === 0) {
// If there's nothing left to remove, then we've run out of space and we're trying to
// persist too large an item.
return false;
}
// We need to try to make some space for the item by removing older items (i.e. items that
// haven't been accessed recently).
this._removeOldestItem();
// Try to persist again.
return this._persistItem(hash, item);
}
}
_removeOldestItem() {
const oldestIndexedItem = this._indexedItems.shift();
// Remove oldest item from storage.
this._sessionStorage.removeItem(oldestIndexedItem.hash);
}
_touchHash(hash) {
// Touching a hash indicates that it's been used recently, so it won't be the first in line
// when we remove items to free up storage space.
// either get or create an indexedItem
const indexedItem = this._getIndexedItem(hash) || { hash };
// set/update the touched time to now so that it's the "newest" item in the index
indexedItem.touched = Date.now();
// ensure that the item is last in the index
pull(this._indexedItems, indexedItem);
this._indexedItems.push(indexedItem);
// Regardless of whether this is a new or updated item, we need to persist the index.
this._sessionStorage.setItem(
HashedItemStore.PERSISTED_INDEX_KEY,
JSON.stringify(this._indexedItems)
);
}
}
HashedItemStore.PERSISTED_INDEX_KEY = 'kbn.hashedItemsIndex.v1';