Make {,Memory}Store asynchronous
This isn't terribly useful as a general-purpose cache if it can't handle async requests.master
parent
acc75cc3a9
commit
0be39adfdb
|
@ -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;
|
||||||
|
} );
|
||||||
|
} );
|
||||||
},
|
},
|
||||||
} );
|
} );
|
||||||
|
|
|
@ -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 )
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} );
|
} );
|
||||||
|
|
|
@ -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 ) );
|
||||||
|
} );
|
||||||
} );
|
} );
|
||||||
} );
|
} );
|
||||||
} );
|
} );
|
|
@ -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 );
|
||||||
} );
|
} );
|
||||||
} );
|
} );
|
||||||
} );
|
} );
|
||||||
|
|
Loading…
Reference in New Issue