Skip to content

Commit

Permalink
[State Management] Move HashedItemStore to kibana_utils plugin. Make …
Browse files Browse the repository at this point in the history
…it stateless on memory level. (#52172) (#52271)

HashedItemStore was also moved to the kibana_utils plugin
  • Loading branch information
Dosant authored Dec 5, 2019
1 parent d85f100 commit 8695f6a
Show file tree
Hide file tree
Showing 14 changed files with 198 additions and 100 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
2 changes: 1 addition & 1 deletion src/legacy/ui/public/state_management/__tests__/state.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down
9 changes: 3 additions & 6 deletions src/legacy/ui/public/state_management/state.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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, [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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', () => {
Expand All @@ -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', () => {
Expand All @@ -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();
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, any>;
Expand Down Expand Up @@ -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', {
Expand All @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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@';
Expand All @@ -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;
}

Expand Down

This file was deleted.

1 change: 1 addition & 0 deletions src/plugins/kibana_utils/public/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,4 @@ export * from './store';
export * from './errors';
export * from './field_mapping';
export * from './storage';
export * from './storage/hashed_item_store';
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
Expand All @@ -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]);
});
});

Expand Down Expand Up @@ -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', () => {
Expand Down
Loading

0 comments on commit 8695f6a

Please sign in to comment.