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 moize from 'moize';
|
||||||
import u from '@yanick/updeep';
|
import u from '@yanick/updeep';
|
||||||
import { createStore as reduxCreateStore } from 'redux';
|
import { createStore as reduxCreateStore, applyMiddleware } from 'redux';
|
||||||
import { mapValues } from 'lodash-es';
|
import { mapValues } from 'lodash-es';
|
||||||
|
|
||||||
import { buildInitial } from './buildInitial/index.js';
|
import { buildInitial } from './buildInitial/index.js';
|
||||||
@ -8,6 +8,7 @@ import { buildActions } from './buildActions/index.js';
|
|||||||
import { buildSelectors } from './buildSelectors/index.js';
|
import { buildSelectors } from './buildSelectors/index.js';
|
||||||
import { action } from './actions.js';
|
import { action } from './actions.js';
|
||||||
import { buildUpreducer } from './buildUpreducer.js';
|
import { buildUpreducer } from './buildUpreducer.js';
|
||||||
|
import { buildMiddleware } from './buildMiddleware/index.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
@ -24,6 +25,7 @@ export class Updux {
|
|||||||
#actions = {};
|
#actions = {};
|
||||||
#selectors = {};
|
#selectors = {};
|
||||||
#mutations = {};
|
#mutations = {};
|
||||||
|
#effects = [];
|
||||||
|
|
||||||
constructor(config) {
|
constructor(config) {
|
||||||
this.#initial = config.initial ?? {};
|
this.#initial = config.initial ?? {};
|
||||||
@ -39,12 +41,26 @@ export class Updux {
|
|||||||
.forEach((action) => {
|
.forEach((action) => {
|
||||||
throw new Error(`action '${action}' is not defined`);
|
throw new Error(`action '${action}' is not defined`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (config.effects) {
|
||||||
|
this.#effects = Object.entries(config.effects);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#memoInitial = moize(buildInitial);
|
#memoInitial = moize(buildInitial);
|
||||||
#memoActions = moize(buildActions);
|
#memoActions = moize(buildActions);
|
||||||
#memoSelectors = moize(buildSelectors);
|
#memoSelectors = moize(buildSelectors);
|
||||||
#memoUpreducer = moize(buildUpreducer);
|
#memoUpreducer = moize(buildUpreducer);
|
||||||
|
#memoMiddleware = moize(buildMiddleware);
|
||||||
|
|
||||||
|
get middleware() {
|
||||||
|
return this.#memoMiddleware(
|
||||||
|
this.#effects,
|
||||||
|
this.actions,
|
||||||
|
this.selectors,
|
||||||
|
this.#subduxes
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
get initial() {
|
get initial() {
|
||||||
return this.#memoInitial(this.#initial, this.#subduxes);
|
return this.#memoInitial(this.#initial, this.#subduxes);
|
||||||
@ -101,7 +117,12 @@ export class Updux {
|
|||||||
}
|
}
|
||||||
|
|
||||||
createStore() {
|
createStore() {
|
||||||
const store = reduxCreateStore(this.reducer);
|
|
||||||
|
const store = reduxCreateStore(
|
||||||
|
this.reducer,
|
||||||
|
this.initial,
|
||||||
|
applyMiddleware(this.middleware)
|
||||||
|
);
|
||||||
|
|
||||||
store.actions = this.actions;
|
store.actions = this.actions;
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { test } from 'tap';
|
import { test } from 'tap';
|
||||||
|
import sinon from 'sinon';
|
||||||
|
|
||||||
import { Updux } from './Updux.js';
|
import { Updux } from './Updux.js';
|
||||||
import { action } from './actions.js';
|
import { action } from './actions.js';
|
||||||
@ -145,3 +146,25 @@ test('mutations', { todo: false }, async (t) => {
|
|||||||
alpha: { quux: 13 },
|
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