This commit is contained in:
Yanick Champoux 2022-08-24 21:19:27 -04:00
parent bb5218d2f6
commit 3aac2e092e
20 changed files with 132 additions and 63 deletions

View File

@ -63,7 +63,7 @@ import Updux from 'updux';
const updux = new Updux({ const updux = new Updux({
initial: { counter: 0 }, initial: { counter: 0 },
mutations: { mutations: {
add: (inc=1) => state => { counter: counter + inc } add: (inc=1) => state => ({ counter: state.counter + inc })
} }
}); });
@ -74,7 +74,7 @@ Converting it to Immer would look like:
``` ```
import Updux from 'updux'; import Updux from 'updux';
import { produce } from 'Immer'; import { produce } from 'immer';
const updux = new Updux({ const updux = new Updux({
initial: { counter: 0 }, initial: { counter: 0 },
@ -91,7 +91,7 @@ can be used to wrap all mutations with it:
``` ```
import Updux from 'updux'; import Updux from 'updux';
import { produce } from 'Immer'; import { produce } from 'immer';
const updux = new Updux({ const updux = new Updux({
initial: { counter: 0 }, initial: { counter: 0 },

View File

@ -1,7 +1,9 @@
{ {
"type": "module",
"types": "./dist",
"dependencies": { "dependencies": {
"@yanick/updeep": "link:../updeep", "@yanick/updeep": "link:../updeep",
"lodash": "^4.17.15", "lodash": "^4.17.21",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"moize": "^6.1.0", "moize": "^6.1.0",
"redux": "^4.0.5", "redux": "^4.0.5",
@ -39,10 +41,10 @@
"ts-node": "^8.10.2", "ts-node": "^8.10.2",
"tsd": "^0.17.0", "tsd": "^0.17.0",
"typedoc": "0.22.5", "typedoc": "0.22.5",
"typescript": "^4.4.3" "typescript": "^4.4.4"
}, },
"license": "MIT", "license": "MIT",
"main": "src/index.js", "main": "dist/index.js",
"name": "updux", "name": "updux",
"description": "Updeep-friendly Redux helper framework", "description": "Updeep-friendly Redux helper framework",
"scripts": { "scripts": {
@ -66,7 +68,6 @@
"url": "https://github.com/yanick/updux/issues" "url": "https://github.com/yanick/updux/issues"
}, },
"homepage": "https://github.com/yanick/updux#readme", "homepage": "https://github.com/yanick/updux#readme",
"types": "./dist/index.d.ts",
"prettier": { "prettier": {
"tabWidth": 4, "tabWidth": 4,
"singleQuote": true, "singleQuote": true,

View File

@ -1,5 +1,5 @@
import { Updux } from './Updux'; import { Updux } from './Updux.js';
import { action } from './actions'; import { action } from './actions.js';
test('basic state', () => { test('basic state', () => {
const alpha = new Updux({ const alpha = new Updux({
@ -96,8 +96,8 @@ test('basic selectors', () => {
test('mutations', () => { test('mutations', () => {
const alpha = new Updux({ const alpha = new Updux({
initial: { quux: 3 }, initial: { quux: 3 },
actions: { add: (x:number)=>x },
}); });
alpha.setAction('add');
alpha.setMutation('add', (toAdd) => (state) => ({ alpha.setMutation('add', (toAdd) => (state) => ({
quux: state.quux + toAdd, quux: state.quux + toAdd,
})); }));
@ -108,8 +108,10 @@ test('mutations', () => {
bar: 4, bar: 4,
}, },
subduxes: { alpha }, subduxes: { alpha },
actions: {
subtract: ()=>{},
}
}); });
dux.setAction('subtract');
dux.setMutation('add', (toAdd) => (state) => ({ dux.setMutation('add', (toAdd) => (state) => ({
...state, ...state,
foo: state.foo + toAdd, foo: state.foo + toAdd,

View File

@ -1,15 +1,19 @@
/* TODO change * for leftovers to +, change subscriptions to reactions */ /* TODO change * for leftovers to +, change subscriptions to reactions */
import moize from 'moize'; import moize from 'moize';
import u from '@yanick/updeep'; import u from 'updeep';
import { createStore as reduxCreateStore, applyMiddleware } from 'redux'; import { createStore as reduxCreateStore, applyMiddleware } from 'redux';
import { get, map, mapValues, merge, difference } from 'lodash'; import { get, map, mapValues, merge, difference } from 'lodash-es';
import { buildInitial } from './buildInitial'; import { buildInitial } from './buildInitial/index.js';
import { buildActions } from './buildActions'; import { buildActions } from './buildActions/index.js';
import { buildSelectors } from './buildSelectors'; import { buildSelectors } from './buildSelectors/index.js';
import { action } from './actions'; import { action } from './actions.js';
import { buildUpreducer } from './buildUpreducer'; import { buildUpreducer } from './buildUpreducer.js';
import { buildMiddleware, augmentMiddlewareApi } from './buildMiddleware'; import {
buildMiddleware,
augmentMiddlewareApi,
effectToMiddleware,
} from './buildMiddleware/index.js';
import { import {
AggregateDuxActions, AggregateDuxActions,
@ -18,7 +22,9 @@ import {
ItemsOf, ItemsOf,
Reducer, Reducer,
Upreducer, Upreducer,
} from './types'; } from './types.js';
type Mutation<TState,TAction extends { payload?: any }> = (payload:TAction['payload'], action:TAction) => (state: TState) => TState;
/** /**
* Configuration object typically passed to the constructor of the class Updux. * Configuration object typically passed to the constructor of the class Updux.
@ -99,6 +105,8 @@ export interface UpduxConfig<
AggregateDuxState<TState, TSubduxes>, AggregateDuxState<TState, TSubduxes>,
ItemsOf<AggregateDuxActions<TActions, TSubduxes>> ItemsOf<AggregateDuxActions<TActions, TSubduxes>>
>; >;
middlewareWrapper?: Function;
} }
export class Updux< export class Updux<
@ -121,7 +129,11 @@ export class Updux<
#mappedReaction = undefined; #mappedReaction = undefined;
#upreducerWrapper = undefined; #upreducerWrapper = undefined;
constructor(config: UpduxConfig<TState, TActions, TSelectors, TSubduxes>) { #middlewareWrapper = undefined;
constructor(
config: UpduxConfig<TState, TActions, TSelectors, TSubduxes>
) {
this.#initial = config.initial ?? {}; this.#initial = config.initial ?? {};
this.#subduxes = config.subduxes ?? {}; this.#subduxes = config.subduxes ?? {};
@ -165,6 +177,8 @@ export class Updux<
this.#mappedReaction = config.mappedReaction; this.#mappedReaction = config.mappedReaction;
this.#upreducerWrapper = config.upreducerWrapper; this.#upreducerWrapper = config.upreducerWrapper;
this.#middlewareWrapper = config.middlewareWrapper;
} }
#memoInitial = moize(buildInitial); #memoInitial = moize(buildInitial);
@ -185,10 +199,16 @@ export class Updux<
this.#effects, this.#effects,
this.actions, this.actions,
this.selectors, this.selectors,
this.#subduxes this.#subduxes,
this.#middlewareWrapper,
this
); );
} }
setMiddlewareWrapper(wrapper: Function) {
this.#middlewareWrapper = wrapper;
}
/** @member { unknown } */ /** @member { unknown } */
get initial(): AggregateDuxState<TState, TSubduxes> { get initial(): AggregateDuxState<TState, TSubduxes> {
return this.#memoInitial(this.#initial, this.#subduxes); return this.#memoInitial(this.#initial, this.#subduxes);
@ -206,6 +226,8 @@ export class Updux<
); );
} }
get subduxes() { return this.#subduxes }
get upreducer(): Upreducer< get upreducer(): Upreducer<
AggregateDuxState<TState, TSubduxes>, AggregateDuxState<TState, TSubduxes>,
ItemsOf<AggregateDuxActions<TActions, TSubduxes>> ItemsOf<AggregateDuxActions<TActions, TSubduxes>>
@ -229,6 +251,11 @@ export class Updux<
this.#reactions = [...this.#reactions, subscription]; this.#reactions = [...this.#reactions, subscription];
} }
addReaction(reaction) {
this.#reactions = [...this.#reactions, reaction];
}
setAction(type, payloadFunc?: (...args: any) => any) { setAction(type, payloadFunc?: (...args: any) => any) {
const theAction = action(type, payloadFunc); const theAction = action(type, payloadFunc);
@ -246,7 +273,8 @@ export class Updux<
return func; return func;
} }
setMutation(name, mutation) { setMutation<TAction extends keyof AggregateDuxActions<TActions,TSubduxes>>(name: TAction, mutation: Mutation<AggregateDuxState<TState, TSubduxes>,
ReturnType<AggregateDuxActions<TActions,TSubduxes>[TAction]>>) {
if (typeof name === 'function') name = name.type; if (typeof name === 'function') name = name.type;
this.#mutations = { this.#mutations = {
@ -257,11 +285,15 @@ export class Updux<
return mutation; return mutation;
} }
addEffect(action, effect) { addEffect<TType, E>(action: TType, effect: E): E {
this.#effects = [...this.#effects, [action, effect]]; this.#effects = [...this.#effects, [action, effect]];
return effect; return effect;
} }
augmentMiddlewareApi(api) {
return augmentMiddlewareApi(api, this.actions, this.selectors);
}
splatSubscriber(store, inner, splatReaction) { splatSubscriber(store, inner, splatReaction) {
const cache = {}; const cache = {};
@ -366,9 +398,12 @@ export class Updux<
return { return {
unsub: () => results.forEach(({ unsub }) => unsub()), unsub: () => results.forEach(({ unsub }) => unsub()),
subscriber: () => results.forEach(({ subscriber }) => subscriber()), subscriber: () =>
results.forEach(({ subscriber }) => subscriber()),
subscriberRaw: (...args) => subscriberRaw: (...args) =>
results.forEach(({ subscriberRaw }) => subscriberRaw(...args)), results.forEach(({ subscriberRaw }) =>
subscriberRaw(...args)
),
}; };
} }
@ -415,4 +450,8 @@ export class Updux<
return store; return store;
} }
effectToMiddleware(effect) {
return effectToMiddleware(effect, this.actions, this.selectors);
}
} }

View File

@ -1,13 +0,0 @@
import { action } from './actions';
test('action generators', () => {
const foo = action('foo');
expect(foo.type).toEqual('foo');
expect(foo()).toMatchObject({ type: 'foo' });
const bar = action('bar');
expect(bar.type).toEqual('bar');
expect(bar()).toMatchObject({ type: 'bar' });
});

34
src/actions.test.ts Normal file
View File

@ -0,0 +1,34 @@
import { action } from './actions.js';
import u from 'updeep';
test('action generators', () => {
const foo = action('foo');
expect(foo.type).toEqual('foo');
expect(foo()).toMatchObject({ type: 'foo' });
const bar = action('bar');
expect(bar.type).toEqual('bar');
expect(bar()).toMatchObject({ type: 'bar' });
const action3 = action('a3', (x:number) => x);
expect(action3(12)).toMatchObject({
type: 'a3',
payload: 12,
});
const action4 = action('a4', undefined, u.updateIn('meta.x','yay'));
expect(action4(13)).toMatchObject({
type: 'a4',
payload: 13,
meta: {x:'yay'},
});
expect(action4.type).toEqual('a4');
});

View File

@ -37,7 +37,7 @@ export function action<
payloadFunction?: TPayload, payloadFunction?: TPayload,
transformer?: Function transformer?: Function
): ActionGenerator<TType, TPayload> { ): ActionGenerator<TType, TPayload> {
const generator = function (...payloadArg) { let generator : any= function (...payloadArg) {
const result: Action = { type }; const result: Action = { type };
if (payloadFunction) { if (payloadFunction) {
@ -49,11 +49,12 @@ export function action<
return result; return result;
}; };
if(transformer) {
const orig = generator;
generator = (...args: any) => transformer(orig(...args), args);
}
generator.type = type; generator.type = type;
return ( return generator;
transformer
? (...args: any) => transformer(generator(...args), args)
: generator
) as any;
} }

View File

@ -1,4 +1,5 @@
import { isPlainObject, mapValues } from 'lodash'; import { mapValues } from 'lodash-es';
import isPlainObject from 'lodash/isPlainObject.js';
import u from 'updeep'; import u from 'updeep';
export function buildInitial(initial, subduxes = {}) { export function buildInitial(initial, subduxes = {}) {

View File

@ -1,4 +1,4 @@
import { buildInitial } from '.'; import { buildInitial } from './index.js';
test('basic', () => { test('basic', () => {
expect(buildInitial({ a: 1 }, { b: { initial: { c: 2 } } })).toMatchObject({ expect(buildInitial({ a: 1 }, { b: { initial: { c: 2 } } })).toMatchObject({

View File

@ -1,6 +1,5 @@
import u from 'updeep'; import u from 'updeep';
import { mapValues, map, get } from 'lodash'; import { mapValues, map, get } from 'lodash-es';
import { Updux } from '../Updux.js';
const middlewareFor = (type, middleware) => (api) => (next) => (action) => { const middlewareFor = (type, middleware) => (api) => (next) => (action) => {
if (type !== '*' && action.type !== type) return next(action); if (type !== '*' && action.type !== type) return next(action);
@ -66,7 +65,9 @@ export function buildMiddleware(
effects = [], effects = [],
actions = {}, actions = {},
selectors = {}, selectors = {},
sub = {} sub = {},
wrapper = undefined,
dux = undefined,
) { ) {
let inner = map(sub, ({ middleware }, slice) => let inner = map(sub, ({ middleware }, slice) =>
slice !== '*' && middleware ? sliceMw(slice, middleware) : undefined slice !== '*' && middleware ? sliceMw(slice, middleware) : undefined
@ -76,7 +77,9 @@ export function buildMiddleware(
effectToMiddleware(effect, actions, selectors) effectToMiddleware(effect, actions, selectors)
); );
const mws = [...local, ...inner]; let mws = [...local, ...inner];
if( wrapper ) mws = wrapper(mws,dux);
return composeMw(mws); return composeMw(mws);
} }

View File

@ -1,4 +1,4 @@
import { map, mapValues, merge } from 'lodash'; import { map, mapValues, merge } from 'lodash-es';
export function buildSelectors( export function buildSelectors(
localSelectors, localSelectors,

View File

@ -1,5 +1,5 @@
import u from 'updeep'; import u from 'updeep';
import { mapValues } from 'lodash'; import { mapValues } from 'lodash-es';
export function buildUpreducer( export function buildUpreducer(
initial, initial,

View File

@ -1,7 +1,7 @@
import u from 'updeep'; import u from 'updeep';
import add from 'lodash/fp/add.js'; import add from 'lodash/fp/add.js';
import { Updux } from '.'; import { Updux } from './index.js';
test('README.md', () => { test('README.md', () => {
const otherDux = new Updux({}); const otherDux = new Updux({});

View File

@ -1,2 +0,0 @@
export { Updux } from './Updux';
export { action } from './actions';

3
src/index.ts Normal file
View File

@ -0,0 +1,3 @@
export { Updux, UpduxConfig } from './Updux.js';
export { action } from './actions.js';
export { AggregateDuxActions, AggregateDuxState } from './types.js';

View File

@ -1,4 +1,4 @@
import { Updux } from './Updux'; import { Updux } from './Updux.js';
test('initial', () => { test('initial', () => {
const foo = new Updux({ const foo = new Updux({

View File

@ -1,4 +1,4 @@
import { difference, omit } from 'lodash'; import { difference, omit } from 'lodash-es';
import { Updux } from './Updux'; import { Updux } from './Updux';

View File

@ -1,5 +1,5 @@
import { Updux } from './Updux'; import { Updux } from './Updux.js';
import { Action, action, ActionGenerator } from './actions'; import { Action, action, ActionGenerator } from './actions.js';
import { expectAssignable, expectType, expectNotType } from 'tsd'; import { expectAssignable, expectType, expectNotType } from 'tsd';
type SimplePayload = (x: number) => number; type SimplePayload = (x: number) => number;

View File

@ -1,4 +1,4 @@
import { Action, ActionGenerator } from './actions'; import { Action, ActionGenerator } from './actions.js';
export type Dict<T> = Record<string, T>; export type Dict<T> = Record<string, T>;

View File

@ -4,9 +4,9 @@
"compilerOptions": { "compilerOptions": {
"rootDir": "src", "rootDir": "src",
"outDir": "dist", "outDir": "dist",
"target": "es2020", "target": "es2015",
"lib": ["es2020"], "lib": ["es2020"],
"module": "ES2020", "module": "esnext",
"moduleResolution": "Node", "moduleResolution": "Node",
"strict": false, "strict": false,
"sourceMap": true, "sourceMap": true,