This commit is contained in:
Yanick Champoux 2023-04-21 14:01:12 -04:00
parent bb0bc14873
commit d405c90a0d
9 changed files with 132 additions and 122 deletions

View File

@ -2,11 +2,23 @@
version: '3' version: '3'
vars:
PARENT_BRANCH: main
tasks: tasks:
build: tsc build: tsc
checks: checks:
deps: [test, build] deps: [lint, test, build]
integrate:
deps: [checks]
cmds:
- git is-clean
# did we had tests?
- git diff-ls {{.PARENT_BRANCH}} | grep test
- git checkout {{.PARENT_BRANCH}}
- git weld -
test: vitest run src test: vitest run src
test:dev: vitest src test:dev: vitest src

View File

@ -1,15 +1,7 @@
import { test, expect } from 'vitest'; import { test, expect } from 'vitest';
import Updux from './Updux'; import Updux from './Updux.js';
test('subdux idempotency', () => { test('subdux idempotency', () => {
// const c = new Updux({
// initialState: 2,
// });
// const b = new Updux({
// subduxes: {
// c,
// },
// });
const foo = new Updux({ const foo = new Updux({
subduxes: { subduxes: {
a: new Updux({ initialState: 2 }), a: new Updux({ initialState: 2 }),
@ -18,21 +10,4 @@ test('subdux idempotency', () => {
let fooState = foo.reducer(undefined, { type: 'noop' }); let fooState = foo.reducer(undefined, { type: 'noop' });
expect(foo.reducer(fooState, { type: 'noop' })).toBe(fooState); expect(foo.reducer(fooState, { type: 'noop' })).toBe(fooState);
return;
const store = foo.createStore();
const s1 = store.getState();
console.log(s1);
store.dispatch({ type: 'noop' });
const s2 = store.getState();
expect(s2.a).toBe(s1.a);
let bState = b.reducer(undefined, { type: 'noop' });
expect(b.reducer(bState, { type: 'noop' })).toBe(bState);
expect(s2.b).toBe(s1.b);
expect(s2).toBe(s1);
}); });

View File

@ -20,9 +20,14 @@ import { AggregateActions, AggregateSelectors, Dux } from './types.js';
import { buildActions } from './buildActions.js'; import { buildActions } from './buildActions.js';
import { buildInitial, AggregateState } from './initial.js'; import { buildInitial, AggregateState } from './initial.js';
import { buildReducer, MutationCase } from './reducer.js'; import { buildReducer, MutationCase } from './reducer.js';
import { augmentGetState, augmentMiddlewareApi, buildEffectsMiddleware, EffectMiddleware } from './effects.js'; import {
augmentGetState,
augmentMiddlewareApi,
buildEffectsMiddleware,
EffectMiddleware,
} from './effects.js';
import { ToolkitStore } from '@reduxjs/toolkit/dist/configureStore.js'; import { ToolkitStore } from '@reduxjs/toolkit/dist/configureStore.js';
import prepare from 'immer'; import { produce } from 'immer';
type MyActionCreator = { type: string } & ((...args: any) => any); type MyActionCreator = { type: string } & ((...args: any) => any);
@ -30,8 +35,8 @@ type XSel<R> = R extends Function ? R : () => R;
type CurriedSelector<S> = S extends (...args: any) => infer R ? XSel<R> : never; type CurriedSelector<S> = S extends (...args: any) => infer R ? XSel<R> : never;
type CurriedSelectors<S> = { type CurriedSelectors<S> = {
[key in keyof S]: CurriedSelector<S[key]> [key in keyof S]: CurriedSelector<S[key]>;
} };
type ResolveAction< type ResolveAction<
ActionType extends string, ActionType extends string,
@ -40,10 +45,10 @@ type ResolveAction<
? ActionArg ? ActionArg
: ActionArg extends (...args: any) => any : ActionArg extends (...args: any) => any
? ActionCreatorWithPreparedPayload< ? ActionCreatorWithPreparedPayload<
Parameters<ActionArg>, Parameters<ActionArg>,
ReturnType<ActionArg>, ReturnType<ActionArg>,
ActionType ActionType
> >
: ActionCreatorWithoutPayload<ActionType>; : ActionCreatorWithoutPayload<ActionType>;
type ResolveActions< type ResolveActions<
@ -51,21 +56,24 @@ type ResolveActions<
[key: string]: any; [key: string]: any;
}, },
> = { > = {
[ActionType in keyof A]: ActionType extends string [ActionType in keyof A]: ActionType extends string
? ResolveAction<ActionType, A[ActionType]> ? ResolveAction<ActionType, A[ActionType]>
: never; : never;
}; };
type Reaction<S = any, M extends MiddlewareAPI = MiddlewareAPI> = type Reaction<S = any, M extends MiddlewareAPI = MiddlewareAPI> = (
(api: M) => (state: S, previousState: S, unsubscribe: () => void) => any; api: M,
) => (state: S, previousState: S, unsubscribe: () => void) => any;
type AugmentedMiddlewareAPI<S, A, SELECTORS> = type AugmentedMiddlewareAPI<S, A, SELECTORS> = MiddlewareAPI<
MiddlewareAPI<Dispatch<AnyAction>, S> & { Dispatch<AnyAction>,
dispatch: A, S
getState: CurriedSelectors<SELECTORS>, > & {
actions: A, dispatch: A;
selectors: SELECTORS, getState: CurriedSelectors<SELECTORS>;
}; actions: A;
selectors: SELECTORS;
};
export type Mutation<A extends Action<any> = Action<any>, S = any> = ( export type Mutation<A extends Action<any> = Action<any>, S = any> = (
payload: A extends { payload: A extends {
@ -134,7 +142,7 @@ export default class Updux<
.filter(([slice, { selectors }]) => selectors) .filter(([slice, { selectors }]) => selectors)
.map(([slice, { selectors }]) => .map(([slice, { selectors }]) =>
R.mapValues(selectors, (s) => (state = {}) => { R.mapValues(selectors, (s) => (state = {}) => {
return s(state?.[slice]) return s(state?.[slice]);
}), }),
), ),
); );
@ -157,25 +165,34 @@ export default class Updux<
...Object.entries(this.#subduxes).flatMap( ...Object.entries(this.#subduxes).flatMap(
([slice, { effects }]) => { ([slice, { effects }]) => {
if (!effects) return []; if (!effects) return [];
return effects.map(effect => (api) => effect({ return effects.map(
...api, (effect) => (api) =>
getState: () => api.getState()[slice], effect({
})) ...api,
} getState: () => api.getState()[slice],
) }),
] );
},
),
];
} }
get reactions(): any { get reactions(): any {
return [...this.#localReactions, return [
...Object.entries(this.#subduxes).flatMap( ...this.#localReactions,
([slice, { reactions }]) => reactions.map( ...Object.entries(this.#subduxes).flatMap(
(r) => (api, unsub) => r({ ([slice, { reactions }]) =>
...api, reactions.map(
getState: () => api.getState()[slice], (r) => (api, unsub) =>
}, unsub) r(
) {
) ...api,
getState: () => api.getState()[slice],
},
unsub,
),
),
),
]; ];
} }
@ -192,7 +209,6 @@ export default class Updux<
this.selectors, this.selectors,
); );
const store = configureStore({ const store = configureStore({
reducer: this.reducer as Reducer< reducer: this.reducer as Reducer<
AggregateState<T_LocalState, T_Subduxes>, AggregateState<T_LocalState, T_Subduxes>,
@ -208,7 +224,7 @@ export default class Updux<
const action = (this.actions as any)[a](...args); const action = (this.actions as any)[a](...args);
dispatch(action); dispatch(action);
return action; return action;
} };
} }
store.getState = augmentGetState(store.getState, this.selectors); store.getState = augmentGetState(store.getState, this.selectors);
@ -223,20 +239,16 @@ export default class Updux<
(store as any).actions = this.actions; (store as any).actions = this.actions;
(store as any).selectors = this.selectors; (store as any).selectors = this.selectors;
return store as ToolkitStore<AggregateState<T_LocalState, T_Subduxes>> &
return store as ToolkitStore< AugmentedMiddlewareAPI<
AggregateState<T_LocalState, T_Subduxes> AggregateState<T_LocalState, T_Subduxes>,
> & AugmentedMiddlewareAPI< AggregateActions<ResolveActions<T_LocalActions>, T_Subduxes>,
AggregateState<T_LocalState, T_Subduxes>, AggregateSelectors<
AggregateActions< T_LocalSelectors,
ResolveActions<T_LocalActions>, T_Subduxes,
T_Subduxes AggregateState<T_LocalState, T_Subduxes>
>, AggregateSelectors< >
T_LocalSelectors, >;
T_Subduxes,
AggregateState<T_LocalState, T_Subduxes>
>
>;
} }
get selectors(): AggregateSelectors< get selectors(): AggregateSelectors<
@ -280,7 +292,7 @@ export default class Updux<
matcher = matcher.match; matcher = matcher.match;
} }
const immerMutation = (...args) => prepare(mutation(...args)); const immerMutation = (...args) => produce(mutation(...args));
this.#localMutations.push({ this.#localMutations.push({
terminal, terminal,
@ -309,31 +321,27 @@ export default class Updux<
this.actions, this.actions,
this.selectors, this.selectors,
); );
} }
#localReactions: any[] = []; #localReactions: any[] = [];
addReaction(reaction: Reaction<AggregateState<T_LocalState, T_Subduxes>, addReaction(
AugmentedMiddlewareAPI< reaction: Reaction<
AggregateState<T_LocalState, T_Subduxes>, AggregateState<T_LocalState, T_Subduxes>,
AggregateActions< AugmentedMiddlewareAPI<
ResolveActions<T_LocalActions>, AggregateState<T_LocalState, T_Subduxes>,
T_Subduxes AggregateActions<ResolveActions<T_LocalActions>, T_Subduxes>,
>, AggregateSelectors< AggregateSelectors<
T_LocalSelectors, T_LocalSelectors,
T_Subduxes, T_Subduxes,
AggregateState<T_LocalState, T_Subduxes> AggregateState<T_LocalState, T_Subduxes>
>
> >
> >,
>) { ) {
let previous: any; let previous: any;
const memoized = (api: any) => { const memoized = (api: any) => {
api = augmentMiddlewareApi(api, api = augmentMiddlewareApi(api, this.actions, this.selectors);
this.actions,
this.selectors
);
const r = reaction(api); const r = reaction(api);
return (unsub: () => void) => { return (unsub: () => void) => {
const state = api.getState(); const state = api.getState();
@ -341,19 +349,21 @@ export default class Updux<
let p = previous; let p = previous;
previous = state; previous = state;
r(state, p, unsub); r(state, p, unsub);
} };
} };
;
this.#localReactions.push(memoized); this.#localReactions.push(memoized);
} }
// internal method REMOVE // internal method REMOVE
subscribeTo(store, subscription) { subscribeTo(store, subscription) {
const localStore = augmentMiddlewareApi({ const localStore = augmentMiddlewareApi(
...store, {
subscribe: (subscriber) => this.subscribeTo(store, subscriber), // TODO not sure ...store,
}, this.actions, this.selectors); subscribe: (subscriber) => this.subscribeTo(store, subscriber), // TODO not sure
},
this.actions,
this.selectors,
);
const subscriber = subscription(localStore); const subscriber = subscription(localStore);
@ -371,4 +381,3 @@ export default class Updux<
return store.subscribe(memoSub); return store.subscribe(memoSub);
} }
} }

