diff --git a/src/actions.test.js b/src/actions.test.js new file mode 100644 index 0000000..b321e98 --- /dev/null +++ b/src/actions.test.js @@ -0,0 +1,24 @@ +import updux from '.'; +import u from 'updeep'; + +test( 'actions defined in effects and mutations, multi-level', () => { + + const { actions } = updux({ + effects: { + foo: api => next => action => { }, + }, + mutations: { bar: () => () => null }, + subduxes: { + mysub: updux({ + effects: { baz: api => next => action => { }, }, + mutations: { quux: () => () => null }, + }) + }, + }); + + const types = Object.keys(actions); + types.sort(); + + expect( types).toEqual([ 'bar', 'baz', 'foo', 'quux', ]); + +}); diff --git a/src/buildMiddleware.js b/src/buildMiddleware.js new file mode 100644 index 0000000..f40988a --- /dev/null +++ b/src/buildMiddleware.js @@ -0,0 +1,29 @@ +import fp from 'lodash/fp'; + +const MiddlewareFor = (type,mw) => api => next => action => { + if (type !== '*' && action.type !== type) return next(action); + + return mw(api)(next)(action); +}; + +export default function buildMiddleware( + {effects = {}, subduxes = {}}, + {actions}, +) { + return api => { + for (let type in actions) { + api.dispatch[type] = (...args) => api.dispatch(actions[type](...args)); + } + + return original_next => { + return [ + ...fp.toPairs(effects).map(([type, effect]) => + MiddlewareFor(type,effect) + ), + ...fp.map('middleware', subduxes), + ] + .filter(x => x) + .reduceRight((next, mw) => mw(api)(next), original_next); + }; + }; +} diff --git a/src/index.js b/src/index.js index e265474..ccd34b8 100644 --- a/src/index.js +++ b/src/index.js @@ -3,6 +3,8 @@ import u from 'updeep'; import { createStore, applyMiddleware } from 'redux'; +import buildMiddleware from './buildMiddleware'; + function actionFor(type) { return (payload = null, meta = null) => { return fp.pickBy(v => v !== null)({type, payload, meta}); @@ -22,7 +24,7 @@ function buildInitial({initial = {}, subduxes = {}}) { return initial; } -function buildActions({mutations = {}, subduxes = {}}) { +function buildActions({mutations = {}, effects = {}, subduxes = {}}) { let actions = fp.mergeAll(fp.map(fp.getOr({}, 'actions'), subduxes)) || {}; Object.keys(mutations).forEach(type => { @@ -31,6 +33,12 @@ function buildActions({mutations = {}, subduxes = {}}) { } }); + Object.keys(effects).forEach(type => { + if (!actions[type]) { + actions[type] = actionFor(type); + } + }); + return actions; } @@ -44,13 +52,13 @@ function buildMutations({mutations = {}, subduxes = {}}) { // without, as the root '*' is not the same as any sub-'*' const actions = fp.uniq( Object.keys(mutations).concat( - ...Object.values( subduxes ).map( ({mutations}) => Object.keys(mutations) ) + ...Object.values( subduxes ).map( ({mutations = {}}) => Object.keys(mutations) ) ) ); let mergedMutations = {}; let [ globby, nonGlobby ] = fp.partition( - ([_,{mutations}]) => mutations['*'], + ([_,{mutations={}}]) => mutations['*'], Object.entries(subduxes) ); @@ -66,7 +74,7 @@ function buildMutations({mutations = {}, subduxes = {}}) { mergedMutations[action] = [ globbyMutation ] }); - nonGlobby.forEach( ([slice, {mutations,reducer}]) => { + nonGlobby.forEach( ([slice, {mutations={},reducer={}}]) => { Object.entries(mutations).forEach(([type,mutation]) => { const localized = (payload=null,action={}) => u.updateIn( slice, mutation(payload,action) ); @@ -82,27 +90,6 @@ function buildMutations({mutations = {}, subduxes = {}}) { } -function buildMiddleware({effects={},subduxes={}},{actions}) { - return api => { - - for ( let type in actions ) { - api.dispatch[type] = (...args) => api.dispatch( actions[type](...args) ); - } - - return original_next => { - return [ - ...fp.toPairs(effects).map(([type,effect])=> { - return api => next => action => { - if( action.type !== type ) return next(action); - - return effect(api)(next)(action); - }; - }), - ...fp.map( 'middleware', subduxes ) - ].filter(x=>x).reduceRight( (next,mw) => mw(api)(next), original_next ) - }} -} - function updux(config) { const dux = {}; diff --git a/src/middleware.test.js b/src/middleware.test.js new file mode 100644 index 0000000..c8be79b --- /dev/null +++ b/src/middleware.test.js @@ -0,0 +1,111 @@ +import updux from '.'; +import u from 'updeep'; + +test( 'simple effect', () => { + + const tracer = jest.fn(); + + const store = updux({ + effects: { + foo: api => next => action => { + 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 => api => next => action => { + tracer(signature); + next(action); + }; + + const store = updux({ + effects: { + foo: tracerEffect('root'), + }, + subduxes: { + zzz: updux({effects: { + foo: tracerEffect('child'), + }}) + }, + }).createStore(); + + expect(tracer).not.toHaveBeenCalled(); + + store.dispatch({ type: 'bar' }); + + expect(tracer).not.toHaveBeenCalled(); + + store.dispatch.foo(); + + expect(tracer).toHaveBeenNthCalledWith(1,'root'); + expect(tracer).toHaveBeenNthCalledWith(2,'child'); + + + +}); + +test( '"*" effect', () => { + + const tracer = jest.fn(); + + const store = updux({ + effects: { + '*': api => next => action => { + tracer(); + next(action); + }, + }, + }).createStore(); + + expect(tracer).not.toHaveBeenCalled(); + + store.dispatch({ type: 'bar' }); + + expect(tracer).toHaveBeenCalled(); +}); + +test( 'async effect', async () => { + + function timeout(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); + } + + const tracer = jest.fn(); + + const store = 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(); +});