middleware get their local state

This commit is contained in:
Yanick Champoux 2019-11-06 19:01:31 -05:00
parent 74d29d3126
commit 9b5a7981d5
6 changed files with 155 additions and 90 deletions

View File

@ -1,5 +1,10 @@
# Revision history for Updux # Revision history for Updux
1.2.0 2019-11-06
- The middleware's 'getState' returns the local state of its updux,
instead of the root state. Plus we add `getRootState` to get
the root state.
1.1.0 2019-11-05 1.1.0 2019-11-05
- Document mapping behavior of the '*' subdux. - Document mapping behavior of the '*' subdux.
- add subduxUpreducer. - add subduxUpreducer.

View File

@ -26,7 +26,7 @@
"build": "tsc && typedoc", "build": "tsc && typedoc",
"test": "jest" "test": "jest"
}, },
"version": "1.1.0", "version": "1.2.0",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git+https://github.com/yanick/updux.git" "url": "git+https://github.com/yanick/updux.git"

View File

@ -1,7 +1,7 @@
import fp from 'lodash/fp'; import fp from 'lodash/fp';
import { Middleware, MiddlewareAPI, Dispatch } from 'redux'; import { Middleware, MiddlewareAPI, Dispatch } from 'redux';
import { Dictionary, ActionCreator, Action, UpduxDispatch } from '../types'; import { Dictionary, ActionCreator, Action, UpduxDispatch, UpduxMiddleware } from '../types';
const MiddlewareFor = (type: any, mw: Middleware ): Middleware => api => next => action => { const MiddlewareFor = (type: any, mw: Middleware ): Middleware => api => next => action => {
if (type !== '*' && action.type !== type) return next(action); if (type !== '*' && action.type !== type) return next(action);
@ -11,12 +11,28 @@ const MiddlewareFor = (type: any, mw: Middleware ): Middleware => api => next =>
type Next = (action: Action) => any; type Next = (action: Action) => any;
function sliceMw( slice: string, mw: Middleware ): Middleware {
return (api) => {
const getSliceState = () => fp.get(slice, api.getState() );
const getRootState = (api as any).getRootState || api.getState;
return mw({...api, getState: getSliceState, getRootState} as any )
};
}
function buildMiddleware<S=any>( function buildMiddleware<S=any>(
effects : Dictionary<Middleware<{},S,UpduxDispatch>>= {}, effects : Dictionary<Middleware<{},S,UpduxDispatch>>= {},
actions : Dictionary<ActionCreator>= {}, actions : Dictionary<ActionCreator>= {},
subMiddlewares :Middleware<{},S,UpduxDispatch>[] = [], subduxes :any = {},
): Middleware<{},S,UpduxDispatch> ): UpduxMiddleware<S>
{ {
const subMiddlewares = fp.flow(
fp.mapValues( fp.get('middleware') ),
fp.toPairs,
fp.filter(x=>x[1]),
fp.map( ([ slice, mw ]: [ string, Middleware]) => sliceMw(slice,mw) )
)( subduxes );
return (api: MiddlewareAPI<UpduxDispatch,S>) => { return (api: MiddlewareAPI<UpduxDispatch,S>) => {
for (let type in actions) { for (let type in actions) {

View File

@ -1,110 +1,142 @@
import Updux from '.'; import Updux from '.';
import u from 'updeep'; import u from 'updeep';
test( 'simple effect', () => { test('simple effect', () => {
const tracer = jest.fn();
const tracer = jest.fn(); const store = new Updux({
effects: {
const store = (new Updux({ foo: (api: any) => (next: any) => (action: any) => {
effects: { tracer();
foo: (api:any) => (next:any) => (action:any) => {
tracer();
next(action);
},
},
})).createStore();
expect(tracer).not.toHaveBeenCalled();
store.dispatch({ type: 'bar' });
expect(tracer).not.toHaveBeenCalled();
store.dispatch.foo();
expect(tracer).toHaveBeenCalled();
});
test( 'effect and sub-effect', () => {
const tracer = jest.fn();
const tracerEffect = ( signature: string ) => ( api:any ) => (next:any) => ( action: any ) => {
tracer(signature);
next(action); next(action);
}; },
},
}).createStore();
const store = (new Updux({ expect(tracer).not.toHaveBeenCalled();
effects: {
foo: tracerEffect('root'),
},
subduxes: {
zzz: {effects: {
foo: tracerEffect('child'),
}
}
},
})).createStore();
expect(tracer).not.toHaveBeenCalled(); store.dispatch({type: 'bar'});
store.dispatch({ type: 'bar' }); expect(tracer).not.toHaveBeenCalled();
expect(tracer).not.toHaveBeenCalled(); store.dispatch.foo();
store.dispatch.foo();
expect(tracer).toHaveBeenNthCalledWith(1,'root');
expect(tracer).toHaveBeenNthCalledWith(2,'child');
expect(tracer).toHaveBeenCalled();
}); });
test( '"*" effect', () => { test('effect and sub-effect', () => {
const tracer = jest.fn();
const tracer = jest.fn(); const tracerEffect = (signature: string) => (api: any) => (next: any) => (
action: any,
) => {
tracer(signature);
next(action);
};
const store = (new Updux({ const store = new Updux({
effects: {
foo: tracerEffect('root'),
},
subduxes: {
zzz: {
effects: { effects: {
'*': api => next => action => { foo: tracerEffect('child'),
tracer();
next(action);
},
}, },
})).createStore(); },
},
}).createStore();
expect(tracer).not.toHaveBeenCalled(); expect(tracer).not.toHaveBeenCalled();
store.dispatch({ type: 'bar' }); store.dispatch({type: 'bar'});
expect(tracer).toHaveBeenCalled(); expect(tracer).not.toHaveBeenCalled();
store.dispatch.foo();
expect(tracer).toHaveBeenNthCalledWith(1, 'root');
expect(tracer).toHaveBeenNthCalledWith(2, 'child');
}); });
test( 'async effect', async () => { test('"*" effect', () => {
const tracer = jest.fn();
function timeout(ms:number) { const store = new Updux({
return new Promise(resolve => setTimeout(resolve, ms)); effects: {
} '*': api => next => action => {
tracer();
next(action);
},
},
}).createStore();
const tracer = jest.fn(); expect(tracer).not.toHaveBeenCalled();
const store = (new Updux({ store.dispatch({type: 'bar'});
effects: {
foo: api => next => async action => {
next(action);
await timeout(1000);
tracer();
},
},
})).createStore();
expect(tracer).not.toHaveBeenCalled(); expect(tracer).toHaveBeenCalled();
});
store.dispatch.foo();
test('async effect', async () => {
expect(tracer).not.toHaveBeenCalled(); function timeout(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms));
await timeout(1000); }
expect(tracer).toHaveBeenCalled(); const tracer = jest.fn();
const store = new Updux({
effects: {
foo: api => next => async action => {
next(action);
await timeout(1000);
tracer();
},
},
}).createStore();
expect(tracer).not.toHaveBeenCalled();
store.dispatch.foo();
expect(tracer).not.toHaveBeenCalled();
await timeout(1000);
expect(tracer).toHaveBeenCalled();
});
test('getState is local', () => {
let childState;
let rootState;
let rootFromChild;
const child = new Updux({
initial: {alpha: 12},
effects: {
doIt: ({getState,getRootState}) => next => action => {
childState = getState();
rootFromChild = getRootState();
next(action);
},
},
});
const root = new Updux({
initial: {beta: 24},
subduxes: {child},
effects: {
doIt: ({getState}) => next => action => {
rootState = getState();
next(action);
},
},
});
const store = root.createStore();
store.dispatch.doIt();
expect(rootState).toEqual({beta: 24, child: {alpha: 12}});
expect(rootFromChild).toEqual({beta: 24, child: {alpha: 12}});
expect(childState).toEqual({alpha: 12});
}); });

View File

@ -178,7 +178,16 @@ export type UpduxConfig<S=any> = {
* ``` * ```
* *
*/ */
effects?: Dictionary<Middleware<{}, S, UpduxDispatch>>; effects?: Dictionary<UpduxMiddleware<S>>;
}; };
export type Upreducer<S = any> = (action: Action) => (state: S) => S; export type Upreducer<S = any> = (action: Action) => (state: S) => S;
export interface UpduxMiddlewareAPI<S> {
dispatch: UpduxDispatch,
getState(): any,
getRootState(): S
}
export type UpduxMiddleware<S=any> = (api: UpduxMiddlewareAPI<S> ) => ( next: UpduxDispatch ) => ( action: Action ) => any;

View File

@ -132,12 +132,15 @@ export class Updux<S = any> {
* A middleware aggregating all the effects defined in the * A middleware aggregating all the effects defined in the
* updux and its subduxes. Effects of the updux itself are * updux and its subduxes. Effects of the updux itself are
* done before the subduxes effects. * done before the subduxes effects.
* Note that `getState` will always return the state of the
* local updux. The function `getRootState` is provided
* alongside `getState` to get the root state.
*/ */
@computed get middleware(): Middleware<{}, S, UpduxDispatch> { @computed get middleware(): Middleware<{}, S, UpduxDispatch> {
return buildMiddleware( return buildMiddleware(
this.localEffects, this.localEffects,
this.actions, this.actions,
Object.values(this.subduxes).map(sd => sd.middleware), this.subduxes,
); );
} }