selectors and store

This commit is contained in:
Yanick Champoux 2023-03-11 17:10:38 -05:00
parent 347d90267e
commit f322691906
4 changed files with 83 additions and 24 deletions

View File

@ -19,11 +19,18 @@ 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 { buildEffectsMiddleware, EffectMiddleware } from './effects.js'; import { augmentGetState, buildEffectsMiddleware, EffectMiddleware } from './effects.js';
import { ToolkitStore } from '@reduxjs/toolkit/dist/configureStore.js'; import { ToolkitStore } from '@reduxjs/toolkit/dist/configureStore.js';
type MyActionCreator = { type: string } & ((...args: any) => any); type MyActionCreator = { type: string } & ((...args: any) => any);
type XSel<R> = R extends Function ? R : () => R;
type CurriedSelector<S> = S extends (...args: any) => infer R ? XSel<R> : never;
type CurriedSelectors<S> = {
[key in keyof S]: CurriedSelector<S[key]>
}
type ResolveAction< type ResolveAction<
ActionType extends string, ActionType extends string,
ActionArg extends any, ActionArg extends any,
@ -176,6 +183,8 @@ export default class Updux<
} }
} }
store.getState = augmentGetState(store.getState, this.selectors);
return store as ToolkitStore< return store as ToolkitStore<
AggregateState<T_LocalState, T_Subduxes> AggregateState<T_LocalState, T_Subduxes>
> & { > & {
@ -183,6 +192,12 @@ export default class Updux<
ResolveActions<T_LocalActions>, ResolveActions<T_LocalActions>,
T_Subduxes T_Subduxes
>; >;
} & {
getState: CurriedSelectors<AggregateSelectors<
T_LocalSelectors,
T_Subduxes,
AggregateState<T_LocalState, T_Subduxes>
>>
}; };
} }
@ -257,3 +272,4 @@ export default class Updux<
} }
} }

View File

@ -1,31 +1,42 @@
import { mapValues, map, get } from 'lodash-es'; import { mapValues, map, get } from 'lodash-es';
const middlewareFor = (type, middleware) => (api) => (next) => (action) => { const middlewareFor = (type, middleware) => (api) => (next) => (action) => {
if (type !== '*' && action.type !== type) if (type !== '*' && action.type !== type) return next(action);
return next(action);
return middleware(api)(next)(action); return middleware(api)(next)(action);
}; };
const sliceMw = (slice, mw) => (api) => { const sliceMw = (slice, mw) => (api) => {
const getSliceState = () => get(api.getState(), slice); const getSliceState = () => get(api.getState(), slice);
return mw(Object.assign(Object.assign({}, api), { getState: getSliceState })); return mw(
Object.assign(Object.assign({}, api), { getState: getSliceState }),
);
}; };
export const augmentGetState = (getState, selectors) =>
Object.assign(
getState,
mapValues(selectors, (selector) => {
return (...args) => {
let result = selector(api.getState());
if (typeof result === 'function') return result(...args);
return result;
};
}),
);
export function augmentMiddlewareApi(api, actions, selectors) { export function augmentMiddlewareApi(api, actions, selectors) {
const getState = () => api.getState(); const getState = augmentGetState(() => api.getState(), selectors);
const dispatch = (action) => api.dispatch(action); const dispatch = (action) => api.dispatch(action);
Object.assign(getState, mapValues(selectors, (selector) => { Object.assign(
return (...args) => { dispatch,
let result = selector(api.getState()); mapValues(actions, (action) => {
if (typeof result === 'function') return (...args) => api.dispatch(action(...args));
return result(...args); }),
return result; );
}; return Object.assign(Object.assign({}, api), {
})); getState,
Object.assign(dispatch, mapValues(actions, (action) => {
return (...args) => api.dispatch(action(...args));
}));
return Object.assign(Object.assign({}, api), { getState,
dispatch, dispatch,
actions, actions,
selectors }); selectors,
});
} }
export const effectToMiddleware = (effect, actions, selectors) => { export const effectToMiddleware = (effect, actions, selectors) => {
let mw = effect; let mw = effect;
@ -37,12 +48,23 @@ export const effectToMiddleware = (effect, actions, selectors) => {
} }
return (api) => mw(augmentMiddlewareApi(api, actions, selectors)); return (api) => mw(augmentMiddlewareApi(api, actions, selectors));
}; };
const composeMw = (mws) => (api) => (original_next) => mws.reduceRight((next, mw) => mw(api)(next), original_next); const composeMw = (mws) => (api) => (original_next) =>
export function buildMiddleware(effects = [], actions = {}, selectors = {}, sub = {}, wrapper = undefined, dux = undefined) { mws.reduceRight((next, mw) => mw(api)(next), original_next);
let inner = map(sub, ({ middleware }, slice) => slice !== '*' && middleware ? sliceMw(slice, middleware) : undefined).filter((x) => x); export function buildMiddleware(
const local = effects.map((effect) => effectToMiddleware(effect, actions, selectors)); effects = [],
actions = {},
selectors = {},
sub = {},
wrapper = undefined,
dux = undefined,
) {
let inner = map(sub, ({ middleware }, slice) =>
slice !== '*' && middleware ? sliceMw(slice, middleware) : undefined,
).filter((x) => x);
const local = effects.map((effect) =>
effectToMiddleware(effect, actions, selectors),
);
let mws = [...local, ...inner]; let mws = [...local, ...inner];
if (wrapper) if (wrapper) mws = wrapper(mws, dux);
mws = wrapper(mws, dux);
return composeMw(mws); return composeMw(mws);
} }

View File

@ -11,6 +11,17 @@ export interface EffectMiddleware<S = any, D extends Dispatch = Dispatch> {
const composeMw = (mws) => (api) => (originalNext) => const composeMw = (mws) => (api) => (originalNext) =>
mws.reduceRight((next, mw) => mw(api)(next), originalNext); mws.reduceRight((next, mw) => mw(api)(next), originalNext);
export const augmentGetState = (originalGetState, selectors) => {
const getState = () => originalGetState();
for (const s in selectors) {
getState[s] = (...args) => {
let result = selectors[s](originalGetState());
if (typeof result === 'function') return result(...args);
return result;
};
}
return getState;
};
export function buildEffectsMiddleware( export function buildEffectsMiddleware(
effects = [], effects = [],
actions = {}, actions = {},

View File

@ -17,6 +17,10 @@ test('basic selectors', () => {
initial: { y: 2 }, initial: { y: 2 },
selectors: { selectors: {
getY: ({ y }: { y: number }) => y, getY: ({ y }: { y: number }) => y,
getYPlus:
({ y }) =>
(incr: number) =>
(y + incr) as number,
}, },
}), }),
}, },
@ -29,4 +33,10 @@ test('basic selectors', () => {
expect(foo.selectors.getY(sample)).toBe(3); expect(foo.selectors.getY(sample)).toBe(3);
expect(foo.selectors.getX(sample)).toBe(4); expect(foo.selectors.getX(sample)).toBe(4);
expect(foo.selectors.getYPlus(sample)(3)).toBe(6);
const store = foo.createStore();
expect(store.getState.getY()).toBe(2);
expect(store.getState.getYPlus(3)).toBe(5);
}); });