typescript
Yanick Champoux 2021-10-07 15:08:21 -04:00
parent b7ada06e3c
commit e4eff8a113
4 changed files with 248 additions and 2 deletions

View File

@ -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;

View File

@ -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" );
} );

View 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
View 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' });
});
});
});