middleware get their local state

typescript
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
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
- Document mapping behavior of the '*' subdux.
- add subduxUpreducer.

View File

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

View File

@ -1,7 +1,7 @@
import fp from 'lodash/fp';
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 => {
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;
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>(
effects : Dictionary<Middleware<{},S,UpduxDispatch>>= {},
actions : Dictionary<ActionCreator>= {},
subMiddlewares :Middleware<{},S,UpduxDispatch>[] = [],
): Middleware<{},S,UpduxDispatch>
subduxes :any = {},
): 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>) => {
for (let type in actions) {

View File

@ -1,110 +1,142 @@
import Updux from '.';
import u from 'updeep';
test( 'simple effect', () => {
test('simple effect', () => {
const tracer = jest.fn();
const tracer = jest.fn();
const store = (new Updux({
effects: {
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);
const store = new Updux({
effects: {
foo: (api: any) => (next: any) => (action: any) => {
tracer();
next(action);
};
},
},
}).createStore();
const store = (new Updux({
effects: {
foo: tracerEffect('root'),
},
subduxes: {
zzz: {effects: {
foo: tracerEffect('child'),
}
}
},
})).createStore();
expect(tracer).not.toHaveBeenCalled();
expect(tracer).not.toHaveBeenCalled();
store.dispatch({type: 'bar'});
store.dispatch({ type: 'bar' });
expect(tracer).not.toHaveBeenCalled();
expect(tracer).not.toHaveBeenCalled();
store.dispatch.foo();
expect(tracer).toHaveBeenNthCalledWith(1,'root');
expect(tracer).toHaveBeenNthCalledWith(2,'child');
store.dispatch.foo();
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: {
'*': api => next => action => {
tracer();
next(action);
},
foo: tracerEffect('child'),
},
})).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) {
return new Promise(resolve => setTimeout(resolve, ms));
}
const store = new Updux({
effects: {
'*': api => next => action => {
tracer();
next(action);
},
},
}).createStore();
const tracer = jest.fn();
expect(tracer).not.toHaveBeenCalled();
const store = (new Updux({
effects: {
foo: api => next => async action => {
next(action);
await timeout(1000);
tracer();
},
},
})).createStore();
store.dispatch({type: 'bar'});
expect(tracer).not.toHaveBeenCalled();
store.dispatch.foo();
expect(tracer).not.toHaveBeenCalled();
await timeout(1000);
expect(tracer).toHaveBeenCalled();
expect(tracer).toHaveBeenCalled();
});
test('async effect', async () => {
function timeout(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms));
}
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 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
* updux and its subduxes. Effects of the updux itself are
* 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> {
return buildMiddleware(
this.localEffects,
this.actions,
Object.values(this.subduxes).map(sd => sd.middleware),
this.subduxes,
);
}