Rename HashingStore interface methods for consistency and clarity. Remove unused '#remove' method.

This commit is contained in:
CJ Cenizal 2016-08-23 16:49:30 -07:00 committed by spalger
parent ed12206f01
commit b51b5cb3fe
6 changed files with 64 additions and 84 deletions

View file

@ -13,7 +13,7 @@ import StubBrowserStorage from 'test_utils/stub_browser_storage';
import EventsProvider from 'ui/events';
describe('State Management', function () {
const notify = new Notifier();
const notifier = new Notifier();
let $rootScope;
let $location;
let State;
@ -33,13 +33,13 @@ describe('State Management', function () {
sinon.stub(config, 'get').withArgs('state:storeInSessionStorage').returns(!!storeInHash);
const store = new StubBrowserStorage();
const hashingStore = new HashingStore({ store });
const state = new State(param, initial, { hashingStore, notify });
const state = new State(param, initial, { hashingStore, notifier });
const getUnhashedSearch = state => {
return unhashQueryString($location.search(), [ state ]);
};
return { notify, store, hashingStore, state, getUnhashedSearch };
return { notifier, store, hashingStore, state, getUnhashedSearch };
};
}));
@ -185,7 +185,6 @@ describe('State Management', function () {
});
});
describe('Hashing', () => {
it('stores state values in a hashingStore, writing the hash to the url', () => {
const { state, hashingStore } = setup({ storeInHash: true });
@ -194,7 +193,7 @@ describe('State Management', function () {
const urlVal = $location.search()[state.getQueryParamName()];
expect(hashingStore.isHash(urlVal)).to.be(true);
expect(hashingStore.lookup(urlVal)).to.eql({ foo: 'bar' });
expect(hashingStore.getItemAtHash(urlVal)).to.eql({ foo: 'bar' });
});
it('should replace rison in the URL with a hash', () => {
@ -208,29 +207,28 @@ describe('State Management', function () {
const urlVal = $location.search()._s;
expect(urlVal).to.not.be(rison);
expect(hashingStore.isHash(urlVal)).to.be(true);
expect(hashingStore.lookup(urlVal)).to.eql(obj);
expect(hashingStore.getItemAtHash(urlVal)).to.eql(obj);
});
context('error handling', () => {
it('notifies the user when a hash value does not map to a stored value', () => {
const { state, hashingStore, notify } = setup({ storeInHash: true });
const { state, hashingStore, notifier } = setup({ storeInHash: true });
const search = $location.search();
const badHash = hashingStore.add({});
hashingStore.remove(badHash);
const badHash = hashingStore._getShortHash('{"a": "b"}');
search[state.getQueryParamName()] = badHash;
$location.search(search);
expect(notify._notifs).to.have.length(0);
expect(notifier._notifs).to.have.length(0);
state.fetch();
expect(notify._notifs).to.have.length(1);
expect(notify._notifs[0].content).to.match(/use the share functionality/i);
expect(notifier._notifs).to.have.length(1);
expect(notifier._notifs[0].content).to.match(/use the share functionality/i);
});
it('presents fatal error linking to github when hashingStore.add fails', () => {
const { state, hashingStore, notify } = setup({ storeInHash: true });
const fatalStub = sinon.stub(notify, 'fatal').throws();
sinon.stub(hashingStore, 'add').throws();
it('presents fatal error linking to github when hashingStore.hashAndSetItem fails', () => {
const { state, hashingStore, notifier } = setup({ storeInHash: true });
const fatalStub = sinon.stub(notifier, 'fatal').throws();
sinon.stub(hashingStore, 'hashAndSetItem').throws();
expect(() => {
state.toQueryParam();

View file

@ -18,14 +18,14 @@ export default function StateProvider(Private, $rootScope, $location, config) {
const Events = Private(EventsProvider);
_.class(State).inherits(Events);
function State(urlParam, defaults, { hashingStore, notify } = {}) {
function State(urlParam, defaults, { hashingStore, notifier } = {}) {
State.Super.call(this);
let self = this;
self.setDefaults(defaults);
self._urlParam = urlParam || '_s';
this._notify = notify || new Notifier();
self._hasher = hashingStore || new HashingStore({
this._notifier = notifier || new Notifier();
self._hashingStore = hashingStore || new HashingStore({
store: new LazyLruStore({
id: `${this._urlParam}:state`,
store: window.sessionStorage,
@ -67,7 +67,7 @@ export default function StateProvider(Private, $rootScope, $location, config) {
return null;
}
if (this._hasher.isHash(urlVal)) {
if (this._hashingStore.isHash(urlVal)) {
return this._parseQueryParamValue(urlVal);
}
@ -80,7 +80,7 @@ export default function StateProvider(Private, $rootScope, $location, config) {
}
if (unableToParse) {
this._notify.error('Unable to parse URL');
this._notifier.error('Unable to parse URL');
search[this._urlParam] = this.toQueryParam(this._defaults);
$location.search(search).replace();
}
@ -194,13 +194,13 @@ export default function StateProvider(Private, $rootScope, $location, config) {
* @return {any} - the stored value, or null if hash does not resolve
*/
State.prototype._parseQueryParamValue = function (queryParam) {
if (!this._hasher.isHash(queryParam)) {
if (!this._hashingStore.isHash(queryParam)) {
return rison.decode(queryParam);
}
const stored = this._hasher.lookup(queryParam);
const stored = this._hashingStore.getItemAtHash(queryParam);
if (stored === null) {
this._notify.error('Unable to completely restore the URL, be sure to use the share functionality.');
this._notifier.error('Unable to completely restore the URL, be sure to use the share functionality.');
}
return stored;
@ -228,10 +228,11 @@ export default function StateProvider(Private, $rootScope, $location, config) {
}
try {
return this._hasher.add(state);
const hash = this._hashingStore.hashAndSetItem(state);
return hash;
} catch (err) {
this._notify.log('Unable to create hash of State due to error: ' + (state.stack || state.message));
this._notify.fatal(
this._notifier.log('Unable to create hash of State due to error: ' + (state.stack || state.message));
this._notifier.fatal(
new Error(
'Kibana is unable to store history items in your session ' +
'because it is full and there don\'t seem to be items any items safe ' +

View file

@ -11,12 +11,12 @@ const setup = ({ createHash } = {}) => {
return { store, hashingStore };
};
describe('State Management Hashing Store', () => {
describe('#add', () => {
describe('Hashing Store', () => {
describe('#hashAndSetItem', () => {
it('adds a value to the store and returns its hash', () => {
const { hashingStore, store } = setup();
const val = { foo: 'bar' };
const hash = hashingStore.add(val);
const hash = hashingStore.hashAndSetItem(val);
expect(hash).to.be.a('string');
expect(hash).to.be.ok();
expect(store).to.have.length(1);
@ -25,8 +25,8 @@ describe('State Management Hashing Store', () => {
it('json encodes the values it stores', () => {
const { hashingStore, store } = setup();
const val = { toJSON() { return 1; } };
const hash = hashingStore.add(val);
expect(hashingStore.lookup(hash)).to.eql(1);
const hash = hashingStore.hashAndSetItem(val);
expect(hashingStore.getItemAtHash(hash)).to.eql(1);
});
it('addresses values with a short hash', () => {
@ -36,7 +36,7 @@ describe('State Management Hashing Store', () => {
createHash: () => longHash
});
const hash = hashingStore.add(val);
const hash = hashingStore.hashAndSetItem(val);
expect(hash.length < longHash.length).to.be.ok();
});
@ -64,48 +64,37 @@ describe('State Management Hashing Store', () => {
}
});
const hash1 = hashingStore.add(fixtures[0].val);
const hash2 = hashingStore.add(fixtures[1].val);
const hash3 = hashingStore.add(fixtures[2].val);
const hash1 = hashingStore.hashAndSetItem(fixtures[0].val);
const hash2 = hashingStore.hashAndSetItem(fixtures[1].val);
const hash3 = hashingStore.hashAndSetItem(fixtures[2].val);
expect(hash3).to.have.length(hash2.length + 1);
expect(hash2).to.have.length(hash1.length + 1);
});
it('bubbles up the error if the store fails to setItem', () => {
it('bubbles up the error if the store fails to hashAndSetItem', () => {
const { store, hashingStore } = setup();
const err = new Error();
sinon.stub(store, 'setItem').throws(err);
expect(() => {
hashingStore.add({});
hashingStore.hashAndSetItem({});
}).to.throwError(e => expect(e).to.be(err));
});
});
describe('#lookup', () => {
describe('#getItemAtHash', () => {
it('reads a value from the store by its hash', () => {
const { hashingStore } = setup();
const val = { foo: 'bar' };
const hash = hashingStore.add(val);
expect(hashingStore.lookup(hash)).to.eql(val);
const hash = hashingStore.hashAndSetItem(val);
expect(hashingStore.getItemAtHash(hash)).to.eql(val);
});
it('returns null when the value is not in the store', () => {
const { hashingStore } = setup();
const val = { foo: 'bar' };
const hash = hashingStore.add(val);
expect(hashingStore.lookup(`${hash} break`)).to.be(null);
});
});
describe('#remove', () => {
it('removes the value by its hash', () => {
const { hashingStore } = setup();
const val = { foo: 'bar' };
const hash = hashingStore.add(val);
expect(hashingStore.lookup(hash)).to.eql(val);
hashingStore.remove(hash);
expect(hashingStore.lookup(hash)).to.be(null);
const hash = hashingStore.hashAndSetItem(val);
expect(hashingStore.getItemAtHash(`${hash} break`)).to.be(null);
});
});
@ -113,7 +102,7 @@ describe('State Management Hashing Store', () => {
it('can identify values that look like hashes', () => {
const { hashingStore } = setup();
const val = { foo: 'bar' };
const hash = hashingStore.add(val);
const hash = hashingStore.hashAndSetItem(val);
expect(hashingStore.isHash(hash)).to.be(true);
});

View file

@ -27,7 +27,7 @@ const setup = (opts = {}) => {
return { lru, store };
};
describe('State Management LazyLruStore', () => {
describe('LazyLruStore', () => {
describe('#getItem()', () => {
it('returns null when item not found', () => {
const { lru } = setup();

View file

@ -5,15 +5,13 @@ import { Sha256 } from 'ui/crypto';
import StubBrowserStorage from 'test_utils/stub_browser_storage';
import { LazyLruStore } from './lazy_lru_store';
const TAG = 'h@';
/**
* The HashingStore is a wrapper around a browser store object
* that hashes the items added to it and stores them by their
* hash. This hash is then returned so that the item can be received
* at a later time.
*/
export default class HashingStore {
class HashingStore {
constructor({ store, createHash, maxItems } = {}) {
this._store = store || window.sessionStorage;
if (createHash) this._createHash = createHash;
@ -22,11 +20,11 @@ export default class HashingStore {
/**
* Determine if the passed value looks like a hash
*
* @param {string} hash
* @param {string} str
* @return {boolean}
*/
isHash(hash) {
return String(hash).slice(0, TAG.length) === TAG;
isHash(str) {
return String(str).indexOf(HashingStore.HASH_TAG) === 0;
}
/**
@ -35,7 +33,7 @@ export default class HashingStore {
* @param {string} hash
* @return {any}
*/
lookup(hash) {
getItemAtHash(hash) {
try {
return JSON.parse(this._store.getItem(hash));
} catch (err) {
@ -50,23 +48,13 @@ export default class HashingStore {
* @param {any} the value to hash
* @return {string} the hash of the value
*/
add(object) {
hashAndSetItem(object) {
const json = angular.toJson(object);
const hash = this._getShortHash(json);
this._store.setItem(hash, json);
return hash;
}
/**
* Remove a value identified by the hash from the store
*
* @param {string} hash
* @return {undefined}
*/
remove(hash) {
this._store.removeItem(hash);
}
// private api
/**
@ -89,7 +77,7 @@ export default class HashingStore {
* @param {string} shortHash
*/
_getShortHash(json) {
const fullHash = `${TAG}${this._createHash(json)}`;
const fullHash = `${HashingStore.HASH_TAG}${this._createHash(json)}`;
let short;
for (let i = 7; i < fullHash.length; i++) {
@ -101,3 +89,7 @@ export default class HashingStore {
return short;
}
}
HashingStore.HASH_TAG = 'h@';
export default HashingStore;

View file

@ -36,7 +36,7 @@ export default class LazyLruStore {
const {
id,
store,
notify = new Notifier(`LazyLruStore (re: probably history hashing)`),
notifier = new Notifier(`LazyLruStore (re: probably history hashing)`),
maxItems = Infinity,
maxSetAttempts = DEFAULT_MAX_SET_ATTEMPTS,
idealClearRatio = DEFAULT_IDEAL_CLEAR_RATIO,
@ -54,7 +54,7 @@ export default class LazyLruStore {
this._id = id;
this._prefix = `lru:${this._id}:`;
this._store = store;
this._notify = notify;
this._notifier = notifier;
this._maxItems = maxItems;
this._itemCountGuess = this._getItemCount();
this._maxSetAttempts = maxSetAttempts;
@ -135,7 +135,7 @@ export default class LazyLruStore {
* then this function will call itself again, this time sending
* attempt + 1 as the attempt number. If this loop continues
* and attempt meets or exceeds the this._maxSetAttempts then a fatal
* error will be sent to notify, as the users session is no longer
* error will be sent to notifier, as the users session is no longer
* usable.
*
* @private
@ -173,7 +173,7 @@ export default class LazyLruStore {
*/
_indexStoredItems() {
const store = this._store;
const notify = this._notify;
const notifier = this._notifier;
const items = [];
let totalBytes = 0;
@ -228,8 +228,8 @@ export default class LazyLruStore {
* @return {boolean} success
*/
_makeSpaceFor(key, chunk) {
const notify = this._notify;
return notify.event(`trying to make room in lru ${this._id}`, () => {
const notifier = this._notifier;
return notifier.event(`trying to make room in lru ${this._id}`, () => {
const { totalBytes, itemsByOldestAccess } = this._indexStoredItems();
// pick how much space we are going to try to clear
@ -238,7 +238,7 @@ export default class LazyLruStore {
const freeMin = key.length + chunk.length;
const freeIdeal = freeMin * this._idealClearRatio;
const toClear = Math.max(freeMin, Math.min(freeIdeal, totalBytes * this._maxIdealClearPercent));
notify.log(`PLAN: min ${freeMin} bytes, target ${toClear} bytes`);
notifier.log(`PLAN: min ${freeMin} bytes, target ${toClear} bytes`);
let remainingToClear = toClear;
let removedItemCount = 0;
@ -253,7 +253,7 @@ export default class LazyLruStore {
const label = success ? 'SUCCESS' : 'FAILURE';
const removedBytes = toClear - remainingToClear;
notify.log(`${label}: removed ${removedItemCount} items for ${removedBytes} bytes`);
notifier.log(`${label}: removed ${removedItemCount} items for ${removedBytes} bytes`);
return success;
});
}
@ -269,7 +269,7 @@ export default class LazyLruStore {
*/
_doItemAutoRemoval(item) {
const timeString = new Date(item.time).toISOString();
this._notify.log(`REMOVE: entry "${item.key}" from ${timeString}, freeing ${item.bytes} bytes`);
this._notifier.log(`REMOVE: entry "${item.key}" from ${timeString}, freeing ${item.bytes} bytes`);
this._store.removeItem(item.key);
this._itemCountGuess -= 1;
}