mutations

typescript
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",
"dependencies": {
"@yanick/updeep": "../updeep",
"@yanick/updeep": "link:../updeep",
"lodash": "^4.17.15",
"lodash-es": "^4.17.21",
"moize": "^6.1.0",
@ -12,15 +12,13 @@
},
"devDependencies": {
"@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",
"@types/jest": "^25.1.4",
"@types/lodash": "^4.14.149",
"@types/sinon": "^7.5.2",
"@typescript-eslint/eslint-plugin": "^2.23.0",
"@typescript-eslint/parser": "^2.23.0",
"babel-jest": "^25.1.0",
"chai": "^4.3.4",
"docsify": "^4.11.2",
"docsify-cli": "^4.4.0",
"docsify-tools": "^1.0.20",
@ -30,12 +28,11 @@
"eslint-plugin-import": "^2.20.1",
"eslint-plugin-prettier": "^3.1.2",
"glob": "^7.1.6",
"jest": "^25.1.0",
"prettier": "^2.4.1",
"promake": "^3.1.3",
"sinon": "^9.0.1",
"standard-version": "^8.0.0",
"tap": "15",
"ts-jest": "^25.2.1",
"tsd": "^0.11.0",
"typedoc": "0.17.7",
"typedoc-plugin-markdown": "^2.2.17",

View File

@ -2,10 +2,10 @@ import moize from 'moize';
import u from '@yanick/updeep';
import { buildInitial } from './buildInitial/index.js';
import { buildActions } from './buildActions/index.js';
import { buildSelectors } from './buildSelectors/index.js';
import { buildActions } from './buildActions/index.js';
import { buildSelectors } from './buildSelectors/index.js';
import { action } from './actions.js';
import { buildUpreducer } from './buildUpreducer.js';
/**
* @public
@ -17,36 +17,64 @@ import { action } from './actions.js';
export class Updux {
#initial = {};
#subduxes = {};
/** @type Record<string,Function> */
#actions = {};
#selectors = {};
#mutations = {};
constructor(config) {
this.#initial = config.initial ?? {};
this.#subduxes = config.subduxes ?? {};
this.#actions = config.actions ?? {};
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);
#memoSelectors = moize(buildSelectors);
#memoUpreducer = moize(buildUpreducer);
get initial() {
return this.#memoInitial(this.#initial,this.#subduxes);
return this.#memoInitial(this.#initial, this.#subduxes);
}
/**
* @return {Record<string,Function>}
*/
get actions() {
return this.#memoActions(this.#actions, this.#subduxes);
}
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) {
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;
}
@ -55,4 +83,15 @@ export class Updux {
this.#selectors[name] = 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 },
});
t.same(dux.initial,{
t.same(dux.initial, {
a: 1,
b: 'two',
alpha: { sub: 1 },
beta: { foo: 1 },
});
});
test('basic actions', async (t) => {
@ -38,14 +37,14 @@ test('basic actions', async (t) => {
const dux = new Updux({
subduxes: { alpha, beta },
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({
actions: {
bar: action('bar'),
@ -53,15 +52,15 @@ test('addAction', async(t) => {
});
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({
initial: { quux: 3 },
selectors: {
getQuux: ({quux}) => quux
}
getQuux: ({ quux }) => quux,
},
});
const dux = new Updux({
@ -74,30 +73,36 @@ test('basic selectors', async(t) => {
getBar: ({ bar }) => bar,
},
});
dux.addSelector('getFoo', (state) => state.foo)
dux.addSelector('getAdd', ({ foo }) => (add) => add + foo)
dux.addSelector('getFoo', (state) => state.foo);
dux.addSelector(
'getAdd',
({ foo }) =>
(add) =>
add + foo
);
dux.addAction('stuff');
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(dux.selectors.getQuux({ alpha: { quux: 1 } }) ,1);
t.equal(alpha.selectors.getQuux({ quux: 1 }), 1);
t.equal(dux.selectors.getQuux({ alpha: { quux: 1 } }), 1);
const store = dux.createStore();
t.equal(store.selectors.getFoo(),1);
t.equal(store.selectors.getQuux(),3);
t.equal(store.selectors.getAdd(7),8);
t.equal(store.selectors.getFoo(), 1);
t.equal(store.selectors.getQuux(), 3);
t.equal(store.selectors.getAdd(7), 8);
});
test('mutations', async () => {
test('mutations', { todo: true }, async () => {
const alpha = new Updux({
initial: { quux: 3 },
})
.addAction( 'add' )
.addMutation( 'add', ( toAdd ) => (state) => ({ quux: state.quux + toAdd }) );
});
alpha.addAction('add');
alpha.addMutation('add', (toAdd) => (state) => ({
quux: state.quux + toAdd,
}));
const dux = new Updux({
initial: {
@ -105,31 +110,38 @@ test('mutations', async () => {
bar: 4,
},
subduxes: { alpha },
})
.addAction( 'subtract' )
.addMutation( 'add', toAdd => state => ({ ...state, foo: state.foo + toAdd }) )
.addMutation( 'subtract', toSubtract => state => ({ ...state, bar: state.bar - toSubtract }) );
});
dux.addAction('subtract');
dux.addMutation('add', (toAdd) => (state) => ({
...state,
foo: state.foo + toAdd,
}));
dux.addMutation('subtract', (toSubtract) => (state) => ({
...state,
bar: state.bar - toSubtract,
}));
const store = dux.createStore();
t.same(store.getState(),{
foo: 1, bar: 4, alpha: { quux: 3 },
})
t.same(store.getState(), {
foo: 1,
bar: 4,
alpha: { quux: 3 },
});
store.dispatch.add(10);
t.same(store.getState(), {
foo: 11, bar: 4, alpha: { quux: 13 },
})
foo: 11,
bar: 4,
alpha: { quux: 13 },
});
store.dispatch.subtract(20);
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 };
if( payloadFunction )
result.payload = payloadFunction(payloadArg);
if (payloadFunction) result.payload = payloadFunction(payloadArg);
return result;
}
return result;
};
generator.type = type;

View File

@ -2,16 +2,14 @@ import { test } from 'tap';
import { action } from './actions.js';
test( 'action generators', async (t) => {
test('action generators', async (t) => {
const foo = action('foo');
t.equal( foo.type, 'foo' );
t.same( foo(), { type: 'foo' } );
t.equal(foo.type, 'foo');
t.same(foo(), { type: 'foo' });
const bar = action('bar');
t.equal( bar.type, 'bar' );
t.same( bar(), { type: 'bar' } );
} )
t.equal(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
const merged = { ...actions };
Object.values(subduxes).forEach(({ actions }) => {
if(!actions) return;
if (!actions) return;
Object.entries(actions).forEach(([type, func]) => {
if (merged[type]) {
if (merged[type] === func) return;
@ -15,7 +15,6 @@ export function buildActions(actions={}, subduxes={}) {
merged[type] = func;
});
});
return merged;
}

View File

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