View File

@ -1,19 +1,28 @@
import { map, mapValues, merge } from 'lodash-es'; import { map, mapValues, merge } from 'lodash-es';
export function buildSelectors(localSelectors, splatSelector = {}, subduxes = {}) { export function buildSelectors(
localSelectors,
splatSelector = {},
subduxes = {},
) {
const subSelectors = map(subduxes, ({ selectors }, slice) => { const subSelectors = map(subduxes, ({ selectors }, slice) => {
if (!selectors) if (!selectors) return {};
return {}; if (slice === '*') return {};
if (slice === '*')
return {};
return mapValues(selectors, (func) => (state) => func(state[slice])); return mapValues(selectors, (func) => (state) => func(state[slice]));
}); });
let splat = {}; let splat = {};
for (const name in splatSelector) { for (const name in splatSelector) {
splat[name] = splat[name] =
(state) => (...args) => { (state) =>
(...args) => {
const value = splatSelector[name](state)(...args); const value = splatSelector[name](state)(...args);
const res = () => value; const res = () => value;
return merge(res, mapValues(subduxes['*'].selectors, (selector) => () => selector(value))); return merge(
res,
mapValues(
subduxes['*'].selectors,
(selector) => () => selector(value),
),
);
}; };
} }
return merge({}, ...subSelectors, localSelectors, splat); return merge({}, ...subSelectors, localSelectors, splat);

View File

@ -39,7 +39,7 @@ test('buildEffectsMiddleware', () => {
expect(seen).toEqual(0); expect(seen).toEqual(0);
const dispatch = vi.fn(); const dispatch = vi.fn();
mw({ getState: () => 'the state', dispatch })(() => { })({ mw({ getState: () => 'the state', dispatch })(() => {})({
type: 'noop', type: 'noop',
}); });
expect(seen).toEqual(1); expect(seen).toEqual(1);
@ -82,7 +82,7 @@ test('subdux', () => {
}); });
let seen = 0; let seen = 0;
bar.addEffect((api) => next => action => { bar.addEffect((api) => (next) => (action) => {
seen++; seen++;
expect(api.getState()).toBe('bar state'); expect(api.getState()).toBe('bar state');
next(action); next(action);
@ -93,7 +93,7 @@ test('subdux', () => {
loaded: true, loaded: true,
}, },
subduxes: { subduxes: {
bar bar,
}, },
}); });

View File

@ -34,7 +34,9 @@ test('initialState to createStore', () => {
initialState, initialState,
}); });
expect(dux.createStore({ initialState: { a: 3, b: 4 } }).getState()).toEqual({ expect(
dux.createStore({ initialState: { a: 3, b: 4 } }).getState(),
).toEqual({
a: 3, a: 3,
b: 4, b: 4,
}); });

