From 1759ce16c6d971c5a89252235e468faf7187f272 Mon Sep 17 00:00:00 2001 From: Yanick Champoux Date: Thu, 7 Oct 2021 12:04:15 -0400 Subject: [PATCH] mutations --- .taprc | 3 ++ package.json | 11 ++-- src/Updux.js | 55 +++++++++++++++++--- src/Updux.test.js | 88 ++++++++++++++++++-------------- src/actions.js | 16 ++---- src/actions.test.js | 14 +++--- src/buildActions/index.js | 5 +- src/buildInitial/test.js | 4 +- src/buildUpreducer.js | 27 ++++++++++ src/mutations.test.js | 103 ++++++++++++++++++++++++++++++++++++++ 10 files changed, 248 insertions(+), 78 deletions(-) create mode 100644 .taprc create mode 100644 src/buildUpreducer.js create mode 100644 src/mutations.test.js diff --git a/.taprc b/.taprc new file mode 100644 index 0000000..fd894de --- /dev/null +++ b/.taprc @@ -0,0 +1,3 @@ +coverage: false +browser: false +test-regex: src/.*\.test.js diff --git a/package.json b/package.json index 88677b2..0da9286 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/Updux.js b/src/Updux.js index 9ea46e2..f46a1cc 100644 --- a/src/Updux.js +++ b/src/Updux.js @@ -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 */ #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} + */ 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; + } } diff --git a/src/Updux.test.js b/src/Updux.test.js index 1445261..0d371ef 100644 --- a/src/Updux.test.js +++ b/src/Updux.test.js @@ -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 }, + }); }); diff --git a/src/actions.js b/src/actions.js index 99cf8a3..dd6b6a6 100644 --- a/src/actions.js +++ b/src/actions.js @@ -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; diff --git a/src/actions.test.js b/src/actions.test.js index 7c58bf4..88baf25 100644 --- a/src/actions.test.js +++ b/src/actions.test.js @@ -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' }); +}); diff --git a/src/buildActions/index.js b/src/buildActions/index.js index 9aa4dff..dfc9b48 100644 --- a/src/buildActions/index.js +++ b/src/buildActions/index.js @@ -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; } diff --git a/src/buildInitial/test.js b/src/buildInitial/test.js index 5977827..aea9ab8 100644 --- a/src/buildInitial/test.js +++ b/src/buildInitial/test.js @@ -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 }, }); diff --git a/src/buildUpreducer.js b/src/buildUpreducer.js new file mode 100644 index 0000000..e2b26b3 --- /dev/null +++ b/src/buildUpreducer.js @@ -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); + }; +} diff --git a/src/mutations.test.js b/src/mutations.test.js new file mode 100644 index 0000000..5a31c31 --- /dev/null +++ b/src/mutations.test.js @@ -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'); + }); +});