add mutations

typescript
Yanick Champoux 2022-08-28 12:47:24 -04:00
parent b4a96b67ea
commit 9255548326
7 changed files with 152 additions and 40 deletions

View File

@ -8,9 +8,9 @@ module.exports = {
browser: true, browser: true,
}, },
plugins: ['todo-plz', 'no-only-tests'], plugins: ['todo-plz', 'no-only-tests'],
overrides: [ overrides: [],
],
rules: { rules: {
'no-console': ['error'],
'todo-plz/ticket-ref': ['error', { pattern: 'GT[0-9]+' }], 'todo-plz/ticket-ref': ['error', { pattern: 'GT[0-9]+' }],
'no-only-tests/no-only-tests': [ 'no-only-tests/no-only-tests': [
'error', 'error',

4
TODO Normal file
View File

@ -0,0 +1,4 @@
- setMutation
- check that the mutations mutate
- documentation generator (mkdocs + jsdoc-to-markdown)
- createStore

View File

@ -1,7 +1,9 @@
import R from 'remeda'; import R from 'remeda';
import u from 'updeep';
import { createStore as reduxCreateStore } from 'redux'; import { createStore as reduxCreateStore } from 'redux';
import { buildSelectors } from './selectors.js'; import { buildSelectors } from './selectors.js';
import { buildUpreducer } from './upreducer.js';
import { action } from './actions.js'; import { action } from './actions.js';
function isActionGen(action) { function isActionGen(action) {
@ -34,7 +36,11 @@ export class Updux {
this.#addSubduxActions(slice, sub), this.#addSubduxActions(slice, sub),
); );
this.#selectors = buildSelectors( config.selectors, config.splatSelectors, this.#subduxes ); this.#selectors = buildSelectors(
config.selectors,
config.splatSelectors,
this.#subduxes,
);
} }
#addSubduxActions(_slice, subdux) { #addSubduxActions(_slice, subdux) {
@ -73,19 +79,43 @@ export class Updux {
} }
get upreducer() { get upreducer() {
return (action) => (state) => { return buildUpreducer(this.#mutations, this.#subduxes);
const mutation = this.#mutations[action.type];
if (mutation) {
state = mutation(action.payload, action)(state);
}
return state;
};
} }
/**
*
* @param {string | Function} action - Action triggering the mutation. If
* the action is a string, it has to have been previously declared for this
* updux, but if it's a function generator, it'll be automatically added to the
* updux if not already present (the idea being that making a typo on a string
* is easy, but passing a wrong function very more unlikely).
* @param {Function} mutation - Mutating function.
* @return {void}
*/
setMutation(action, mutation) { setMutation(action, mutation) {
this.#mutations[action.type] = mutation; // TODO option strict: false to make it okay to auto-create
// the actions as strings?
if (action.type) {
if (!this.#actions[action.type]) {
this.#actions[action.type] = action;
} else if (this.#actions[action.type] !== action) {
throw new Error(
`action '${action.type}' not defined for this updux or definition is different`,
);
}
action = action.type;
}
if (!this.#actions[action]) {
throw new Error(`action '${action}' is not defined`);
}
this.#mutations[action] = mutation;
}
get mutations() {
return this.#mutations;
} }
createStore(initial = undefined, enhancerGenerator = undefined) { createStore(initial = undefined, enhancerGenerator = undefined) {

View File

@ -2,25 +2,23 @@ import { test, expect } from 'vitest';
import { dux } from './Updux.js'; import { dux } from './Updux.js';
test( "basic selectors", () => { test('basic selectors', () => {
const foo = dux({ const foo = dux({
initial: { initial: {
x: 1, x: 1,
}, },
selectors: { selectors: {
getX: ({x}) => x, getX: ({ x }) => x,
}, },
subduxes: { subduxes: {
bar: { bar: {
initial: { y: 2 }, initial: { y: 2 },
selectors: { selectors: {
getY: ({y}) => y getY: ({ y }) => y,
} },
} },
} },
}); });
expect( foo.selectors.getY({bar:{y:3}} ) ).toBe(3); expect(foo.selectors.getY({ bar: { y: 3 } })).toBe(3);
});
} );

View File

@ -4,7 +4,7 @@ import u from 'updeep';
import { action } from './actions.js'; import { action } from './actions.js';
import { Updux } from './Updux.js'; import { Updux, dux } from './Updux.js';
test('set a mutation', () => { test('set a mutation', () => {
const dux = new Updux({ const dux = new Updux({
@ -17,14 +17,57 @@ test('set a mutation', () => {
}, },
}); });
dux.setMutation(dux.actions.foo, (payload, action) => dux.setMutation(dux.actions.foo, (payload, action) => {
u({ expect(payload).toEqual({ x: 'hello ' });
expect(action).toEqual(dux.actions.foo('hello '));
return u({
x: payload.x + action.type, x: payload.x + action.type,
}), });
); });
const result = dux.reducer(undefined, dux.actions.foo('hello ')); const result = dux.reducer(undefined, dux.actions.foo('hello '));
expect(result).toEqual({ expect(result).toEqual({
x: 'hello foo', x: 'hello foo',
}); });
}); });
test('mutation of a subdux', async () => {
const bar = dux({
actions: {
baz: null,
},
});
bar.setMutation('baz', () => (state) => ({ ...state, x: 1 }));
const foo = dux({
subduxes: { bar },
});
const store = foo.createStore();
store.dispatch.baz();
expect(store.getState()).toMatchObject({ bar: { x: 1 } });
});
test('strings and generators', async () => {
const actionA = action('a');
const foo = dux({
actions: {
b: null,
a: actionA,
},
});
// as a string and defined
expect(() => foo.setMutation('a', () => {})).not.toThrow();
// as a generator and defined
expect(() => foo.setMutation(actionA, () => {})).not.toThrow();
// as a string, not defined
expect(() => foo.setMutation('c', () => {})).toThrow();
foo.setMutation(action('d'), () => {});
expect(foo.actions.d).toBeTypeOf('function');
});

View File

@ -3,17 +3,19 @@ import R from 'remeda';
export function buildSelectors( export function buildSelectors(
localSelectors, localSelectors,
splatSelector = {}, splatSelector = {},
subduxes = {} subduxes = {},
) { ) {
const subSelectors = Object.entries(subduxes).map(([slice,{ selectors }]) => { const subSelectors = Object.entries(subduxes).map(
if (!selectors) return {}; ([slice, { selectors }]) => {
if (slice === '*') return {}; if (!selectors) return {};
if (slice === '*') return {};
return R.mapValues( return R.mapValues(
selectors, selectors,
(func) => (state) => func(state[slice]) (func) => (state) => func(state[slice]),
); );
}); },
);
let splat = {}; let splat = {};
@ -28,10 +30,10 @@ export function buildSelectors(
res, res,
mapValues( mapValues(
subduxes['*'].selectors, subduxes['*'].selectors,
(selector) => () => selector(value) (selector) => () => selector(value),
) ),
); );
}; };
} }
return R.mergeAll([ ...subSelectors, localSelectors, splat ]); return R.mergeAll([...subSelectors, localSelectors, splat]);
} }

35
src/upreducer.js Normal file
View File

@ -0,0 +1,35 @@
import R from 'remeda';
import u from 'updeep';
const localMutation = (mutations) => (action) => (state) => {
const mutation = mutations[action.type];
if (!mutation) return state;
return mutation(action.payload, action)(state);
};
const subMutations = (subduxes) => (action) => (state) => {
const subReducers =
Object.keys(subduxes).length > 0
? R.mapValues(subduxes, R.prop('upreducer'))
: null;
if (!subReducers) return state;
if (subReducers['*']) {
return u.updateIn('*', subReducers['*'](action), state);
}
const update = R.mapValues(subReducers, (upReducer) => upReducer(action));
return u(update, state);
};
export function buildUpreducer(mutations, subduxes) {
return (action) => (state) => {
state = subMutations(subduxes)(action)(state);
return localMutation(mutations)(action)(state);
};
}