1
0
Fork 0

Make {,Memory}Store asynchronous

This isn't terribly useful as a general-purpose cache if it can't
handle async requests.
master
Mike Gerwitz 2016-12-30 13:58:06 -05:00
parent acc75cc3a9
commit 0be39adfdb
4 changed files with 195 additions and 86 deletions

View File

@ -32,22 +32,23 @@ var Trait = require( 'easejs' ).Trait,
* when `#clear` is invoked on `S`. * when `#clear` is invoked on `S`.
* *
* @example * @example
* let store_a = Store().add( 'key', value' ), * let store_a = Store(),
* store_b = Store().add( 'foo', 'bar' ); * store_b = Store();
* *
* store_a.get( 'key' ); // value * // assuming sync. store for example (ignore promises)
* store_b.get( 'foo' ); // bar * store_a.add( 'key', 'value' );
* store_b.add( 'foo', 'bar' );
* *
* Store.use( Cascading ) * let container = Store.use( Cascading );
* .add( 'a', store_a ) * container.add( 'a', store_a );
* .add( 'b', store_b ) * container.add( 'b', store_b );
* .clear(); * container.clear();
* *
* store_a.get( 'key' ); // undefined * store_a.get( 'key' ); // Promise rejects
* store_b.get( 'foo' ); // undefined * store_b.get( 'foo' ); // Promise rejects
* *
* Store.use( Cascading ).add( 'invalid', 'value' ); * 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 * Although clear cascades to each `Store`, other methods do not (for
* example, `get` will not query all `Store`s); another trait should * 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 {string} key store key
* @param {Store} value Store to attach * @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 ) 'virtual abstract override public add': function( key, value )
{ {
if ( !Class.isA( Store, 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 ); return this.__super( key, value );
@ -87,13 +90,28 @@ module.exports = Trait( 'Cascading' )
/** /**
* Clear all stores in the store * Clear all stores in the store
* *
* @return {Store} self * @return {Promise} promise to clear all caches
*/ */
'virtual abstract override public clear': function() 'virtual abstract override public clear': function()
{ {
this.reduce( function( _, store ) return this.reduce(
{ function( accum, store )
store.clear(); {
} ); accum.push( store.clear() );
return accum;
},
[]
)
.then( function( promises )
{
return Promise.all( promises );
} )
.then( function( result )
{
return result.every( function( value )
{
return value === true;
} );
} );
}, },
} ); } );

View File

