so it begins
This commit is contained in:
parent
fb2dbce81b
commit
fa0a2969b8
127
README.md
Normal file
127
README.md
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
|
||||||
|
# What's Updux?
|
||||||
|
|
||||||
|
So, I'm a fan of [Redux][]. Two days ago I discovered
|
||||||
|
[rematch][] alonside a few other frameworks built atop Redux.
|
||||||
|
|
||||||
|
It has a couple of pretty good ideas that removes some of the
|
||||||
|
boilerplate. Keeping mutations and asynchronous effects close to the
|
||||||
|
reducer definition, à la [VueX][]? Nice. Automatically infering the
|
||||||
|
actions from the said mutations and effects? Genius!
|
||||||
|
|
||||||
|
But it also enforces a flat hierarchy of reducers -- where
|
||||||
|
is the fun in that? And I'm also having a strong love for
|
||||||
|
[Updeep][], so I want reducer state updates to leverage the heck out of it.
|
||||||
|
|
||||||
|
All that to say, I had some fun yesterday and hacked a proto-lovechild
|
||||||
|
of `Rematch` and `Updeep`, with a dash of [VueX][] inspiration.
|
||||||
|
I call it... `Updux`.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
#### battle.js
|
||||||
|
|
||||||
|
```
|
||||||
|
import { updux } from 'updux';
|
||||||
|
|
||||||
|
import game from './game';
|
||||||
|
import log from './log';
|
||||||
|
import bogeys from './bogeys';
|
||||||
|
|
||||||
|
const { createStore } = updux({
|
||||||
|
reducers: { game, log, bogeys }
|
||||||
|
})
|
||||||
|
|
||||||
|
export default createStore;
|
||||||
|
```
|
||||||
|
|
||||||
|
#### game.js
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
import { updux } from 'updux';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import u from 'updeep';
|
||||||
|
|
||||||
|
import { calculateMovement } from 'game/rules';
|
||||||
|
|
||||||
|
|
||||||
|
export default updux({
|
||||||
|
initial: { game: "", players: [], turn: 0, },
|
||||||
|
mutations: {
|
||||||
|
init_game: ({game: { name, players }}) => {name, players},
|
||||||
|
play_turn: () => u({ turn: x => x+1 }),
|
||||||
|
},
|
||||||
|
effects: {
|
||||||
|
play_turn: ({getState,dispatch}) => next => action => {
|
||||||
|
|
||||||
|
const bogeys = api.getState().bogeys;
|
||||||
|
|
||||||
|
// only allow the turn to be played if
|
||||||
|
// all ships have their orders in
|
||||||
|
if( bogeys.any( bogey => ! bogey.orders ) ) return;
|
||||||
|
|
||||||
|
bogeys.forEach( bogey => {
|
||||||
|
dispatch.move( calculateMovement(bogey) )
|
||||||
|
} );
|
||||||
|
|
||||||
|
next(action);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
#### log.js
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
import { updux } from 'updux';
|
||||||
|
|
||||||
|
export default updux({
|
||||||
|
initial: [],
|
||||||
|
actions: {
|
||||||
|
'*': (payload,action) => state => [ ...state, action ],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
#### bogeys.js
|
||||||
|
|
||||||
|
```
|
||||||
|
import { updux } from 'updux';
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
export default updux({
|
||||||
|
initial: [],
|
||||||
|
mutations: {
|
||||||
|
init_game: ({bogeys}) => () => _.keyBy( bogeys, 'id' ),
|
||||||
|
move: ({position}) => u({ position }),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
#### myGame.js
|
||||||
|
|
||||||
|
```
|
||||||
|
import Battle from './battle';
|
||||||
|
|
||||||
|
const battle = Battle();
|
||||||
|
|
||||||
|
battle.dispatch.init_game({
|
||||||
|
name: 'Gemini Prime',
|
||||||
|
players: [ 'yenzie' ],
|
||||||
|
bogeys: [ { id: 'Enkidu' } ]
|
||||||
|
});
|
||||||
|
|
||||||
|
battle.dispatch.play_game();
|
||||||
|
|
||||||
|
....
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
[Redux]: https://redux.js.org
|
||||||
|
[rematch]: https://rematch.github.io
|
||||||
|
[Updeep]: https://github.com/substantial/updeep
|
||||||
|
[VueX]: https://vuex.vuejs.org/
|
12
babel.config.js
Normal file
12
babel.config.js
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
module.exports = {
|
||||||
|
presets: [
|
||||||
|
[
|
||||||
|
'@babel/preset-env',
|
||||||
|
{
|
||||||
|
targets: {
|
||||||
|
node: 'current',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
};
|
15
package.json
Normal file
15
package.json
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"name": "updux",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"main": "index.js",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/core": "^7.6.4",
|
||||||
|
"@babel/preset-env": "^7.6.3",
|
||||||
|
"babel-jest": "^24.9.0",
|
||||||
|
"jest": "^24.9.0",
|
||||||
|
"lodash": "^4.17.15",
|
||||||
|
"redux": "^4.0.4",
|
||||||
|
"updeep": "^1.2.0"
|
||||||
|
}
|
||||||
|
}
|
138
src/index.js
Normal file
138
src/index.js
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
import fp from 'lodash/fp';
|
||||||
|
import u from 'updeep';
|
||||||
|
|
||||||
|
import { createStore, applyMiddleware } from 'redux';
|
||||||
|
|
||||||
|
function actionFor(type) {
|
||||||
|
return (payload = null, meta = null) => {
|
||||||
|
return fp.pickBy(v => v !== null)({type, payload, meta});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildInitial({initial = {}, reducers = {}}) {
|
||||||
|
let state = initial;
|
||||||
|
|
||||||
|
if (fp.isPlainObject(initial)) {
|
||||||
|
initial = fp.mergeAll([
|
||||||
|
fp.mapValues(fp.getOr({}, 'initial'), reducers),
|
||||||
|
initial,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return initial;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildActions({mutations = {}, reducers = {}}) {
|
||||||
|
let actions = fp.mergeAll(fp.map(fp.getOr({}, 'actions'), reducers)) || {};
|
||||||
|
|
||||||
|
Object.keys(mutations).forEach(type => {
|
||||||
|
if (!actions[type]) {
|
||||||
|
actions[type] = actionFor(type);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return actions;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildMutations({mutations = {}, reducers = {}}) {
|
||||||
|
let subMut = {};
|
||||||
|
|
||||||
|
for (let slice in reducers) {
|
||||||
|
for (let mutation in reducers[slice].mutations) {
|
||||||
|
subMut = u(
|
||||||
|
{
|
||||||
|
[mutation]: {
|
||||||
|
[slice]: u.constant(reducers[slice].mutations[mutation]),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
subMut,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
subMut = fp.mapValues(updates => action =>
|
||||||
|
u(fp.mapValues(f => f(action))(updates)),
|
||||||
|
)(subMut);
|
||||||
|
|
||||||
|
for (let name in mutations) {
|
||||||
|
if (subMut[name]) {
|
||||||
|
const pre = subMut[name];
|
||||||
|
|
||||||
|
subMut[name] = action => state =>
|
||||||
|
mutations[name](action)(pre(action)(state));
|
||||||
|
} else {
|
||||||
|
subMut[name] = mutations[name];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return subMut;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildMiddleware({effects={},reducers={}},{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 => {
|
||||||
|
console.log(action);
|
||||||
|
|
||||||
|
if( action.type !== type ) return next(action);
|
||||||
|
|
||||||
|
return effect(api)(next)(action);
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
...fp.map( 'middleware', reducers )
|
||||||
|
].filter(x=>x).reduceRight( (next,mw) => mw(api)(next), original_next )
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updux(config) {
|
||||||
|
const dux = {};
|
||||||
|
|
||||||
|
dux.actions = buildActions(config);
|
||||||
|
|
||||||
|
dux.initial = buildInitial(config);
|
||||||
|
|
||||||
|
dux.mutations = buildMutations(config);
|
||||||
|
|
||||||
|
dux.upreducer = action => state => {
|
||||||
|
if (state === null) state = dux.initial;
|
||||||
|
|
||||||
|
const a =
|
||||||
|
dux.mutations[action.type] ||
|
||||||
|
dux.mutations['*'] ||
|
||||||
|
(() => state => state);
|
||||||
|
|
||||||
|
return a(action.payload, action)(state);
|
||||||
|
};
|
||||||
|
|
||||||
|
dux.reducer = (state, action) => {
|
||||||
|
return dux.upreducer(action)(state);
|
||||||
|
};
|
||||||
|
|
||||||
|
dux.middleware = buildMiddleware(config,dux);
|
||||||
|
|
||||||
|
dux.createStore = () => {
|
||||||
|
const store = createStore( dux.reducer, dux.initial,
|
||||||
|
applyMiddleware( dux.middleware)
|
||||||
|
);
|
||||||
|
for ( let a in dux.actions ) {
|
||||||
|
store.dispatch[a] = (...args) => {
|
||||||
|
store.dispatch(dux.actions[a](...args))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return store;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return dux;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default updux;
|
178
src/test.js
Normal file
178
src/test.js
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
import updux from '.';
|
||||||
|
|
||||||
|
test('actions from mutations', () => {
|
||||||
|
const {
|
||||||
|
actions: {foo, bar},
|
||||||
|
} = updux({
|
||||||
|
mutations: {
|
||||||
|
foo: () => x => x,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(foo()).toEqual({type: 'foo'});
|
||||||
|
|
||||||
|
expect(foo(true)).toEqual({type: 'foo', payload: true});
|
||||||
|
|
||||||
|
expect(foo({bar: 2}, {timestamp: 613})).toEqual({
|
||||||
|
type: 'foo',
|
||||||
|
payload: {bar: 2},
|
||||||
|
meta: {timestamp: 613},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('reducer', () => {
|
||||||
|
const {actions, reducer} = updux({
|
||||||
|
initial: {counter: 1},
|
||||||
|
mutations: {
|
||||||
|
inc: () => ({counter}) => ({counter: counter + 1}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
let state = reducer(null, {});
|
||||||
|
|
||||||
|
expect(state).toEqual({counter: 1});
|
||||||
|
|
||||||
|
state = reducer(state, actions.inc());
|
||||||
|
|
||||||
|
expect(state).toEqual({counter: 2});
|
||||||
|
});
|
||||||
|
|
||||||
|
test( 'sub reducers', () => {
|
||||||
|
const foo = updux({
|
||||||
|
initial: 1,
|
||||||
|
mutations: {
|
||||||
|
doFoo: () => (x) => x + 1,
|
||||||
|
doAll: () => x => x + 10,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const bar = updux({
|
||||||
|
initial: 'a',
|
||||||
|
mutations: {
|
||||||
|
doBar: () => x => x + 'a',
|
||||||
|
doAll: () => x => x + 'b',
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const { initial, actions, reducer } = updux({
|
||||||
|
reducers: {
|
||||||
|
foo, bar
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(initial).toEqual({ foo: 1, bar: 'a' });
|
||||||
|
|
||||||
|
expect(Object.keys(actions)).toHaveLength(3);
|
||||||
|
|
||||||
|
let state = reducer(null,{});
|
||||||
|
|
||||||
|
expect(state).toEqual({ foo: 1, bar: 'a' });
|
||||||
|
|
||||||
|
state = reducer(state, actions.doFoo() );
|
||||||
|
|
||||||
|
expect(state).toEqual({ foo: 2, bar: 'a' });
|
||||||
|
|
||||||
|
state = reducer(state, actions.doBar() );
|
||||||
|
|
||||||
|
expect(state).toEqual({ foo: 2, bar: 'aa' });
|
||||||
|
|
||||||
|
state = reducer(state, actions.doAll() );
|
||||||
|
|
||||||
|
expect(state).toEqual({ foo: 12, bar: 'aab' });
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
test('precedence between root and sub-reducers', () => {
|
||||||
|
const {
|
||||||
|
initial,
|
||||||
|
reducer,
|
||||||
|
actions,
|
||||||
|
} = updux({
|
||||||
|
initial: {
|
||||||
|
foo: { bar: 4 },
|
||||||
|
},
|
||||||
|
mutations: {
|
||||||
|
inc: () => state => {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
surprise: state.foo.bar
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
reducers: {
|
||||||
|
foo: updux({
|
||||||
|
initial: {
|
||||||
|
bar: 2,
|
||||||
|
quux: 3,
|
||||||
|
},
|
||||||
|
mutations: {
|
||||||
|
inc: () => state => ({...state, bar: state.bar + 1 })
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(initial).toEqual({
|
||||||
|
foo: { bar: 4, quux: 3 }
|
||||||
|
});
|
||||||
|
|
||||||
|
expect( reducer(null,actions.inc() ) ).toEqual({
|
||||||
|
foo: { bar: 5, quux: 3 }, surprise: 5
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
function timeout(ms) {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
|
||||||
|
test( 'middleware', async () => {
|
||||||
|
const {
|
||||||
|
middleware,
|
||||||
|
createStore
|
||||||
|
} = updux({
|
||||||
|
initial: "",
|
||||||
|
mutations: {
|
||||||
|
inc: (addition) => state => state + addition,
|
||||||
|
doEeet: () => state => {
|
||||||
|
console.log("up", state);
|
||||||
|
|
||||||
|
return state + 'Z';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
effects: {
|
||||||
|
doEeet: api => next => async action => {
|
||||||
|
console.log(api);
|
||||||
|
api.dispatch.inc('a');
|
||||||
|
next(action);
|
||||||
|
await timeout(1000);
|
||||||
|
api.dispatch.inc('c');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
reducers: {
|
||||||
|
foo: updux({
|
||||||
|
effects: {
|
||||||
|
doEeet: (api) => next => action => {
|
||||||
|
console.log("in foo",api);
|
||||||
|
|
||||||
|
api.dispatch({ type: 'inc', payload: 'b'});
|
||||||
|
next(action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const store = createStore();
|
||||||
|
|
||||||
|
console.log(store);
|
||||||
|
|
||||||
|
store.dispatch.doEeet();
|
||||||
|
|
||||||
|
expect(store.getState()).toEqual( 'abZ' );
|
||||||
|
|
||||||
|
await timeout(1000);
|
||||||
|
|
||||||
|
expect(store.getState()).toEqual( 'abZc' );
|
||||||
|
|
||||||
|
});
|
Loading…
Reference in New Issue
Block a user