From 8695f6a1b034c625f7aba2c56edb16cac769d0ec Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Thu, 5 Dec 2019 15:03:44 +0100 Subject: [PATCH] [State Management] Move HashedItemStore to kibana_utils plugin. Make it stateless on memory level. (#52172) (#52271) HashedItemStore was also moved to the kibana_utils plugin --- .../public/dashboard/dashboard_state.test.ts | 1 - .../state_management/__tests__/state.js | 2 +- .../ui/public/state_management/state.js | 9 +- .../state_hashing/hash_unhash_url.test.ts | 37 +++---- .../state_hashing/hash_unhash_url.ts | 6 +- .../state_hashing/state_hash.test.ts | 5 +- .../state_hashing/state_hash.ts | 4 +- .../hashed_item_store_singleton.ts | 22 ----- src/plugins/kibana_utils/public/index.ts | 1 + .../hashed_item_store.test.ts | 85 +++++++++++++++- .../hashed_item_store}/hashed_item_store.ts | 97 +++++++++++++------ .../storage/hashed_item_store}/index.ts | 4 +- .../public/storage/hashed_item_store}/mock.ts | 9 +- .../kibana_utils/public/storage/types.ts | 16 +-- 14 files changed, 198 insertions(+), 100 deletions(-) delete mode 100644 src/legacy/ui/public/state_management/state_storage/hashed_item_store_singleton.ts rename src/{legacy/ui/public/state_management/state_storage => plugins/kibana_utils/public/storage/hashed_item_store}/hashed_item_store.test.ts (79%) rename src/{legacy/ui/public/state_management/state_storage => plugins/kibana_utils/public/storage/hashed_item_store}/hashed_item_store.ts (70%) rename src/{legacy/ui/public/state_management/state_storage => plugins/kibana_utils/public/storage/hashed_item_store}/index.ts (83%) rename src/{legacy/ui/public/state_management/state_storage => plugins/kibana_utils/public/storage/hashed_item_store}/mock.ts (82%) diff --git a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_state.test.ts b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_state.test.ts index 209fc7af7f402..e810867c699e0 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_state.test.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_state.test.ts @@ -18,7 +18,6 @@ */ import './np_core.test.mocks'; -import 'ui/state_management/state_storage/mock'; import { DashboardStateManager } from './dashboard_state_manager'; import { getAppStateMock, getSavedDashboardMock } from './__tests__'; import { AppStateClass } from './legacy_imports'; diff --git a/src/legacy/ui/public/state_management/__tests__/state.js b/src/legacy/ui/public/state_management/__tests__/state.js index 0848604889870..6f6f74c9d2bec 100644 --- a/src/legacy/ui/public/state_management/__tests__/state.js +++ b/src/legacy/ui/public/state_management/__tests__/state.js @@ -30,7 +30,7 @@ import { isStateHash, unhashQuery } from '../state_hashing'; -import { HashedItemStore } from '../state_storage/hashed_item_store'; +import { HashedItemStore } from '../../../../../plugins/kibana_utils/public'; import { StubBrowserStorage } from 'test_utils/stub_browser_storage'; import { EventsProvider } from '../../events'; diff --git a/src/legacy/ui/public/state_management/state.js b/src/legacy/ui/public/state_management/state.js index ca57252ba3e72..359dfa5749611 100644 --- a/src/legacy/ui/public/state_management/state.js +++ b/src/legacy/ui/public/state_management/state.js @@ -35,10 +35,7 @@ import { fatalError, toastNotifications } from '../notify'; import './config_provider'; import { createLegacyClass } from '../utils/legacy_class'; import { callEach } from '../utils/function'; - -import { - HashedItemStoreSingleton, -} from './state_storage'; +import { hashedItemStore } from '../../../../plugins/kibana_utils/public'; import { createStateHash, isStateHash @@ -56,13 +53,13 @@ export function StateProvider(Private, $rootScope, $location, stateManagementCon function State( urlParam, defaults, - hashedItemStore = HashedItemStoreSingleton + _hashedItemStore = hashedItemStore ) { State.Super.call(this); this.setDefaults(defaults); this._urlParam = urlParam || '_s'; - this._hashedItemStore = hashedItemStore; + this._hashedItemStore = _hashedItemStore; // When the URL updates we need to fetch the values from the URL this._cleanUpListeners = _.partial(callEach, [ diff --git a/src/legacy/ui/public/state_management/state_hashing/hash_unhash_url.test.ts b/src/legacy/ui/public/state_management/state_hashing/hash_unhash_url.test.ts index 8a298be8636c8..afbe86a4b4d12 100644 --- a/src/legacy/ui/public/state_management/state_hashing/hash_unhash_url.test.ts +++ b/src/legacy/ui/public/state_management/state_hashing/hash_unhash_url.test.ts @@ -17,14 +17,15 @@ * under the License. */ -import { mockSessionStorage } from '../state_storage/mock'; -import { HashedItemStore } from '../state_storage/hashed_item_store'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { mockStorage } from '../../../../../plugins/kibana_utils/public/storage/hashed_item_store/mock'; +import { HashedItemStore } from '../../../../../plugins/kibana_utils/public'; import { hashUrl, unhashUrl } from './hash_unhash_url'; describe('hash unhash url', () => { beforeEach(() => { - mockSessionStorage.clear(); - mockSessionStorage.setStubbedSizeLimit(5000000); + mockStorage.clear(); + mockStorage.setStubbedSizeLimit(5000000); }); describe('hash url', () => { @@ -96,8 +97,8 @@ describe('hash unhash url', () => { expect(result).toMatchInlineSnapshot( `"https://localhost:5601/app/kibana#/discover?foo=bar&_g=h@4e60e02"` ); - expect(mockSessionStorage.getItem('kbn.hashedItemsIndex.v1')).toBeTruthy(); - expect(mockSessionStorage.getItem('h@4e60e02')).toEqual(JSON.stringify({ yes: true })); + expect(mockStorage.getItem('kbn.hashedItemsIndex.v1')).toBeTruthy(); + expect(mockStorage.getItem('h@4e60e02')).toEqual(JSON.stringify({ yes: true })); }); it('if uses multiple states params', () => { @@ -112,15 +113,15 @@ describe('hash unhash url', () => { expect(result).toMatchInlineSnapshot( `"https://localhost:5601/app/kibana#/discover?foo=bar&_g=h@4e60e02&_a=h@61fa078&_b=(yes:!f)"` ); - expect(mockSessionStorage.getItem('h@4e60e02')).toEqual(JSON.stringify({ yes: true })); - expect(mockSessionStorage.getItem('h@61fa078')).toEqual(JSON.stringify({ yes: false })); + expect(mockStorage.getItem('h@4e60e02')).toEqual(JSON.stringify({ yes: true })); + expect(mockStorage.getItem('h@61fa078')).toEqual(JSON.stringify({ yes: false })); if (!HashedItemStore.PERSISTED_INDEX_KEY) { // This is very brittle and depends upon HashedItemStore implementation details, // so let's protect ourselves from accidentally breaking this test. throw new Error('Missing HashedItemStore.PERSISTED_INDEX_KEY'); } - expect(mockSessionStorage.getItem(HashedItemStore.PERSISTED_INDEX_KEY)).toBeTruthy(); - expect(mockSessionStorage.length).toBe(3); + expect(mockStorage.getItem(HashedItemStore.PERSISTED_INDEX_KEY)).toBeTruthy(); + expect(mockStorage.length).toBe(3); }); it('hashes only whitelisted properties', () => { @@ -136,14 +137,14 @@ describe('hash unhash url', () => { `"https://localhost:5601/app/kibana#/discover?foo=bar&_g=h@4e60e02&_a=h@61fa078&_someother=(yes:!f)"` ); - expect(mockSessionStorage.length).toBe(3); // 2 hashes + HashedItemStoreSingleton.PERSISTED_INDEX_KEY + expect(mockStorage.length).toBe(3); // 2 hashes + HashedItemStoreSingleton.PERSISTED_INDEX_KEY }); }); it('throws error if unable to hash url', () => { const stateParamKey1 = '_g'; const stateParamValue1 = '(yes:!t)'; - mockSessionStorage.setStubbedSizeLimit(1); + mockStorage.setStubbedSizeLimit(1); const url = `https://localhost:5601/app/kibana#/discover?foo=bar&${stateParamKey1}=${stateParamValue1}`; expect(() => hashUrl(url)).toThrowError(); @@ -206,7 +207,7 @@ describe('hash unhash url', () => { const stateParamKey = '_g'; const stateParamValueHashed = 'h@4e60e02'; const state = { yes: true }; - mockSessionStorage.setItem(stateParamValueHashed, JSON.stringify(state)); + mockStorage.setItem(stateParamValueHashed, JSON.stringify(state)); const url = `https://localhost:5601/app/kibana#/discover?foo=bar&${stateParamKey}=${stateParamValueHashed}`; const result = unhashUrl(url); @@ -224,8 +225,8 @@ describe('hash unhash url', () => { const stateParamValueHashed2 = 'h@61fa078'; const state2 = { yes: false }; - mockSessionStorage.setItem(stateParamValueHashed1, JSON.stringify(state1)); - mockSessionStorage.setItem(stateParamValueHashed2, JSON.stringify(state2)); + mockStorage.setItem(stateParamValueHashed1, JSON.stringify(state1)); + mockStorage.setItem(stateParamValueHashed2, JSON.stringify(state2)); const url = `https://localhost:5601/app/kibana#/discover?foo=bar&${stateParamKey1}=${stateParamValueHashed1}&${stateParamKey2}=${stateParamValueHashed2}`; const result = unhashUrl(url); @@ -247,9 +248,9 @@ describe('hash unhash url', () => { const stateParamValueHashed3 = 'h@61fa078'; const state3 = { yes: false }; - mockSessionStorage.setItem(stateParamValueHashed1, JSON.stringify(state1)); - mockSessionStorage.setItem(stateParamValueHashed2, JSON.stringify(state2)); - mockSessionStorage.setItem(stateParamValueHashed3, JSON.stringify(state3)); + mockStorage.setItem(stateParamValueHashed1, JSON.stringify(state1)); + mockStorage.setItem(stateParamValueHashed2, JSON.stringify(state2)); + mockStorage.setItem(stateParamValueHashed3, JSON.stringify(state3)); const url = `https://localhost:5601/app/kibana#/discover?foo=bar&${stateParamKey1}=${stateParamValueHashed1}&${stateParamKey2}=${stateParamValueHashed2}&${stateParamKey3}=${stateParamValueHashed3}`; const result = unhashUrl(url); diff --git a/src/legacy/ui/public/state_management/state_hashing/hash_unhash_url.ts b/src/legacy/ui/public/state_management/state_hashing/hash_unhash_url.ts index 345924b92467d..7142683c25115 100644 --- a/src/legacy/ui/public/state_management/state_hashing/hash_unhash_url.ts +++ b/src/legacy/ui/public/state_management/state_hashing/hash_unhash_url.ts @@ -22,7 +22,7 @@ import rison, { RisonObject } from 'rison-node'; import { stringify as stringifyQueryString } from 'querystring'; import encodeUriQuery from 'encode-uri-query'; import { format as formatUrl, parse as parseUrl } from 'url'; -import { HashedItemStoreSingleton } from '../state_storage'; +import { hashedItemStore } from '../../../../../plugins/kibana_utils/public'; import { createStateHash, isStateHash } from './state_hash'; export type IParsedUrlQuery = Record; @@ -95,7 +95,7 @@ function createQueryReplacer( // src/legacy/ui/public/state_management/state_storage/hashed_item_store.ts // maybe to become simplified stateless version export function retrieveState(stateHash: string): RisonObject { - const json = HashedItemStoreSingleton.getItem(stateHash); + const json = hashedItemStore.getItem(stateHash); const throwUnableToRestoreUrlError = () => { throw new Error( i18n.translate('common.ui.stateManagement.unableToRestoreUrlErrorMessage', { @@ -121,7 +121,7 @@ export function persistState(state: RisonObject): string { const json = JSON.stringify(state); const hash = createStateHash(json); - const isItemSet = HashedItemStoreSingleton.setItem(hash, json); + const isItemSet = hashedItemStore.setItem(hash, json); if (isItemSet) return hash; // If we ran out of space trying to persist the state, notify the user. const message = i18n.translate( diff --git a/src/legacy/ui/public/state_management/state_hashing/state_hash.test.ts b/src/legacy/ui/public/state_management/state_hashing/state_hash.test.ts index 5cbf0c8fdcfc5..83a94e37785c4 100644 --- a/src/legacy/ui/public/state_management/state_hashing/state_hash.test.ts +++ b/src/legacy/ui/public/state_management/state_hashing/state_hash.test.ts @@ -18,12 +18,13 @@ */ import { encode as encodeRison } from 'rison-node'; -import { mockSessionStorage } from '../state_storage/mock'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { mockStorage } from '../../../../../plugins/kibana_utils/public/storage/hashed_item_store/mock'; import { createStateHash, isStateHash } from '../state_hashing'; describe('stateHash', () => { beforeEach(() => { - mockSessionStorage.clear(); + mockStorage.clear(); }); describe('#createStateHash', () => { diff --git a/src/legacy/ui/public/state_management/state_hashing/state_hash.ts b/src/legacy/ui/public/state_management/state_hashing/state_hash.ts index a7a75eec63ccb..b3574876bafae 100644 --- a/src/legacy/ui/public/state_management/state_hashing/state_hash.ts +++ b/src/legacy/ui/public/state_management/state_hashing/state_hash.ts @@ -18,7 +18,7 @@ */ import { Sha256 } from '../../../../../core/public/utils'; -import { HashedItemStoreSingleton } from '../state_storage'; +import { hashedItemStore } from '../../../../../plugins/kibana_utils/public'; // This prefix is used to identify hash strings that have been encoded in the URL. const HASH_PREFIX = 'h@'; @@ -42,7 +42,7 @@ export function createStateHash( shortenedHash = hash.slice(0, i); const existingJson = existingJsonProvider ? existingJsonProvider(shortenedHash) - : HashedItemStoreSingleton.getItem(shortenedHash); + : hashedItemStore.getItem(shortenedHash); if (existingJson === null || existingJson === json) break; } diff --git a/src/legacy/ui/public/state_management/state_storage/hashed_item_store_singleton.ts b/src/legacy/ui/public/state_management/state_storage/hashed_item_store_singleton.ts deleted file mode 100644 index 234559c95ebf7..0000000000000 --- a/src/legacy/ui/public/state_management/state_storage/hashed_item_store_singleton.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { HashedItemStore } from './hashed_item_store'; - -export const HashedItemStoreSingleton = new HashedItemStore(window.sessionStorage); diff --git a/src/plugins/kibana_utils/public/index.ts b/src/plugins/kibana_utils/public/index.ts index 04845c72cb755..22ac720246d4b 100644 --- a/src/plugins/kibana_utils/public/index.ts +++ b/src/plugins/kibana_utils/public/index.ts @@ -27,3 +27,4 @@ export * from './store'; export * from './errors'; export * from './field_mapping'; export * from './storage'; +export * from './storage/hashed_item_store'; diff --git a/src/legacy/ui/public/state_management/state_storage/hashed_item_store.test.ts b/src/plugins/kibana_utils/public/storage/hashed_item_store/hashed_item_store.test.ts similarity index 79% rename from src/legacy/ui/public/state_management/state_storage/hashed_item_store.test.ts rename to src/plugins/kibana_utils/public/storage/hashed_item_store/hashed_item_store.test.ts index 3eb4e045f2cf1..f0ff77d516270 100644 --- a/src/legacy/ui/public/state_management/state_storage/hashed_item_store.test.ts +++ b/src/plugins/kibana_utils/public/storage/hashed_item_store/hashed_item_store.test.ts @@ -27,7 +27,8 @@ describe('hashedItemStore', () => { const sessionStorage = new StubBrowserStorage(); const spy = jest.spyOn(sessionStorage, 'getItem'); - new HashedItemStore(sessionStorage); + const hashedItemStore = new HashedItemStore(sessionStorage); + (hashedItemStore as any).getIndexedItems(); // trigger retrieving of indexedItems array from HashedItemStore.PERSISTED_INDEX_KEY expect(spy).toBeCalledWith(HashedItemStore.PERSISTED_INDEX_KEY); spy.mockReset(); }); @@ -54,7 +55,7 @@ describe('hashedItemStore', () => { sessionStorage.setItem(HashedItemStore.PERSISTED_INDEX_KEY, JSON.stringify({ a, b, c })); const hashedItemStore = new HashedItemStore(sessionStorage); - expect((hashedItemStore as any).indexedItems).toEqual([a, c, b]); + expect((hashedItemStore as any).getIndexedItems()).toEqual([a, c, b]); }); }); @@ -260,6 +261,86 @@ describe('hashedItemStore', () => { }); }); }); + + describe('#removeItem', () => { + describe('if the item exists in sessionStorage', () => { + let sessionStorage: Storage; + let hashedItemStore: HashedItemStore; + + beforeEach(() => { + sessionStorage = new StubBrowserStorage(); + hashedItemStore = new HashedItemStore(sessionStorage); + hashedItemStore.setItem('1', 'a'); + hashedItemStore.setItem('2', 'b'); + }); + + it('removes and returns an item', () => { + const removedItem1 = hashedItemStore.removeItem('1'); + expect(removedItem1).toBe('a'); + expect(hashedItemStore.getItem('1')).toBeNull(); + expect(hashedItemStore.getItem('2')).not.toBeNull(); + expect((hashedItemStore as any).getIndexedItems()).toHaveLength(1); + + const removedItem2 = hashedItemStore.removeItem('2'); + expect(removedItem2).toBe('b'); + expect(hashedItemStore.getItem('1')).toBeNull(); + expect(hashedItemStore.getItem('2')).toBeNull(); + expect((hashedItemStore as any).getIndexedItems()).toHaveLength(0); + }); + }); + + describe(`if the item doesn't exist in sessionStorage`, () => { + let sessionStorage: Storage; + let hashedItemStore: HashedItemStore; + const hash = 'a'; + + beforeEach(() => { + sessionStorage = new StubBrowserStorage(); + hashedItemStore = new HashedItemStore(sessionStorage); + }); + + it('returns null', () => { + const removedItem = hashedItemStore.removeItem(hash); + expect(removedItem).toBe(null); + }); + }); + }); + + describe('#clear', () => { + describe('if the items exist in sessionStorage', () => { + let sessionStorage: Storage; + let hashedItemStore: HashedItemStore; + + beforeEach(() => { + sessionStorage = new StubBrowserStorage(); + hashedItemStore = new HashedItemStore(sessionStorage); + hashedItemStore.setItem('1', 'a'); + hashedItemStore.setItem('2', 'b'); + }); + + it('removes all items', () => { + hashedItemStore.clear(); + + expect(hashedItemStore.getItem('1')).toBeNull(); + expect(hashedItemStore.getItem('2')).toBeNull(); + expect((hashedItemStore as any).getIndexedItems()).toHaveLength(0); + }); + }); + + describe(`if items don't exist in sessionStorage`, () => { + let sessionStorage: Storage; + let hashedItemStore: HashedItemStore; + + beforeEach(() => { + sessionStorage = new StubBrowserStorage(); + hashedItemStore = new HashedItemStore(sessionStorage); + }); + + it("doesn't throw", () => { + expect(() => hashedItemStore.clear()).not.toThrowError(); + }); + }); + }); }); describe('behavior', () => { diff --git a/src/legacy/ui/public/state_management/state_storage/hashed_item_store.ts b/src/plugins/kibana_utils/public/storage/hashed_item_store/hashed_item_store.ts similarity index 70% rename from src/legacy/ui/public/state_management/state_storage/hashed_item_store.ts rename to src/plugins/kibana_utils/public/storage/hashed_item_store/hashed_item_store.ts index f49d993e15760..485aa643c4f01 100644 --- a/src/legacy/ui/public/state_management/state_storage/hashed_item_store.ts +++ b/src/plugins/kibana_utils/public/storage/hashed_item_store/hashed_item_store.ts @@ -71,34 +71,24 @@ */ import { pull, sortBy } from 'lodash'; +import { IStorage } from '../types'; interface IndexedItem { hash: string; touched?: number; // Date.now() } -export class HashedItemStore { +export class HashedItemStore implements IStorage { static readonly PERSISTED_INDEX_KEY = 'kbn.hashedItemsIndex.v1'; - private sessionStorage: Storage; - - // 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. - private indexedItems: IndexedItem[] = []; + private storage: Storage; /** * 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 + * in storage. An indexed item is shaped {hash, touched}. The touched date is when the item * was last referenced by the browser history. */ - constructor(sessionStorage: Storage) { - this.sessionStorage = sessionStorage; - - // 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'); - } + constructor(storage: Storage) { + this.storage = storage; } setItem(hash: string, item: string): boolean { @@ -112,7 +102,7 @@ export class HashedItemStore { } getItem(hash: string): string | null { - const item = this.sessionStorage.getItem(hash); + const item = this.storage.getItem(hash); if (item !== null) { this.touchHash(hash); @@ -121,17 +111,64 @@ export class HashedItemStore { return item; } - private getIndexedItem(hash: string) { - return this.indexedItems.find(indexedItem => indexedItem.hash === hash); + removeItem(hash: string): string | null { + const indexedItems = this.getIndexedItems(); + const itemToRemove = this.storage.getItem(hash); + const indexedItemToRemove = this.getIndexedItem(hash, indexedItems); + + if (indexedItemToRemove) { + pull(indexedItems, indexedItemToRemove); + this.setIndexedItems(indexedItems); + } + + if (itemToRemove) { + this.storage.removeItem(hash); + } + + return itemToRemove || null; + } + + clear() { + const indexedItems = this.getIndexedItems(); + indexedItems.forEach(({ hash }) => { + this.storage.removeItem(hash); + }); + this.setIndexedItems([]); + } + + // 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. + private ensuredSorting = false; + private getIndexedItems(): IndexedItem[] { + // Restore a previously persisted index + const persistedItemIndex = this.storage.getItem(HashedItemStore.PERSISTED_INDEX_KEY); + let items = persistedItemIndex ? JSON.parse(persistedItemIndex) || [] : []; + + // ensure sorting once, as sorting all indexed items on each get is a performance hit + if (!this.ensuredSorting) { + items = sortBy(items, 'touched'); + this.setIndexedItems(items); + this.ensuredSorting = true; + } + + return items; + } + + private setIndexedItems(items: IndexedItem[]) { + this.storage.setItem(HashedItemStore.PERSISTED_INDEX_KEY, JSON.stringify(items)); + } + + private getIndexedItem(hash: string, indexedItems: IndexedItem[] = this.getIndexedItems()) { + return indexedItems.find(indexedItem => indexedItem.hash === hash); } private persistItem(hash: string, item: string): boolean { try { - this.sessionStorage.setItem(hash, item); + this.storage.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 (this.getIndexedItems().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; @@ -147,31 +184,31 @@ export class HashedItemStore { } private removeOldestItem() { - const oldestIndexedItem = this.indexedItems.shift(); + const indexedItems = this.getIndexedItems(); + const oldestIndexedItem = indexedItems.shift(); if (oldestIndexedItem) { // Remove oldest item from storage. - this.sessionStorage.removeItem(oldestIndexedItem.hash); + this.storage.removeItem(oldestIndexedItem.hash); + this.setIndexedItems(indexedItems); } } private touchHash(hash: string) { + const indexedItems = this.getIndexedItems(); // 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 }; + const indexedItem = this.getIndexedItem(hash, indexedItems) || { 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); + pull(indexedItems, indexedItem); + 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) - ); + this.setIndexedItems(indexedItems); } } diff --git a/src/legacy/ui/public/state_management/state_storage/index.ts b/src/plugins/kibana_utils/public/storage/hashed_item_store/index.ts similarity index 83% rename from src/legacy/ui/public/state_management/state_storage/index.ts rename to src/plugins/kibana_utils/public/storage/hashed_item_store/index.ts index 8316e10fb8062..062266359c6c5 100644 --- a/src/legacy/ui/public/state_management/state_storage/index.ts +++ b/src/plugins/kibana_utils/public/storage/hashed_item_store/index.ts @@ -17,4 +17,6 @@ * under the License. */ -export { HashedItemStoreSingleton } from './hashed_item_store_singleton'; +import { HashedItemStore } from './hashed_item_store'; +export { HashedItemStore }; +export const hashedItemStore = new HashedItemStore(window.sessionStorage); diff --git a/src/legacy/ui/public/state_management/state_storage/mock.ts b/src/plugins/kibana_utils/public/storage/hashed_item_store/mock.ts similarity index 82% rename from src/legacy/ui/public/state_management/state_storage/mock.ts rename to src/plugins/kibana_utils/public/storage/hashed_item_store/mock.ts index 59700cb369e6e..e3360e0e3cf51 100644 --- a/src/legacy/ui/public/state_management/state_storage/mock.ts +++ b/src/plugins/kibana_utils/public/storage/hashed_item_store/mock.ts @@ -28,10 +28,11 @@ import { HashedItemStore } from './hashed_item_store'; * And all tests in the test file will use HashedItemStoreSingleton * with underlying mockSessionStorage we have access to */ -export const mockSessionStorage = new StubBrowserStorage(); -const mockHashedItemStore = new HashedItemStore(mockSessionStorage); -jest.mock('../state_storage', () => { +export const mockStorage = new StubBrowserStorage(); +const mockHashedItemStore = new HashedItemStore(mockStorage); +jest.mock('./', () => { return { - HashedItemStoreSingleton: mockHashedItemStore, + HashedItemStore: require('./hashed_item_store').HashedItemStore, + hashedItemStore: mockHashedItemStore, }; }); diff --git a/src/plugins/kibana_utils/public/storage/types.ts b/src/plugins/kibana_utils/public/storage/types.ts index 875bb44bcad17..a25d4729fd320 100644 --- a/src/plugins/kibana_utils/public/storage/types.ts +++ b/src/plugins/kibana_utils/public/storage/types.ts @@ -17,16 +17,16 @@ * under the License. */ -export interface IStorageWrapper { - get: (key: string) => any; - set: (key: string, value: any) => void; - remove: (key: string) => any; +export interface IStorageWrapper { + get: (key: string) => T | null; + set: (key: string, value: T) => S; + remove: (key: string) => T | null; clear: () => void; } -export interface IStorage { - getItem: (key: string) => any; - setItem: (key: string, value: any) => void; - removeItem: (key: string) => any; +export interface IStorage { + getItem: (key: string) => T | null; + setItem: (key: string, value: T) => S; + removeItem: (key: string) => T | null; clear: () => void; }