From 0be39adfdb2ebe7afbb963a2180294b1f62bd57d Mon Sep 17 00:00:00 2001 From: Mike Gerwitz Date: Fri, 30 Dec 2016 13:58:06 -0500 Subject: [PATCH] Make {,Memory}Store asynchronous This isn't terribly useful as a general-purpose cache if it can't handle async requests. --- src/store/Cascading.js | 54 +++++++++++++------- src/store/MemoryStore.js | 92 +++++++++++++++++++++++------------ test/store/CascadingTest.js | 69 +++++++++++++++++++++----- test/store/MemoryStoreTest.js | 66 +++++++++++++++---------- 4 files changed, 195 insertions(+), 86 deletions(-) diff --git a/src/store/Cascading.js b/src/store/Cascading.js index 0bf4dfa..5e6c55d 100644 --- a/src/store/Cascading.js +++ b/src/store/Cascading.js @@ -32,22 +32,23 @@ var Trait = require( 'easejs' ).Trait, * when `#clear` is invoked on `S`. * * @example - * let store_a = Store().add( 'key', value' ), - * store_b = Store().add( 'foo', 'bar' ); + * let store_a = Store(), + * store_b = Store(); * - * store_a.get( 'key' ); // value - * store_b.get( 'foo' ); // bar + * // assuming sync. store for example (ignore promises) + * store_a.add( 'key', 'value' ); + * store_b.add( 'foo', 'bar' ); * - * Store.use( Cascading ) - * .add( 'a', store_a ) - * .add( 'b', store_b ) - * .clear(); + * let container = Store.use( Cascading ); + * container.add( 'a', store_a ); + * container.add( 'b', store_b ); + * container.clear(); * - * store_a.get( 'key' ); // undefined - * store_b.get( 'foo' ); // undefined + * store_a.get( 'key' ); // Promise rejects + * store_b.get( 'foo' ); // Promise rejects * * Store.use( Cascading ).add( 'invalid', 'value' ); - * // TypeError: Can only add Store to Cascading stores + * // 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 @@ -71,13 +72,15 @@ module.exports = Trait( 'Cascading' ) * @param {string} key store key * @param {Store} value Store to attach * - * @return {Store} self + * @return {Promise} promise to add item to store */ 'virtual abstract override public add': function( key, value ) { if ( !Class.isA( Store, value ) ) { - throw TypeError( "Can only add Store to Cascading stores" ); + return Promise.reject( + TypeError( "Can only add Store to Cascading stores" ) + ); } return this.__super( key, value ); @@ -87,13 +90,28 @@ module.exports = Trait( 'Cascading' ) /** * Clear all stores in the store * - * @return {Store} self + * @return {Promise} promise to clear all caches */ 'virtual abstract override public clear': function() { - this.reduce( function( _, store ) - { - store.clear(); - } ); + 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 index 74bfbdf..52786f4 100644 --- a/src/store/MemoryStore.js +++ b/src/store/MemoryStore.js @@ -36,14 +36,28 @@ var Class = require( 'easejs' ).Class, * @example * let s = MemoryStore(); * - * s.add( 'foo', 'bar' ); - * s.add( 'baz', 'quux' ); - * s.get( 'foo' ); // bar - * s.get( 'baz' ); // quux + * 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(); - * s.get( 'foo' ); // undefined - * s.get( 'baz' ); // undefined + * s.clear().then( function() + * { + * s.get( 'foo' ) + * .catch( function() + * { + * // foo is no longer defined + * } ); + * } ); */ module.exports = Class( 'MemoryStore' ) .implement( Store ) @@ -58,51 +72,62 @@ module.exports = Class( 'MemoryStore' ) /** - * Add item to cache under `key` with value `value` + * Add item to store under `key` with value `value` * - * @param {string} key cache key + * 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 {Store} self + * @return {Promise} promise to add item to store */ 'virtual public add': function( key, value ) { this._store[ key ] = value; - return this; + return Promise.resolve( { + key: key, + value: value, + } ); }, /** - * Retrieve item from cache under `key` + * Retrieve item from store under `key` * - * @param {string} key cache key + * The promise will be rejected if the key is unavailable. * - * @return {*} `key` value + * @param {string} key store key + * + * @return {Promise} promise for the key value */ 'virtual public get': function( key ) { - return this._store[ key ]; + return ( this._store[ key ] !== undefined ) + ? Promise.resolve( this._store[ key ] ) + : Promise.reject( 'Key ' + key + ' does not exist' ); }, /** - * Clear all items in cache + * Clear all items in store * - * @return {Store} self + * @return {Promise} promise to clear store */ 'virtual public clear': function() { this._store = {}; - return this; + return Promise.resolve( true ); }, /** - * Fold (reduce) all cached values + * Fold (reduce) all stored values * - * This provides a way to iterate through all cached values and + * 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. @@ -110,25 +135,30 @@ module.exports = Class( 'MemoryStore' ) * 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 cached item; and the third is the key of that item. + * 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 {*} folded value (final accumulator value) + * @return {Promise} promise of a folded value (final accumulator value) */ 'public reduce': function( callback, initial ) { var store = this._store; - return Object.keys( store ) - .map( function( key ) - { - return [ key, store[ key ] ]; - } ) - .reduce( function( accum, values ) - { - return callback( accum, values[ 1 ], values[ 0 ] ); - }, initial ); + 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/test/store/CascadingTest.js b/test/store/CascadingTest.js index 51aaca0..737ca76 100644 --- a/test/store/CascadingTest.js +++ b/test/store/CascadingTest.js @@ -22,25 +22,28 @@ "use strict"; var store = require( '../../' ).store, - expect = require( 'chai' ).expect, + 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.throw( TypeError ); + expect( Store.use( Sut )().add( 'invalid', {} ) ) + .to.be.rejectedWith( TypeError ); } ); it( 'allows attaching Store objects', () => { - expect( () => Store.use( Sut )().add( 'valid', Store() ) ) - .to.not.throw( TypeError ); + return Store.use( Sut )().add( 'valid', Store() ); } ); } ); @@ -56,6 +59,8 @@ describe( 'store.Cascading', () => 'override clear'() { cleared.push( this.__inst ); + + return Promise.resolve( true ); } } ); @@ -65,13 +70,55 @@ describe( 'store.Cascading', () => stores.forEach( ( store, i ) => sut.add( i, store ) ); // should trigger clear on all stores - sut.clear(); + return sut.clear() + .then( () => + { + expect( + stores.every( store => + cleared.some( item => item === store ) + ) + ).to.be.true; + } ); + } ); - expect( - stores.every( store => - cleared.some( item => item === store ) - ) - ).to.be.true; + + [ + [ [ 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 index a152911..82392af 100644 --- a/test/store/MemoryStoreTest.js +++ b/test/store/MemoryStoreTest.js @@ -21,11 +21,14 @@ "use strict"; -var store = require( '../../' ).store, - expect = require( 'chai' ).expect, - Class = require( 'easejs' ).Class, - Trait = require( 'easejs' ).Trait, - Sut = store.MemoryStore; +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', () => @@ -37,10 +40,10 @@ describe( 'store.MemoryStore', () => const sut = Sut(); const item = {}; - expect( + return expect( sut.add( 'foo', item ) - .get( 'foo' ) - ).to.equal( item ); + .then( () => sut.get( 'foo' ) ) + ).to.eventually.equal( item ); } ); @@ -49,11 +52,22 @@ describe( 'store.MemoryStore', () => const sut = Sut(); const item = {}; - expect( + return expect( sut.add( 'foo', [] ) - .add( 'foo', item ) - .get( 'foo' ) - ).to.equal( item ); + .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 } ); } ); } ); @@ -61,9 +75,10 @@ describe( 'store.MemoryStore', () => // most things implicitly tested above describe( '#get', () => { - it( 'returns undefined if store item does not exist', () => + it( 'rejects promise if store item does not exist', () => { - expect( Sut().get( 'unknown' ) ).to.be.undefined; + return expect( Sut().get( 'unknown' ) ) + .to.eventually.be.rejected; } ); } ); @@ -78,17 +93,15 @@ describe( 'store.MemoryStore', () => keys.forEach( key => sut.add( key ) ); // should remove all items - sut.clear(); - - keys.forEach( key => expect( sut.get( key ) ).to.be.undefined ); - } ); - - - it( 'returns self', () => - { - const sut = Sut(); - - expect( sut.clear() ).to.equal( sut ); + return sut.clear().then( () => + { + return Promise.all( + keys.map( key => { + expect( sut.get( key ) ) + .to.eventually.be.rejected + } ) + ); + } ); } ); } ); @@ -179,7 +192,8 @@ describe( 'store.MemoryStore', () => ); // implicitly tests initial - expect( sut.sum() ).to.equal( 11 ); + return expect( sut.sum() ) + to.equal( 11 ); } ); } ); } );