@ -36,14 +36,28 @@ var Class = require( 'easejs' ).Class,
* @example * @example
* let s = MemoryStore(); * let s = MemoryStore();
* *
* s.add( 'foo', 'bar' ); * Promise.all( [
* s.add( 'baz', 'quux' ); * s.add( 'foo', 'bar' ),
* s.get( 'foo' ); // bar * s.add( 'baz', 'quux' ),
* s.get( 'baz' ); // quux * ] )
* .then( Promise.all( [
* {
* s.get( 'foo' ),
* s.get( 'baz' ),
* ] ) } )
* .then( function( values )
* {
* // values = [ 'bar', 'quux' ]
* } );
* *
* s.clear(); * s.clear().then( function()
* s.get( 'foo' ); // undefined * {
* s.get( 'baz' ); // undefined * s.get( 'foo' )
* .catch( function()
* {
* // foo is no longer defined
* } );
* } );
*/ */
module.exports = Class( 'MemoryStore' ) module.exports = Class( 'MemoryStore' )
.implement( Store ) .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 * @param {*} value value for key
* *
* @return {Store} self * @return {Promise} promise to add item to store
*/ */
'virtual public add': function( key, value ) 'virtual public add': function( key, value )
{ {
this._store[ 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 ) '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() 'virtual public clear': function()
{ {
this._store = {}; 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). * their keys while providing a useful functional result (folding).
* *
* The order of folding is undefined. * The order of folding is undefined.
@ -110,25 +135,30 @@ module.exports = Class( 'MemoryStore' )
* The ternary function `callback` is of the same form as * The ternary function `callback` is of the same form as
* {@link Array#fold}: the first argument is the value of the * {@link Array#fold}: the first argument is the value of the
* accumulator (initialized to the value of `initial`; the second * 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 {function(*,*,string=)} callback folding function
* @param {*} initial initial value for accumulator * @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 ) 'public reduce': function( callback, initial )
{ {
var store = this._store; var store = this._store;
return Object.keys( store ) return Promise.resolve(
.map( function( key ) Object.keys( store )
{ .map( function( key )
return [ key, store[ key ] ]; {
} ) return [ key, store[ key ] ];
.reduce( function( accum, values ) } )
{ .reduce( function( accum, values )
return callback( accum, values[ 1 ], values[ 0 ] ); {
}, initial ); return callback( accum, values[ 1 ], values[ 0 ] );
}, initial )
);
} }
} ); } );

View File

@ -22,25 +22,28 @@
"use strict"; "use strict";
var store = require( '../../' ).store, var store = require( '../../' ).store,
expect = require( 'chai' ).expect, chai = require( 'chai' ),
expect = chai.expect,
Store = store.MemoryStore, Store = store.MemoryStore,
Sut = store.Cascading; Sut = store.Cascading;
chai.use( require( 'chai-as-promised' ) );
describe( 'store.Cascading', () => describe( 'store.Cascading', () =>
{ {
describe( '#add', () => describe( '#add', () =>
{ {
it( 'does not allow attaching non-store objects', () => it( 'does not allow attaching non-store objects', () =>
{ {
expect( () => Store.use( Sut )().add( 'invalid', {} ) ) expect( Store.use( Sut )().add( 'invalid', {} ) )
.to.throw( TypeError ); .to.be.rejectedWith( TypeError );
} ); } );
it( 'allows attaching Store objects', () => it( 'allows attaching Store objects', () =>
{ {
expect( () => Store.use( Sut )().add( 'valid', Store() ) ) return Store.use( Sut )().add( 'valid', Store() );
.to.not.throw( TypeError );
} ); } );
} ); } );
@ -56,6 +59,8 @@ describe( 'store.Cascading', () =>
'override clear'() 'override clear'()
{ {
cleared.push( this.__inst ); cleared.push( this.__inst );
return Promise.resolve( true );
} }
} ); } );
@ -65,13 +70,55 @@ describe( 'store.Cascading', () =>
stores.forEach( ( store, i ) => sut.add( i, store ) ); stores.forEach( ( store, i ) => sut.add( i, store ) );
// should trigger clear on all stores // 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 ) [ [ true, true, true ], true ],
) [ [ true, true, false ], false ],
).to.be.true; [ [ 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 ) );
} );
} ); } );
} ); } );
} ); } );

View File

@ -21,11 +21,14 @@
"use strict"; "use strict";
var store = require( '../../' ).store, var store = require( '../../' ).store,
expect = require( 'chai' ).expect, chai = require( 'chai' ),
Class = require( 'easejs' ).Class, expect = chai.expect,
Trait = require( 'easejs' ).Trait, Class = require( 'easejs' ).Class,
Sut = store.MemoryStore; Trait = require( 'easejs' ).Trait,
Sut = store.MemoryStore;
chai.use( require( 'chai-as-promised' ) );
describe( 'store.MemoryStore', () => describe( 'store.MemoryStore', () =>
@ -37,10 +40,10 @@ describe( 'store.MemoryStore', () =>
const sut = Sut(); const sut = Sut();
const item = {}; const item = {};
expect( return expect(
sut.add( 'foo', item ) sut.add( 'foo', item )
.get( 'foo' ) .then( () => sut.get( 'foo' ) )
).to.equal( item ); ).to.eventually.equal( item );
} ); } );
@ -49,11 +52,22 @@ describe( 'store.MemoryStore', () =>
const sut = Sut(); const sut = Sut();
const item = {}; const item = {};
expect( return expect(
sut.add( 'foo', [] ) sut.add( 'foo', [] )
.add( 'foo', item ) .then( () => sut.add( 'foo', item ) )
.get( 'foo' ) .then( () => sut.get( 'foo' ) )
).to.equal( item ); ).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 // most things implicitly tested above
describe( '#get', () => 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 ) ); keys.forEach( key => sut.add( key ) );
// should remove all items // should remove all items
sut.clear(); return sut.clear().then( () =>
{
keys.forEach( key => expect( sut.get( key ) ).to.be.undefined ); return Promise.all(
} ); keys.map( key => {
expect( sut.get( key ) )
.to.eventually.be.rejected
it( 'returns self', () => } )
{ );
const sut = Sut(); } );
expect( sut.clear() ).to.equal( sut );
} ); } );
} ); } );
@ -179,7 +192,8 @@ describe( 'store.MemoryStore', () =>
); );
// implicitly tests initial // implicitly tests initial
expect( sut.sum() ).to.equal( 11 ); return expect( sut.sum() )
to.equal( 11 );
} ); } );
} ); } );
} ); } );