effects
This commit is contained in:
parent
b7ada06e3c
commit
e4eff8a113
25
src/Updux.js
25
src/Updux.js
@ -1,6 +1,6 @@
|
||||
import moize from 'moize';
|
||||
import u from '@yanick/updeep';
|
||||
import { createStore as reduxCreateStore } from 'redux';
|
||||
import { createStore as reduxCreateStore, applyMiddleware } from 'redux';
|
||||
import { mapValues } from 'lodash-es';
|
||||
|
||||
import { buildInitial } from './buildInitial/index.js';
|
||||
@ -8,6 +8,7 @@ import { buildActions } from './buildActions/index.js';
|
||||
import { buildSelectors } from './buildSelectors/index.js';
|
||||
import { action } from './actions.js';
|
||||
import { buildUpreducer } from './buildUpreducer.js';
|
||||
import { buildMiddleware } from './buildMiddleware/index.js';
|
||||
|
||||
/**
|
||||
* @public
|
||||
@ -24,6 +25,7 @@ export class Updux {
|
||||
#actions = {};
|
||||
#selectors = {};
|
||||
#mutations = {};
|
||||
#effects = [];
|
||||
|
||||
constructor(config) {
|
||||
this.#initial = config.initial ?? {};
|
||||
@ -39,12 +41,26 @@ export class Updux {
|
||||
.forEach((action) => {
|
||||
throw new Error(`action '${action}' is not defined`);
|
||||
});
|
||||
|
||||
if (config.effects) {
|
||||
this.#effects = Object.entries(config.effects);
|
||||
}
|
||||
}
|
||||
|
||||
#memoInitial = moize(buildInitial);
|
||||
#memoActions = moize(buildActions);
|
||||
#memoSelectors = moize(buildSelectors);
|
||||
#memoUpreducer = moize(buildUpreducer);
|
||||
#memoMiddleware = moize(buildMiddleware);
|
||||
|
||||
get middleware() {
|
||||
return this.#memoMiddleware(
|
||||
this.#effects,
|
||||
this.actions,
|
||||
this.selectors,
|
||||
this.#subduxes
|
||||
);
|
||||
}
|
||||
|
||||
get initial() {
|
||||
return this.#memoInitial(this.#initial, this.#subduxes);
|
||||
@ -101,7 +117,12 @@ export class Updux {
|
||||
}
|
||||
|
||||
createStore() {
|
||||
const store = reduxCreateStore(this.reducer);
|
||||
|
||||
const store = reduxCreateStore(
|
||||
this.reducer,
|
||||
this.initial,
|
||||
applyMiddleware(this.middleware)
|
||||
);
|
||||
|
||||
store.actions = this.actions;
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { test } from 'tap';
|
||||
import sinon from 'sinon';
|
||||
|
||||
import { Updux } from './Updux.js';
|
||||
import { action } from './actions.js';
|
||||
@ -145,3 +146,25 @@ test('mutations', { todo: false }, async (t) => {
|
||||
alpha: { quux: 13 },
|
||||
});
|
||||
});
|
||||
|
||||
test( 'middleware', async(t) => {
|
||||
const fooEffect = sinon.fake.returns(true);
|
||||
|
||||
const dux = new Updux({
|
||||
effects: {
|
||||
foo: () => next => action => {
|
||||
fooEffect();
|
||||
next(action);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const store = dux.createStore();
|
||||
|
||||
t.notOk( fooEffect.called, 'not called yet' );
|
||||
|
||||
store.dispatch({type: 'foo'});
|
||||
|
||||
t.ok( fooEffect.called, "now it's been called" );
|
||||
|
||||
} );
|
||||
|
82
src/buildMiddleware/index.js
Normal file
82
src/buildMiddleware/index.js
Normal file
@ -0,0 +1,82 @@
|
||||
import u from '@yanick/updeep';
|
||||
import { mapValues, map, get } from 'lodash-es';
|
||||
import { Updux } from '../Updux.js';
|
||||
|
||||
const middlewareFor = (type, middleware) => (api) => (next) => (action) => {
|
||||
if (type !== '*' && action.type !== type) return next(action);
|
||||
|
||||
return middleware(api)(next)(action);
|
||||
};
|
||||
|
||||
const sliceMw = (slice, mw) => (api) => {
|
||||
const getSliceState = () => get(api.getState(), slice);
|
||||
return mw({ ...api, getState: getSliceState });
|
||||
};
|
||||
|
||||
function augmentMiddlewareApi(api, actions, selectors) {
|
||||
const getState = () => api.getState();
|
||||
const dispatch = (action) => api.dispatch(action);
|
||||
|
||||
Object.assign(
|
||||
getState,
|
||||
mapValues(selectors, (selector) => {
|
||||
return (...args) => {
|
||||
let result = selector(api.getState());
|
||||
|
||||
if (typeof result === 'function') return result(...args);
|
||||
|
||||
return result;
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
Object.assign(
|
||||
dispatch,
|
||||
mapValues(actions, (action) => {
|
||||
return (...args) => api.dispatch(action(...args));
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
...api,
|
||||
getState,
|
||||
dispatch,
|
||||
actions,
|
||||
selectors,
|
||||
};
|
||||
}
|
||||
|
||||
export const effectToMiddleware = (effect, actions, selectors) => {
|
||||
let mw = effect;
|
||||
let action = '*';
|
||||
|
||||
if (Array.isArray(effect)) {
|
||||
action = effect[0];
|
||||
mw = effect[1];
|
||||
mw = middlewareFor(action, mw);
|
||||
}
|
||||
|
||||
return (api) => mw(augmentMiddlewareApi(api, actions, selectors));
|
||||
};
|
||||
|
||||
const composeMw = (mws) => (api) => (original_next) =>
|
||||
mws.reduceRight((next, mw) => mw(api)(next), original_next);
|
||||
|
||||
export function buildMiddleware(
|
||||
effects = [],
|
||||
actions = {},
|
||||
selectors = {},
|
||||
sub = {}
|
||||
) {
|
||||
let inner = map(sub, ({ middleware }, slice) =>
|
||||
middleware ? sliceMw(slice, middleware) : undefined
|
||||
).filter((x) => x);
|
||||
|
||||
const local = effects.map((effect) =>
|
||||
effectToMiddleware(effect, actions, selectors)
|
||||
);
|
||||
|
||||
const mws = [...local, ...inner];
|
||||
|
||||
return composeMw(mws);
|
||||
}
|
120
src/buildMiddleware/test.js
Normal file
120
src/buildMiddleware/test.js
Normal file
@ -0,0 +1,120 @@
|
||||
import { test } from 'tap';
|
||||
import sinon from 'sinon';
|
||||
|
||||
import { buildMiddleware } from './index.js';
|
||||
import { action } from '../actions.js';
|
||||
|
||||
test('single effect', async (t) => {
|
||||
const effect = (api) => (next) => (action) => {
|
||||
t.same(action, { type: 'foo' });
|
||||
t.has(api, {
|
||||
actions: {},
|
||||
selectors: {},
|
||||
});
|
||||
next();
|
||||
};
|
||||
|
||||
const middleware = buildMiddleware([effect]);
|
||||
|
||||
await new Promise((resolve) => {
|
||||
middleware({})(resolve)({ type: 'foo' });
|
||||
});
|
||||
});
|
||||
|
||||
test('augmented api', async (t) => {
|
||||
const effect = (api) => (next) => async (action) => {
|
||||
await t.test('selectors', async (t) => {
|
||||
t.same(api.getState.getB(), 1);
|
||||
t.same(api.selectors.getB({ a: { b: 2 } }), 2);
|
||||
|
||||
t.same(api.getState(), { a: { b: 1 } });
|
||||
});
|
||||
|
||||
await t.test('dispatch', async (t) => {
|
||||
t.same(api.actions.actionOne(), { type: 'actionOne' });
|
||||
api.dispatch.actionOne('the payload');
|
||||
});
|
||||
|
||||
next();
|
||||
};
|
||||
|
||||
const middleware = buildMiddleware(
|
||||
[effect],
|
||||
{
|
||||
actionOne: action('actionOne'),
|
||||
},
|
||||
{
|
||||
getB: (state) => state.a.b,
|
||||
}
|
||||
);
|
||||
|
||||
const getState = sinon.fake.returns({ a: { b: 1 } });
|
||||
const dispatch = sinon.fake.returns();
|
||||
|
||||
await new Promise((resolve) => {
|
||||
middleware({
|
||||
getState,
|
||||
dispatch,
|
||||
})(resolve)({ type: 'foo' });
|
||||
});
|
||||
|
||||
t.ok(dispatch.calledOnce);
|
||||
|
||||
t.ok(dispatch.calledWith({ type: 'actionOne', payload: 'the payload' }));
|
||||
});
|
||||
|
||||
test('subduxes', async (t) => {
|
||||
const effect1 = (api) => (next) => (action) => {
|
||||
next({
|
||||
type: action.type,
|
||||
payload: [...action.payload, 1],
|
||||
});
|
||||
};
|
||||
const effect2 = (api) => (next) => (action) => {
|
||||
next({
|
||||
type: action.type,
|
||||
payload: [...action.payload, 2],
|
||||
});
|
||||
};
|
||||
const effect3 = (api) => (next) => (action) => {
|
||||
next({
|
||||
type: action.type,
|
||||
payload: [...action.payload, 3],
|
||||
});
|
||||
};
|
||||
|
||||
const mw = buildMiddleware(
|
||||
[effect1],
|
||||
{},
|
||||
{},
|
||||
{
|
||||
a: { middleware: effect2 },
|
||||
b: { middleware: effect3 },
|
||||
}
|
||||
);
|
||||
|
||||
mw({})(({ payload }) => {
|
||||
t.same(payload, [1, 2, 3]);
|
||||
})({ type: 'foo', payload: [] });
|
||||
|
||||
t.test('api for subduxes', async (t) => {
|
||||
const effect = (api) => (next) => (action) => {
|
||||
t.same(api.getState(), 3);
|
||||
next();
|
||||
};
|
||||
|
||||
const mwInner = buildMiddleware([effect]);
|
||||
const mwOuter = buildMiddleware(
|
||||
[],
|
||||
{},
|
||||
{},
|
||||
{ alpha: { middleware: mwInner } }
|
||||
);
|
||||
|
||||
await new Promise((resolve) => {
|
||||
mwOuter({
|
||||
getState: () => ({ alpha: 3 }),
|
||||
})(resolve)({ type: 'foo' });
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue
Block a user