View File

@ -19,5 +19,8 @@ export function buildInitial(localInitial, subduxes) {
); );
} }
return u(localInitial, R.mapValues(subduxes, R.pathOr(['initialState'], {}))); return u(
localInitial,
R.mapValues(subduxes, R.pathOr(['initialState'], {})),
);
} }

View File

@ -4,7 +4,7 @@ import * as R from 'remeda';
import { Dux } from './types.js'; import { Dux } from './types.js';
import { Mutation } from './Updux.js'; import { Mutation } from './Updux.js';
import u from '@yanick/updeep-remeda'; import u from '@yanick/updeep-remeda';
import prepare from 'immer'; import { produce } from 'immer';
export type MutationCase = { export type MutationCase = {
matcher: (action: Action) => boolean; matcher: (action: Action) => boolean;
@ -36,7 +36,7 @@ export function buildReducer(
.forEach(({ mutation, terminal: t }) => { .forEach(({ mutation, terminal: t }) => {
if (t) terminal = true; if (t) terminal = true;
didSomething = true; didSomething = true;
state = prepare( state = produce(
state, state,
mutation((action as any).payload, action), mutation((action as any).payload, action),
); );

View File

@ -19,8 +19,8 @@ test('basic selectors', () => {
getY: ({ y }: { y: number }) => y, getY: ({ y }: { y: number }) => y,
getYPlus: getYPlus:
({ y }) => ({ y }) =>
(incr: number) => (incr: number) =>
(y + incr) as number, (y + incr) as number,
}, },
}), }),
}, },