mutations

This commit is contained in:
Yanick Champoux 2021-10-07 12:04:15 -04:00
parent 1a653fac5b
commit 1759ce16c6
10 changed files with 248 additions and 78 deletions

3
.taprc Normal file
View File

@ -0,0 +1,3 @@
coverage: false
browser: false
test-regex: src/.*\.test.js

View File

@ -1,7 +1,7 @@
{ {
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"@yanick/updeep": "../updeep", "@yanick/updeep": "link:../updeep",
"lodash": "^4.17.15", "lodash": "^4.17.15",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"moize": "^6.1.0", "moize": "^6.1.0",
@ -12,15 +12,13 @@
}, },
"devDependencies": { "devDependencies": {
"@babel/cli": "^7.8.4", "@babel/cli": "^7.8.4",
"@babel/core": "^7.8.7", "@babel/core": "^7.15.5",
"@babel/plugin-transform-modules-commonjs": "^7.15.4",
"@babel/preset-env": "^7.8.7", "@babel/preset-env": "^7.8.7",
"@types/jest": "^25.1.4",
"@types/lodash": "^4.14.149", "@types/lodash": "^4.14.149",
"@types/sinon": "^7.5.2", "@types/sinon": "^7.5.2",
"@typescript-eslint/eslint-plugin": "^2.23.0", "@typescript-eslint/eslint-plugin": "^2.23.0",
"@typescript-eslint/parser": "^2.23.0", "@typescript-eslint/parser": "^2.23.0",
"babel-jest": "^25.1.0",
"chai": "^4.3.4",
"docsify": "^4.11.2", "docsify": "^4.11.2",
"docsify-cli": "^4.4.0", "docsify-cli": "^4.4.0",
"docsify-tools": "^1.0.20", "docsify-tools": "^1.0.20",
@ -30,12 +28,11 @@
"eslint-plugin-import": "^2.20.1", "eslint-plugin-import": "^2.20.1",
"eslint-plugin-prettier": "^3.1.2", "eslint-plugin-prettier": "^3.1.2",
"glob": "^7.1.6", "glob": "^7.1.6",
"jest": "^25.1.0", "prettier": "^2.4.1",
"promake": "^3.1.3", "promake": "^3.1.3",
"sinon": "^9.0.1", "sinon": "^9.0.1",
"standard-version": "^8.0.0", "standard-version": "^8.0.0",
"tap": "15", "tap": "15",
"ts-jest": "^25.2.1",
"tsd": "^0.11.0", "tsd": "^0.11.0",
"typedoc": "0.17.7", "typedoc": "0.17.7",
"typedoc-plugin-markdown": "^2.2.17", "typedoc-plugin-markdown": "^2.2.17",

View File

@ -5,7 +5,7 @@ import { buildInitial } from './buildInitial/index.js';
import { buildActions } from './buildActions/index.js'; 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';
/** /**
* @public * @public
@ -17,36 +17,64 @@ import { action } from './actions.js';
export class Updux { export class Updux {
#initial = {}; #initial = {};
#subduxes = {}; #subduxes = {};
/** @type Record<string,Function> */
#actions = {}; #actions = {};
#selectors = {}; #selectors = {};
#mutations = {};
constructor(config) { constructor(config) {
this.#initial = config.initial ?? {}; this.#initial = config.initial ?? {};
this.#subduxes = config.subduxes ?? {}; this.#subduxes = config.subduxes ?? {};
this.#actions = config.actions ?? {}; this.#actions = config.actions ?? {};
this.#selectors = config.selectors ?? {}; this.#selectors = config.selectors ?? {};
this.#mutations = config.mutations ?? {};
Object.keys(this.#mutations)
.filter((action) => action !== '*')
.filter((action) => !this.actions.hasOwnProperty(action))
.forEach((action) => {
throw new Error(`action '${action}' is not defined`);
});
} }
#memoInitial = moize( buildInitial ); #memoInitial = moize(buildInitial);
#memoActions = moize(buildActions); #memoActions = moize(buildActions);
#memoSelectors = moize(buildSelectors); #memoSelectors = moize(buildSelectors);
#memoUpreducer = moize(buildUpreducer);
get initial() { get initial() {
return this.#memoInitial(this.#initial,this.#subduxes); return this.#memoInitial(this.#initial, this.#subduxes);
} }
/**
* @return {Record<string,Function>}
*/
get actions() { get actions() {
return this.#memoActions(this.#actions, this.#subduxes); return this.#memoActions(this.#actions, this.#subduxes);
} }
get selectors() { get selectors() {
return this.#memoSelectors(this.#selectors,this.#subduxes); return this.#memoSelectors(this.#selectors, this.#subduxes);
}
get upreducer() {
return this.#memoUpreducer(
this.initial,
this.#mutations,
this.#subduxes
);
}
get reducer() {
return (state, action) => this.upreducer(action)(state);
} }
addAction(type, payloadFunc) { addAction(type, payloadFunc) {
const theAction = action(type,payloadFunc); const theAction = action(type, payloadFunc);
this.#actions = u( { [type]: theAction }, this.#actions ); this.#actions = u({ [type]: theAction }, this.#actions);
return theAction; return theAction;
} }
@ -55,4 +83,15 @@ export class Updux {
this.#selectors[name] = func; this.#selectors[name] = func;
return func; return func;
} }
addMutation(name, mutation) {
if (typeof name === 'function') name = name.type;
this.#mutations = {
...this.#mutations,
[name]: mutation,
};
return this;
}
} }

