Mike Gerwitz

Activist for User Freedom

aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMike Gerwitz <gerwitzm@lovullo.com>2017-01-06 13:08:40 -0500
committerMike Gerwitz <gerwitzm@lovullo.com>2017-01-06 13:08:40 -0500
commitb83cc40dc598a0cf1738c9cac5fc7bc073023794 (patch)
tree16dd42455dbe1b1f2f5d12225c9b6a8e00b1ce7f
parent6dd5b83a271dea1917d2e59c2c49d58e785aa9a8 (diff)
parentbb17085448401045d9626c4a742f997ddde5eee5 (diff)
downloadliza-b83cc40dc598a0cf1738c9cac5fc7bc073023794.tar.gz
liza-b83cc40dc598a0cf1738c9cac5fc7bc073023794.tar.bz2
liza-b83cc40dc598a0cf1738c9cac5fc7bc073023794.zip
Add store
-rw-r--r--package.json.in9
-rw-r--r--src/store/Cascading.js117
-rw-r--r--src/store/MemoryStore.js167
-rw-r--r--src/store/MissLookup.js163
-rw-r--r--src/store/Store.js91
-rw-r--r--src/store/StoreMissError.js31
-rw-r--r--test/store/CascadingTest.js138
-rw-r--r--test/store/MemoryStoreTest.js200
-rw-r--r--test/store/MissLookupTest.js163
9 files changed, 1075 insertions, 4 deletions
diff --git a/package.json.in b/package.json.in
index def2f71..5959465 100644
--- a/package.json.in
+++ b/package.json.in
@@ -19,10 +19,11 @@
"easejs": "0.2.x"
},
"devDependencies": {
- "chai": ">=1.9.1",
- "mocha": ">=1.18.2",
- "sinon": ">=1.17.4",
- "es6-promise": "~3"
+ "chai": ">=1.9.1",
+ "chai-as-promised": ">=6.0.0",
+ "mocha": ">=1.18.2",
+ "sinon": ">=1.17.4",
+ "es6-promise": "~3"
},
"licenses": [
diff --git a/src/store/Cascading.js b/src/store/Cascading.js
new file mode 100644
index 0000000..5e6c55d
--- /dev/null
+++ b/src/store/Cascading.js
@@ -0,0 +1,117 @@
+/**
+ * Recurisvely-clearing key/value store
+ *
+ * Copyright (C) 2016 LoVullo Associates, Inc.
+ *
+ * This file is part of the Liza Data Collection Framework
+ *
+ * Liza is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+var Trait = require( 'easejs' ).Trait,
+ Class = require( 'easejs' ).Class,
+ Store = require( './Store' );
+
+
+/**
+ * Store of stores with cascading clear
+ *
+ * The store `S` into which this trait is mixed will accept only other
+ * Store objects which will each have their `#clear` method called
+ * when `#clear` is invoked on `S`.
+ *
+ * @example
+ * let store_a = Store(),
+ * store_b = Store();
+ *
+ * // assuming sync. store for example (ignore promises)
+ * store_a.add( 'key', 'value' );
+ * store_b.add( 'foo', 'bar' );
+ *
+ * let container = Store.use( Cascading );
+ * container.add( 'a', store_a );
+ * container.add( 'b', store_b );
+ * container.clear();
+ *
+ * store_a.get( 'key' ); // Promise rejects
+ * store_b.get( 'foo' ); // Promise rejects
+ *
+ * Store.use( Cascading ).add( 'invalid', 'value' );
+ * // rejected with TypeError: Can only add Store to Cascading stores
+ *
+ * Although clear cascades to each `Store`, other methods do not (for
+ * example, `get` will not query all `Store`s); another trait should
+ * be added for such a thing. This behavior allows for, effectively,
+ * namespacing.
+ *
+ * This trait can be used, for example, to implement a centralized
+ * caching system that can trigger a reload of objects realtime
+ * system-wide, allowing for transparent hot code swapping (assuming
+ * that the caller will re-store).
+ */
+module.exports = Trait( 'Cascading' )
+ .implement( Store )
+ .extend(
+{
+ /**
+ * Add item to store under `key` with value `value`
+ *
+ * Only [`Store`]{@link module:store.Store} objects may be attached.
+ *
+ * @param {string} key store key
+ * @param {Store} value Store to attach
+ *
+ * @return {Promise} promise to add item to store
+ */
+ 'virtual abstract override public add': function( key, value )
+ {
+ if ( !Class.isA( Store, value ) )
+ {
+ return Promise.reject(
+ TypeError( "Can only add Store to Cascading stores" )
+ );
+ }
+
+ return this.__super( key, value );
+ },
+
+
+ /**
+ * Clear all stores in the store
+ *
+ * @return {Promise} promise to clear all caches
+ */
+ 'virtual abstract override public clear': function()
+ {
+ return this.reduce(
+ function( accum, store )
+ {
+ accum.push( store.clear() );
+ return accum;
+ },
+ []
+ )
+ .then( function( promises )
+ {
+ return Promise.all( promises );
+ } )
+ .then( function( result )
+ {
+ return result.every( function( value )
+ {
+ return value === true;
+ } );
+ } );
+ },
+} );
diff --git a/src/store/MemoryStore.js b/src/store/MemoryStore.js
new file mode 100644
index 0000000..d4b92da
--- /dev/null
+++ b/src/store/MemoryStore.js
@@ -0,0 +1,167 @@
+/**
+ * Generic key/value store in local memory
+ *
+ * Copyright (C) 2016 LoVullo Associates, Inc.
+ *
+ * This file is part of the Liza Data Collection Framework
+ *
+ * Liza is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+var Class = require( 'easejs' ).Class,
+ Store = require( './Store' ),
+ StoreMissError = require( './StoreMissError' );
+
+
+/**
+ * Generic key/value store with bulk clear
+ *
+ * @todo There's a lot of overlap between this concept and that of the
+ * Bucket. Maybe have the Bucket layer atop of simple Store
+ * interface as a step toward a new, simpler Bucket
+ * implementation. This was not implemented atop of the Bucket
+ * interface because its haphazard implementation would
+ * overcomplicate this.
+ *
+ * @example
+ * let s = MemoryStore();
+ *
+ * Promise.all( [
+ * s.add( 'foo', 'bar' ),
+ * s.add( 'baz', 'quux' ),
+ * ] )
+ * .then( Promise.all( [
+ * {
+ * s.get( 'foo' ),
+ * s.get( 'baz' ),
+ * ] ) } )
+ * .then( function( values )
+ * {
+ * // values = [ 'bar', 'quux' ]
+ * } );
+ *
+ * s.clear().then( function()
+ * {
+ * s.get( 'foo' )
+ * .catch( function()
+ * {
+ * // foo is no longer defined
+ * } );
+ * } );
+ */
+module.exports = Class( 'MemoryStore' )
+ .implement( Store )
+ .extend(
+{
+ /**
+ * Key/value store
+ *
+ * @type {Object}
+ */
+ 'private _store': {},
+
+
+ /**
+ * Add item to store under `key` with value `value`
+ *
+ * The promise will be fulfilled with an object containing the
+ * `key` and `value` added to the store; this is convenient for
+ * promises.
+ *
+ * @param {string} key store key
+ * @param {*} value value for key
+ *
+ * @return {Promise} promise to add item to store
+ */
+ 'virtual public add': function( key, value )
+ {
+ this._store[ key ] = value;
+
+ return Promise.resolve( {
+ key: key,
+ value: value,
+ } );
+ },
+
+
+ /**
+ * Retrieve item from store under `key`
+ *
+ * The promise will be rejected if the key is unavailable.
+ *
+ * @param {string} key store key
+ *
+ * @return {Promise} promise for the key value
+ */
+ 'virtual public get': function( key )
+ {
+ return ( this._store[ key ] !== undefined )
+ ? Promise.resolve( this._store[ key ] )
+ : Promise.reject(
+ StoreMissError( "Key '" + key + "' does not exist" )
+ );
+ },
+
+
+ /**
+ * Clear all items in store
+ *
+ * @return {Promise} promise to clear store
+ */
+ 'virtual public clear': function()
+ {
+ this._store = {};
+
+ return Promise.resolve( true );
+ },
+
+
+ /**
+ * Fold (reduce) all stored values
+ *
+ * This provides a way to iterate through all stored values and
+ * their keys while providing a useful functional result (folding).
+ *
+ * The order of folding is undefined.
+ *
+ * The ternary function `callback` is of the same form as
+ * {@link Array#fold}: the first argument is the value of the
+ * accumulator (initialized to the value of `initial`; the second
+ * is the stored item; and the third is the key of that item.
+ *
+ * Warning: if a subtype or mixin has an intensive store lookup
+ * operating, reducing could take some time.
+ *
+ * @param {function(*,*,string=)} callback folding function
+ * @param {*} initial initial value for accumulator
+ *
+ * @return {Promise} promise of a folded value (final accumulator value)
+ */
+ 'public reduce': function( callback, initial )
+ {
+ var store = this._store;
+
+ return Promise.resolve(
+ Object.keys( store )
+ .map( function( key )
+ {
+ return [ key, store[ key ] ];
+ } )
+ .reduce( function( accum, values )
+ {
+ return callback( accum, values[ 1 ], values[ 0 ] );
+ }, initial )
+ );
+ }
+} );
diff --git a/src/store/MissLookup.js b/src/store/MissLookup.js
new file mode 100644
index 0000000..b3f3aea
--- /dev/null
+++ b/src/store/MissLookup.js
@@ -0,0 +1,163 @@
+/**
+ * Auto-lookup for store misses
+ *
+ * Copyright (C) 2016 LoVullo Associates, Inc.
+ *
+ * This file is part of the Liza Data Collection Framework
+ *
+ * Liza is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+var Trait = require( 'easejs' ).Trait,
+ Class = require( 'easejs' ).Class,
+ Store = require( './Store' );
+
+
+/**
+ * Automatically try to look up values on store miss
+ *
+ * A common use case for key/value stores is caching. Unless the
+ * cache is being warmed by another system, it's likely that the
+ * caller will process whatever item is missing and then immediately
+ * add it to the cache. This simplifies that situation by
+ * automatically calling a function on miss and holding the request
+ * until the datum becomes available.
+ *
+ * To guard against stampeding, and to relieve callers from handling
+ * other concurrency issues, all requests for the same key will return
+ * the same promise until that promise is resolved or rejected (see
+ * `#get`).
+ *
+ * @example
+ * function lookup( key )
+ * {
+ * return Promise.resolve( key + ' foobar' );
+ * }
+ *
+ * let store = Store.use( MissLookup( lookup ) );
+ *
+ * store.get( 'unknown' )
+ * .then( function( value )
+ * {
+ * // value 'unknown foobar'
+ * } );
+ *
+ * This trait can also be used purely to prevent stampeding by
+ * providing a miss function that is effectively a noop for a given
+ * situation.
+ */
+module.exports = Trait( 'MissLookup' )
+ .implement( Store )
+ .extend(
+{
+ /**
+ * Store miss key lookup function
+ *
+ * @type {function(string)}
+ */
+ 'private _lookup': null,
+
+ /**
+ * Unresolved promises for misses, by key
+ *
+ * @type {Object}
+ */
+ 'private _misses': {},
+
+
+ /**
+ * Initialize key miss lookup
+ *
+ * The unary miss lookup function `lookup` will be provided with
+ * the key of the store item that missed and should return the
+ * intended value of that item. The resulting item will be stored
+ * and returned. If no item is found, `lookup` should return
+ * `undefined`.
+ *
+ * @param {function(string): Promise} lookup store miss key lookup
+ */
+ __mixin: function( lookup )
+ {
+ if ( typeof lookup !== 'function' )
+ {
+ throw TypeError( 'Lookup function must be a function' );
+ }
+
+ this._lookup = lookup;
+ },
+
+
+ /**
+ * Retrieve item from the store under `key`, attempting lookup on
+ * store miss
+ *
+ * In the event of a miss, the looked up value is added to the
+ * store. This method waits for the add operation to complete
+ * before fulfilling the promise by re-requesting the key from the
+ * supertype (allowing it to do its own thing).
+ *
+ * On the first miss for a given key `K`, a promise `P` is stored.
+ * To prevent stampeding and other awkward concurrency issues,
+ * all further requests for `K` will receive the same promise
+ * `P` until it is resolved.
+ *
+ * A word of caution: if the lookup function does not return a
+ * value, it will continue to be called for each request
+ * thereafter. This might not be a good thing if the lookup
+ * operating is intensive, so the lookup should take into
+ * consideration this possibility.
+ *
+ * @param {string} key store key
+ *
+ * @return {Promise} promise for the key value
+ */
+ 'virtual abstract override public get': function( key )
+ {
+ var _self = this,
+ __super = this.__super.bind( this );
+
+ // to prevent stampeding, return any existing unresolved
+ // promise for this key
+ if ( this._misses[ key ] )
+ {
+ return this._misses[ key ];
+ }
+
+ // note that we have to store the reference immediately so we
+ // don't have two concurrent failures, so this will store a
+ // promise even if the key already exists (which is okay)
+ return this._misses[ key ] = this.__super( key )
+ .then( function( value )
+ {
+ // already exists
+ delete _self._misses[ key ];
+
+ return value;
+ } )
+ .catch( function()
+ {
+ delete _self._misses[ key ];
+
+ return _self._lookup( key )
+ .then( function( value )
+ {
+ return _self.add( key, value );
+ } )
+ .then( function()
+ {
+ return __super( key );
+ } );
+ } );
+ },
+} );
diff --git a/src/store/Store.js b/src/store/Store.js
new file mode 100644
index 0000000..3daf69b
--- /dev/null
+++ b/src/store/Store.js
@@ -0,0 +1,91 @@
+/**
+ * Generic key/value store
+ *
+ * Copyright (C) 2016 LoVullo Associates, Inc.
+ *
+ * This file is part of the Liza Data Collection Framework
+ *
+ * Liza is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+var Interface = require( 'easejs' ).Interface;
+
+
+/**
+ * Generic key/value store with bulk clear
+ *
+ * @todo There's a lot of overlap between this concept and that of the
+ * Bucket. Maybe have the Bucket layer atop of simple Store
+ * interface as a step toward a new, simpler Bucket
+ * implementation. This was not implemented atop of the Bucket
+ * interface because its haphazard implementation would
+ * overcomplicate this.
+ */
+module.exports = Interface( 'Store',
+{
+ /**
+ * Add item to store under `key` with value `value`
+ *
+ * The promise will be fulfilled with an object containing the
+ * `key` and `value` added to the store; this is convenient for
+ * promises.
+ *
+ * @param {string} key store key
+ * @param {*} value value for key
+ *
+ * @return {Promise} promise to add item to store
+ */
+ 'public add': [ 'key', 'value' ],
+
+
+ /**
+ * Retrieve item from store under `key`
+ *
+ * The promise will be rejected if the key is unavailable.
+ *
+ * @param {string} key store key
+ *
+ * @return {Promise} promise for the key value
+ */
+ 'public get': [ 'key' ],
+
+
+ /**
+ * Clear all items in store
+ *
+ * @return {Promise} promise to clear store
+ */
+ 'public clear': [],
+
+
+ /**
+ * Fold (reduce) all stored values
+ *
+ * This provides a way to iterate through all stored values and
+ * their keys while providing a useful functional result (folding).
+ *
+ * The order of folding is undefined.
+ *
+ * The ternary function `callback` is of the same form as
+ * {@link Array#fold}: the first argument is the value of the
+ * accumulator (initialized to the value of `initial`; the second
+ * is the stored item; and the third is the key of that item.
+ *
+ * @param {function(*,*,string=)} callback folding function
+ * @param {*} initial initial value for accumulator
+ *
+ * @return {Promise} promise of a folded value (final accumulator value)
+ */
+ 'public reduce': [ 'callback', 'initial' ],
+} );
diff --git a/src/store/StoreMissError.js b/src/store/StoreMissError.js
new file mode 100644
index 0000000..8017fdb
--- /dev/null
+++ b/src/store/StoreMissError.js
@@ -0,0 +1,31 @@
+/**
+ * Error when key is not found in store
+ *
+ * Copyright (C) 2016 LoVullo Associates, Inc.
+ *
+ * This file is part of the Liza Data Collection Framework
+ *
+ * Liza is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+var Class = require( 'easejs' ).Class;
+
+
+/**
+ * Store key miss
+ *
+ * A key was not found in a store.
+ */
+module.exports = Class( 'StoreMissError' )
+ .extend( ReferenceError, {} );
diff --git a/test/store/CascadingTest.js b/test/store/CascadingTest.js
new file mode 100644
index 0000000..f50d886
--- /dev/null
+++ b/test/store/CascadingTest.js
@@ -0,0 +1,138 @@
+/**
+ * Test case for Cascading store
+ *
+ * Copyright (C) 2016 LoVullo Associates, Inc.
+ *
+ * This file is part of the Liza Data Collection Framework
+ *
+ * Liza is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+"use strict";
+
+var store = require( '../../' ).store,
+ chai = require( 'chai' ),
+ expect = chai.expect,
+ Store = store.MemoryStore,
+ Sut = store.Cascading;
+
+chai.use( require( 'chai-as-promised' ) );
+
+
+describe( 'store.Cascading', () =>
+{
+ describe( '#add', () =>
+ {
+ it( 'does not allow attaching non-store objects', () =>
+ {
+ expect( Store.use( Sut )().add( 'invalid', {} ) )
+ .to.be.rejectedWith( TypeError );
+ } );
+
+
+ it( 'allows attaching Store objects', () =>
+ {
+ return Store.use( Sut )().add( 'valid', Store() );
+ } );
+ } );
+
+
+ describe( '#clear', () =>
+ {
+ it( 'invokes #clear on all contained stores', () =>
+ {
+ const cleared = [];
+
+ const MockStore = Store.extend(
+ {
+ 'override clear'()
+ {
+ cleared.push( this.__inst );
+
+ return Promise.resolve( true );
+ }
+ } );
+
+ const stores = [ 1, 2, 3 ].map( () => MockStore() );
+ const sut = Store.use( Sut )();
+
+ stores.forEach( ( store, i ) => sut.add( i, store ) );
+
+ // should trigger clear on all stores
+ return sut.clear()
+ .then( () =>
+ {
+ expect(
+ stores.every( store =>
+ cleared.some( item => item === store )
+ )
+ ).to.be.true;
+ } );
+ } );
+
+
+ it( 'does not clear self', () =>
+ {
+ const sut = Store.use( Sut )();
+ const substore = Store();
+
+ return sut.add( 'foo', substore )
+ .then( () => sut.clear() )
+ .then( () => {
+ return expect( sut.get( 'foo' ) )
+ .to.eventually.equal( substore )
+ } );
+ } );
+
+
+ [
+ [ [ true, true, true ], true ],
+ [ [ true, true, false ], false ],
+ [ [ false, true, true ], false ],
+ [ [ false, false, false ], false ],
+ ].forEach( testdata =>
+ {
+ let clears = testdata[ 0 ],
+ expected = testdata[ 1 ];
+
+ it( 'fails if any store fails to clear', () =>
+ {
+ let StubStore = Store.extend(
+ {
+ _result: false,
+
+ __construct( result )
+ {
+ this._result = result;
+ },
+
+ 'override clear'()
+ {
+ return Promise.resolve( this._result );
+ },
+ } );
+
+ let sut = Store.use( Sut )();
+
+ clears.forEach( ( result, i ) =>
+ {
+ sut.add( i, StubStore( result ) );
+ } );
+
+ return sut.clear()
+ .then( result => expect( result ).to.equal( expected ) );
+ } );
+ } );
+ } );
+} ); \ No newline at end of file
diff --git a/test/store/MemoryStoreTest.js b/test/store/MemoryStoreTest.js
new file mode 100644
index 0000000..7d47bc2
--- /dev/null
+++ b/test/store/MemoryStoreTest.js
@@ -0,0 +1,200 @@
+/**
+ * Test case for MemoryStore
+ *
+ * Copyright (C) 2016 LoVullo Associates, Inc.
+ *
+ * This file is part of the Liza Data Collection Framework
+ *
+ * Liza is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+"use strict";
+
+var store = require( '../../' ).store,
+ chai = require( 'chai' ),
+ expect = chai.expect,
+ Class = require( 'easejs' ).Class,
+ Trait = require( 'easejs' ).Trait,
+ Sut = store.MemoryStore;
+
+chai.use( require( 'chai-as-promised' ) );
+
+
+describe( 'store.MemoryStore', () =>
+{
+ describe( '#add', () =>
+ {
+ it( 'adds item to store when missing', () =>
+ {
+ const sut = Sut();
+ const item = {};
+
+ return expect(
+ sut.add( 'foo', item )
+ .then( () => sut.get( 'foo' ) )
+ ).to.eventually.equal( item );
+ } );
+
+
+ it( 'replaces item in store if existing', () =>
+ {
+ const sut = Sut();
+ const item = {};
+
+ return expect(
+ sut.add( 'foo', [] )
+ .then( () => sut.add( 'foo', item ) )
+ .then( () => sut.get( 'foo' ) )
+ ).to.eventually.equal( item );
+ } );
+
+
+ it( 'provides the key and value of the added item', () =>
+ {
+ const key = 'key';
+ const value = 'val';
+
+ return expect(
+ Sut().add( key, value )
+ ).to.eventually.deep.equal( { key: key, value: value } );
+ } );
+ } );
+
+
+ // most things implicitly tested above
+ describe( '#get', () =>
+ {
+ it( 'rejects promise if store item does not exist', () =>
+ {
+ return expect( Sut().get( 'unknown' ) )
+ .to.eventually.be.rejected
+ .and.be.instanceof( store.StoreMissError );
+ } );
+ } );
+
+
+ describe( '#clear', () =>
+ {
+ it( 'removes all items from store', () =>
+ {
+ const sut = Sut();
+ const keys = [ 'foo', 'bar', 'baz' ];
+
+ keys.forEach( key => sut.add( key ) );
+
+ // should remove all items
+ return sut.clear().then( () =>
+ {
+ return Promise.all(
+ keys.map( key => {
+ expect( sut.get( key ) )
+ .to.eventually.be.rejected
+ } )
+ );
+ } );
+ } );
+ } );
+
+
+ describe( 'with mixin', () =>
+ {
+ it( 'allows overriding add', done =>
+ {
+ const expected_key = 'foo';
+ const expected_value = {};
+
+ Sut.use(
+ Trait.extend( Sut,
+ {
+ 'override add'( key, value )
+ {
+ expect( key ).to.equal( expected_key );
+ expect( value ).to.equal( expected_value );
+ done();
+ }
+ } )
+ )().add( expected_key, expected_value );
+ } );
+
+
+ it( 'allows overriding get', done =>
+ {
+ const expected_key = 'bar';
+
+ Sut.use(
+ Trait.extend( Sut,
+ {
+ 'override get'( key )
+ {
+ expect( key ).to.equal( expected_key );
+ done();
+ }
+ } )
+ )().get( expected_key );
+ } );
+
+
+ it( 'allows overriding clear', done =>
+ {
+ Sut.use(
+ Trait.extend( Sut,
+ {
+ 'override clear'( key )
+ {
+ done();
+ }
+ } )
+ )().clear();
+ } );
+ } );
+
+
+ describe( '#reduce', () =>
+ {
+ it( 'folds each stored item', () =>
+ {
+ const StubSut = Sut.extend(
+ {
+ sum()
+ {
+ return this.reduce(
+ ( accum, item, key ) =>
+ {
+ // correct key for item?
+ expect( item ).to.equal( vals[ key ] );
+
+ return accum + item;
+ },
+ 5
+ );
+ }
+ } );
+
+ const sut = StubSut();
+ const vals = {
+ one: 1,
+ two: 2,
+ three: 3,
+ };
+
+ Object.keys( vals ).forEach(
+ ( key, i ) => sut.add( key, vals[ key ] )
+ );
+
+ // implicitly tests initial
+ return expect( sut.sum() )
+ to.equal( 11 );
+ } );
+ } );
+} );
diff --git a/test/store/MissLookupTest.js b/test/store/MissLookupTest.js
new file mode 100644
index 0000000..2c7e7d3
--- /dev/null
+++ b/test/store/MissLookupTest.js
@@ -0,0 +1,163 @@
+/**
+ * Test case for Cascading store
+ *
+ * Copyright (C) 2016 LoVullo Associates, Inc.
+ *
+ * This file is part of the Liza Data Collection Framework
+ *
+ * Liza is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+"use strict";
+
+var Class = require( 'easejs' ).Class,
+ store = require( '../../' ).store,
+ chai = require( 'chai' ),
+ expect = chai.expect,
+ Store = store.MemoryStore,
+ Sut = store.MissLookup;
+
+chai.use( require( 'chai-as-promised' ) );
+
+const StubStore = Class.extend( Store,
+{
+ 'virtual override get': function( key )
+ {
+ return this.__super( key )
+ .then( val => val + ' get' );
+ },
+} );
+
+
+describe( 'store.MissLookup', () =>
+{
+ it( 'requires function for cache miss', () =>
+ {
+ expect( () => StubStore.use( Sut( {} ) )() )
+ .to.throw( TypeError );
+ } );
+
+ it( 'invokes lookup on cache miss', () =>
+ {
+ const expected = 'quux';
+ const miss = ( key ) => Promise.resolve( key + expected );
+ const sut = StubStore.use( Sut( miss ) )();
+
+ // key + expected + StubStore#get
+ return Promise.all( [
+ expect( sut.get( 'foo' ) )
+ .to.eventually.equal( 'foo' + expected + ' get' ),
+
+ expect( sut.get( 'bar' ) )
+ .to.eventually.equal( 'bar' + expected + ' get' ),
+ ] );
+ } );
+
+
+ it( 'caches miss lookup', () =>
+ {
+ let calln = 0;
+
+ const expected = {};
+
+ function miss()
+ {
+ // should not be called more than once
+ expect( ++calln ).to.equal( 1 );
+ return Promise.resolve( expected );
+ }
+
+ const sut = Store.use( Sut( miss ) )();
+
+ // should miss
+ return expect( sut.get( 'foo' ) ).to.eventually.equal( expected )
+ .then( () =>
+ {
+ // should not miss
+ expect( sut.get( 'foo' ) ).to.eventually.equal( expected );
+ } );
+ } );
+
+
+ it( 'does not miss on existing cache item', () =>
+ {
+ const fail = () => { throw Error( 'Should not have missed' ) };
+ const sut = Store.use( Sut( fail ) )();
+
+ return expect(
+ sut.add( 'foo', 'bar' )
+ .then( () => sut.get( 'foo' ) )
+ ).to.eventually.equal( 'bar' );
+ } );
+
+
+ // prevent stampeding and concurrency issues
+ it( 'shares promise given concurrent miss requests', () =>
+ {
+ let n = 0;
+ let resolve = null;
+
+ const p = new Promise( r => resolve = r );
+
+ // return our mock promise, which should only be once (after
+ // that, the SUT should have cached the promise)
+ function miss()
+ {
+ if ( n++ > 0 ) throw Error( 'Miss called more than once' );
+ return p;
+ }
+
+ const sut = Store.use( Sut( miss ) )();
+
+ // set of three promises, each on the same key
+ const misses = [ 1, 2, 3 ].map( i => sut.get( 'foo' ) );
+
+ // we don't really care what promises were returned to us
+ // (that's an implementation detail), but we do care that they
+ // all resolve once we resolve our promise
+ resolve( true );
+
+ return Promise.all( misses );
+ } );
+
+
+ it( 'does not share old promise after miss resolves', () =>
+ {
+ let missret = {};
+
+ const key = 'foo';
+ const miss1 = {};
+ const miss2 = {};
+
+ function miss()
+ {
+ return Promise.resolve( missret );
+ }
+
+ const sut = Store.use( Sut( miss ) )();
+
+ missret = miss1;
+
+ return sut.get( key )
+ .then( val => expect( val ).to.equal( miss1 ) )
+ .then( () =>
+ {
+ sut.clear();
+
+ missret = miss2;
+ return sut.get( key )
+ .then( val => expect( val ).to.equal( miss2 ) )
+ } );
+ } );
+} );