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`.
*
* @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;
} );
} );
},
} );

View File

@ -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 )
);
}
} );

View File

@ -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 ) );
} );
} );
} );
} );

View File

@ -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 );
} );
} );
} );