effectsMiddleware
This commit is contained in:
parent
7a5733425e
commit
95768706fa
47
src/Updux.ts
47
src/Updux.ts
@ -4,6 +4,8 @@ import {
|
|||||||
applyMiddleware,
|
applyMiddleware,
|
||||||
DeepPartial,
|
DeepPartial,
|
||||||
Action,
|
Action,
|
||||||
|
MiddlewareAPI,
|
||||||
|
AnyAction,
|
||||||
} from 'redux';
|
} from 'redux';
|
||||||
import {
|
import {
|
||||||
configureStore,
|
configureStore,
|
||||||
@ -16,6 +18,8 @@ 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 { ToolkitStore } from '@reduxjs/toolkit/dist/configureStore.js';
|
||||||
|
|
||||||
type MyActionCreator = { type: string } & ((...args: any) => any);
|
type MyActionCreator = { type: string } & ((...args: any) => any);
|
||||||
|
|
||||||
@ -84,6 +88,8 @@ export default class Updux<
|
|||||||
>;
|
>;
|
||||||
#selectors: any;
|
#selectors: any;
|
||||||
|
|
||||||
|
#effects: any[] = [];
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
config: Partial<{
|
config: Partial<{
|
||||||
initial: T_LocalState;
|
initial: T_LocalState;
|
||||||
@ -129,11 +135,31 @@ export default class Updux<
|
|||||||
) {
|
) {
|
||||||
const preloadedState: any = options.initial ?? this.initial;
|
const preloadedState: any = options.initial ?? this.initial;
|
||||||
const store = configureStore({
|
const store = configureStore({
|
||||||
reducer: ((state) => state) as Reducer<T_LocalState, any>,
|
reducer: ((state) => state) as Reducer<
|
||||||
|
AggregateState<T_LocalState, T_Subduxes>,
|
||||||
|
AnyAction
|
||||||
|
>,
|
||||||
preloadedState,
|
preloadedState,
|
||||||
|
middleware: [this.effectsMiddleware],
|
||||||
});
|
});
|
||||||
|
|
||||||
return store;
|
const dispatch: any = store.dispatch;
|
||||||
|
for (const a in this.actions) {
|
||||||
|
dispatch[a] = (...args) => {
|
||||||
|
const action = (this.actions as any)[a](...args);
|
||||||
|
dispatch(action);
|
||||||
|
return action;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return store as ToolkitStore<
|
||||||
|
AggregateState<T_LocalState, T_Subduxes>
|
||||||
|
> & {
|
||||||
|
dispatch: AggregateActions<
|
||||||
|
ResolveActions<T_LocalActions>,
|
||||||
|
T_Subduxes
|
||||||
|
>;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
get selectors(): AggregateSelectors<
|
get selectors(): AggregateSelectors<
|
||||||
@ -193,4 +219,21 @@ export default class Updux<
|
|||||||
) {
|
) {
|
||||||
this.#defaultMutation = { mutation, terminal };
|
this.#defaultMutation = { mutation, terminal };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addEffect(effect: EffectMiddleware) {
|
||||||
|
this.#effects.push(effect);
|
||||||
|
}
|
||||||
|
|
||||||
|
get effects() {
|
||||||
|
return this.#effects;
|
||||||
|
}
|
||||||
|
|
||||||
|
get effectsMiddleware() {
|
||||||
|
return buildEffectsMiddleware(
|
||||||
|
this.effects,
|
||||||
|
this.actions,
|
||||||
|
this.selectors,
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
79
src/effects.test.ts
Normal file
79
src/effects.test.ts
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import { buildEffectsMiddleware } from './effects.js';
|
||||||
|
import Updux, { createAction } from './index.js';
|
||||||
|
|
||||||
|
test('buildEffectsMiddleware', () => {
|
||||||
|
let seen = 0;
|
||||||
|
|
||||||
|
const mw = buildEffectsMiddleware(
|
||||||
|
[
|
||||||
|
(api) => (next) => (action) => {
|
||||||
|
seen++;
|
||||||
|
expect(api).toHaveProperty('getState');
|
||||||
|
expect(api.getState).toBeTypeOf('function');
|
||||||
|
|
||||||
|
expect(api.getState()).toEqual('the state');
|
||||||
|
|
||||||
|
expect(action).toHaveProperty('type');
|
||||||
|
expect(next).toBeTypeOf('function');
|
||||||
|
|
||||||
|
expect(api).toHaveProperty('actions');
|
||||||
|
expect(api.actions.action1()).toHaveProperty('type', 'action1');
|
||||||
|
|
||||||
|
api.dispatch.action1();
|
||||||
|
|
||||||
|
expect(api.selectors.getFoo(2)).toBe(2);
|
||||||
|
expect(api.getState.getFoo()).toBe('the state');
|
||||||
|
expect(api.getState.getBar(2)).toBe('the state2');
|
||||||
|
|
||||||
|
next(action);
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{
|
||||||
|
action1: createAction('action1'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
getFoo: (state) => state,
|
||||||
|
getBar: (state) => (i) => state + i,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(seen).toEqual(0);
|
||||||
|
const dispatch = vi.fn();
|
||||||
|
mw({ getState: () => 'the state', dispatch })(() => { })({
|
||||||
|
type: 'noop',
|
||||||
|
});
|
||||||
|
expect(seen).toEqual(1);
|
||||||
|
expect(dispatch).toHaveBeenCalledWith({ type: 'action1' });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('basic', () => {
|
||||||
|
const dux = new Updux({
|
||||||
|
initial: {
|
||||||
|
loaded: true,
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
foo: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
let seen = 0;
|
||||||
|
dux.addEffect((api) => (next) => (action) => {
|
||||||
|
seen++;
|
||||||
|
expect(api).toHaveProperty('getState');
|
||||||
|
expect(api.getState()).toHaveProperty('loaded');
|
||||||
|
expect(action).toHaveProperty('type');
|
||||||
|
expect(next).toBeTypeOf('function');
|
||||||
|
next(action);
|
||||||
|
});
|
||||||
|
|
||||||
|
const store = dux.createStore();
|
||||||
|
|
||||||
|
expect(seen).toEqual(0);
|
||||||
|
|
||||||
|
store.dispatch.foo();
|
||||||
|
|
||||||
|
expect(seen).toEqual(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO subdux effects
|
||||||
|
// TODO allow to subscribe / unsubscribe effects?
|
47
src/effects.ts
Normal file
47
src/effects.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import { AnyAction } from '@reduxjs/toolkit';
|
||||||
|
import { MiddlewareAPI } from '@reduxjs/toolkit';
|
||||||
|
import { Dispatch } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
|
export interface EffectMiddleware<S = any, D extends Dispatch = Dispatch> {
|
||||||
|
(api: MiddlewareAPI<D, S>): (
|
||||||
|
next: Dispatch<AnyAction>,
|
||||||
|
) => (action: AnyAction) => any;
|
||||||
|
}
|
||||||
|
|
||||||
|
const composeMw = (mws) => (api) => (originalNext) =>
|
||||||
|
mws.reduceRight((next, mw) => mw(api)(next), originalNext);
|
||||||
|
|
||||||
|
export function buildEffectsMiddleware(
|
||||||
|
effects = [],
|
||||||
|
actions = {},
|
||||||
|
selectors = {},
|
||||||
|
) {
|
||||||
|
return ({
|
||||||
|
getState: originalGetState,
|
||||||
|
dispatch: originalDispatch,
|
||||||
|
...rest
|
||||||
|
}) => {
|
||||||
|
const dispatch = (action) => originalDispatch(action);
|
||||||
|
|
||||||
|
for (const a in actions) {
|
||||||
|
dispatch[a] = (...args) => dispatch(actions[a](...args));
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let mws = effects.map((e) =>
|
||||||
|
e({ getState, dispatch, actions, selectors, ...rest }),
|
||||||
|
);
|
||||||
|
|
||||||
|
return (originalNext) => {
|
||||||
|
return mws.reduceRight((next, mw) => mw(next), originalNext);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
@ -1,43 +0,0 @@
|
|||||||
import R from 'remeda';
|
|
||||||
|
|
||||||
export function buildSelectors(
|
|
||||||
localSelectors,
|
|
||||||
findSelectors = {},
|
|
||||||
subduxes = {},
|
|
||||||
) {
|
|
||||||
const subSelectors = Object.entries(subduxes).map(
|
|
||||||
([slice, { selectors }]) => {
|
|
||||||
if (!selectors) return {};
|
|
||||||
if (slice === '*') return {};
|
|
||||||
|
|
||||||
return R.mapValues(
|
|
||||||
selectors,
|
|
||||||
(func) => (state) => func(state[slice]),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
let splat = {};
|
|
||||||
|
|
||||||
for (const name in findSelectors) {
|
|
||||||
splat[name] =
|
|
||||||
(mainState) =>
|
|
||||||
(...args) => {
|
|
||||||
const state = findSelectors[name](mainState)(...args);
|
|
||||||
|
|
||||||
return R.merge(
|
|
||||||
{ state },
|
|
||||||
R.mapValues(
|
|
||||||
subduxes['*']?.selectors ?? {},
|
|
||||||
(selector) =>
|
|
||||||
(...args) => {
|
|
||||||
let value = selector(state);
|
|
||||||
if (typeof value !== 'function') return value;
|
|
||||||
return value(...args);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return R.mergeAll([...subSelectors, localSelectors, splat]);
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user