View File

@ -17,13 +17,12 @@ test('basic state', async (t) => {
subduxes: { alpha, beta }, subduxes: { alpha, beta },
}); });
t.same(dux.initial,{ t.same(dux.initial, {
a: 1, a: 1,
b: 'two', b: 'two',
alpha: { sub: 1 }, alpha: { sub: 1 },
beta: { foo: 1 }, beta: { foo: 1 },
}); });
}); });
test('basic actions', async (t) => { test('basic actions', async (t) => {
@ -38,14 +37,14 @@ test('basic actions', async (t) => {
const dux = new Updux({ const dux = new Updux({
subduxes: { alpha, beta }, subduxes: { alpha, beta },
actions: { actions: {
bar: action('bar' ), bar: action('bar'),
}, },
}); });
t.same(Object.keys(dux.actions).sort(),['bar', 'foo']); t.same(Object.keys(dux.actions).sort(), ['bar', 'foo']);
}); });
test('addAction', async(t) => { test('addAction', async (t) => {
const dux = new Updux({ const dux = new Updux({
actions: { actions: {
bar: action('bar'), bar: action('bar'),
@ -53,15 +52,15 @@ test('addAction', async(t) => {
}); });
dux.addAction('foo'); dux.addAction('foo');
t.same(Object.keys(dux.actions).sort(),['bar', 'foo']); t.same(Object.keys(dux.actions).sort(), ['bar', 'foo']);
}); });
test('basic selectors', async(t) => { test('basic selectors', { todo: true }, async (t) => {
const alpha = new Updux({ const alpha = new Updux({
initial: { quux: 3 }, initial: { quux: 3 },
selectors: { selectors: {
getQuux: ({quux}) => quux getQuux: ({ quux }) => quux,
} },
}); });
const dux = new Updux({ const dux = new Updux({
@ -74,30 +73,36 @@ test('basic selectors', async(t) => {
getBar: ({ bar }) => bar, getBar: ({ bar }) => bar,
}, },
}); });
dux.addSelector('getFoo', (state) => state.foo) dux.addSelector('getFoo', (state) => state.foo);
dux.addSelector('getAdd', ({ foo }) => (add) => add + foo) dux.addSelector(
'getAdd',
({ foo }) =>
(add) =>
add + foo
);
dux.addAction('stuff'); dux.addAction('stuff');
t.equal(dux.selectors.getBar({ bar: 3 }), 3); t.equal(dux.selectors.getBar({ bar: 3 }), 3);
t.equal(dux.selectors.getFoo({ foo: 3 }) , 3); t.equal(dux.selectors.getFoo({ foo: 3 }), 3);
t.equal(alpha.selectors.getQuux({ quux: 1 }),1); t.equal(alpha.selectors.getQuux({ quux: 1 }), 1);
t.equal(dux.selectors.getQuux({ alpha: { quux: 1 } }) ,1); t.equal(dux.selectors.getQuux({ alpha: { quux: 1 } }), 1);
const store = dux.createStore(); const store = dux.createStore();
t.equal(store.selectors.getFoo(),1); t.equal(store.selectors.getFoo(), 1);
t.equal(store.selectors.getQuux(),3); t.equal(store.selectors.getQuux(), 3);
t.equal(store.selectors.getAdd(7),8); t.equal(store.selectors.getAdd(7), 8);
}); });
test('mutations', async () => { test('mutations', { todo: true }, async () => {
const alpha = new Updux({ const alpha = new Updux({
initial: { quux: 3 }, initial: { quux: 3 },
}) });
.addAction( 'add' ) alpha.addAction('add');
.addMutation( 'add', ( toAdd ) => (state) => ({ quux: state.quux + toAdd }) ); alpha.addMutation('add', (toAdd) => (state) => ({
quux: state.quux + toAdd,
}));
const dux = new Updux({ const dux = new Updux({
initial: { initial: {
@ -105,31 +110,38 @@ test('mutations', async () => {
bar: 4, bar: 4,
}, },
subduxes: { alpha }, subduxes: { alpha },
}) });
.addAction( 'subtract' ) dux.addAction('subtract');
.addMutation( 'add', toAdd => state => ({ ...state, foo: state.foo + toAdd }) ) dux.addMutation('add', (toAdd) => (state) => ({
.addMutation( 'subtract', toSubtract => state => ({ ...state, bar: state.bar - toSubtract }) ); ...state,
foo: state.foo + toAdd,
}));
dux.addMutation('subtract', (toSubtract) => (state) => ({
...state,
bar: state.bar - toSubtract,
}));
const store = dux.createStore(); const store = dux.createStore();
t.same(store.getState(),{ t.same(store.getState(), {
foo: 1, bar: 4, alpha: { quux: 3 }, foo: 1,
}) bar: 4,
alpha: { quux: 3 },
});
store.dispatch.add(10); store.dispatch.add(10);
t.same(store.getState(), { t.same(store.getState(), {
foo: 11, bar: 4, alpha: { quux: 13 }, foo: 11,
}) bar: 4,
alpha: { quux: 13 },
});
store.dispatch.subtract(20); store.dispatch.subtract(20);
t.same(store.getState(), { t.same(store.getState(), {
foo: 11, bar: -16, alpha: { quux: 13 }, foo: 11,
}) bar: -16,
alpha: { quux: 13 },
});
}); });

View File

@ -1,17 +1,11 @@
export function action(type, payloadFunction = null) {
const generator = function (payloadArg) {
export function action( type, payloadFunction = null ) {
const generator = function(payloadArg) {
const result = { type }; const result = { type };
if( payloadFunction ) if (payloadFunction) result.payload = payloadFunction(payloadArg);
result.payload = payloadFunction(payloadArg);
return result; return result;
};
}
generator.type = type; generator.type = type;

View File

@ -2,16 +2,14 @@ import { test } from 'tap';
import { action } from './actions.js'; import { action } from './actions.js';
test( 'action generators', async (t) => { test('action generators', async (t) => {
const foo = action('foo'); const foo = action('foo');
t.equal( foo.type, 'foo' ); t.equal(foo.type, 'foo');
t.same( foo(), { type: 'foo' } ); t.same(foo(), { type: 'foo' });
const bar = action('bar'); const bar = action('bar');
t.equal( bar.type, 'bar' ); t.equal(bar.type, 'bar');
t.same( bar(), { type: 'bar' } ); t.same(bar(), { type: 'bar' });
});
} )

View File

@ -1,10 +1,10 @@
export function buildActions(actions={}, subduxes={}) { export function buildActions(actions = {}, subduxes = {}) {
// priority => generics => generic subs => craft subs => creators // priority => generics => generic subs => craft subs => creators
const merged = { ...actions }; const merged = { ...actions };
Object.values(subduxes).forEach(({ actions }) => { Object.values(subduxes).forEach(({ actions }) => {
if(!actions) return; if (!actions) return;
Object.entries(actions).forEach(([type, func]) => { Object.entries(actions).forEach(([type, func]) => {
if (merged[type]) { if (merged[type]) {
if (merged[type] === func) return; if (merged[type] === func) return;
@ -15,7 +15,6 @@ export function buildActions(actions={}, subduxes={}) {
merged[type] = func; merged[type] = func;
}); });
}); });
return merged; return merged;
} }

View File

@ -3,9 +3,7 @@ import { test } from 'tap';
import { buildInitial } from './index.js'; import { buildInitial } from './index.js';
test('basic', async (t) => { test('basic', async (t) => {
t.same( t.same(buildInitial({ a: 1 }, { b: { initial: { c: 2 } } }), {
buildInitial({ a: 1 }, { b: { initial: { c: 2 } } })
,{
a: 1, a: 1,
b: { c: 3 }, b: { c: 3 },
}); });

27
src/buildUpreducer.js Normal file
View File

@ -0,0 +1,27 @@
import u from '@yanick/updeep';
import { mapValues } from 'lodash-es';
export function buildUpreducer(initial, mutations, subduxes = {}) {
const subReducers =
Object.keys(subduxes).length > 0
? mapValues(subduxes, ({ upreducer }) => upreducer)
: null;
return (action) => (state) => {
let newState = state ?? initial;
if (subReducers) {
const update = mapValues(subReducers, (upReducer) =>
upReducer(action)
);
newState = u(update, newState);
}
const a = mutations[action.type] || mutations['*'];
if (!a) return newState;
return a(action.payload, action)(newState);
};
}

103
src/mutations.test.js Normal file
View File

@ -0,0 +1,103 @@
import { test } from 'tap';
import { Updux } from './Updux.js';
import { action } from './actions.js';
test('basic', async (t) => {
const doIt = action('doIt');
const thisToo = action('thisToo');
const dux = new Updux({
initial: '',
actions: { doIt, thisToo },
mutations: {
doIt: () => () => 'bingo',
thisToo: () => () => 'straight type',
},
});
t.equal(dux.reducer(undefined, dux.actions.doIt()), 'bingo');
t.equal(dux.reducer(undefined, dux.actions.thisToo()), 'straight type');
});
test('override', async (t) => {
const foo = action('foo');
const dux = new Updux({
initial: { alpha: [] },
mutations: {
'*': (payload, action) => (state) => ({
...state,
alpha: [...state.alpha, action.type],
}),
},
subduxes: {
subbie: new Updux({
initial: 0,
actions: {
foo,
},
mutations: {
foo: () => (state) => state + 1,
},
}),
},
});
let state = [foo(), { type: 'bar' }].reduce(
(state, action) => dux.upreducer(action)(state),
undefined
);
t.match(state, {
alpha: ['foo', 'bar'],
subbie: 1,
});
});
test('order of processing', async (t) => {
const foo = action('foo');
const dux = new Updux({
initial: {
x: [],
},
mutations: {
foo:
() =>
({ x }) => ({ x: [...x, 'main'] }),
},
subduxes: {
x: new Updux({
actions: { foo },
mutations: {
foo: () => (state) => [...state, 'subdux'],
},
}),
},
});
t.same(dux.reducer(undefined, foo()), { x: ['subdux', 'main'] });
});
test('addMutation', async (t) => {
const foo = action('foo');
const dux = new Updux({
initial: '',
});
t.equal(dux.reducer(undefined, foo()), '', 'noop');
dux.addMutation('foo', () => () => 'foo');
t.equal(dux.reducer(undefined, foo()), 'foo', 'foo was added');
await t.test('name as function', async (t) => {
const bar = action('bar');
dux.addMutation(bar, () => () => 'bar');
t.equal(dux.reducer(undefined, bar()), 'bar', 'bar was added');
});
});