add asDux

This commit is contained in:
Yanick Champoux 2024-08-10 09:07:23 -04:00
parent 91e6bdd4f0
commit 5fe858ef16
4 changed files with 107 additions and 18 deletions

View File

@ -11,6 +11,7 @@ import {
DuxSelectors, DuxSelectors,
DuxState, DuxState,
Mutation, Mutation,
MutationEntry,
} from './types.js'; } from './types.js';
import baseMoize from 'moize/mjs/index.mjs'; import baseMoize from 'moize/mjs/index.mjs';
import { buildInitialState } from './initialState.js'; import { buildInitialState } from './initialState.js';
@ -36,6 +37,7 @@ interface ActionCreator<T extends string, P, A extends Array<any>> {
const moize = (func) => baseMoize(func, { maxSize: 1 }); const moize = (func) => baseMoize(func, { maxSize: 1 });
/** /**
* @description main Updux class * @description main Updux class
*/ */
@ -68,7 +70,7 @@ export default class Updux<D extends DuxConfig> {
}; };
/** @internal */ /** @internal */
#effects = []; #effects: EffectMiddleware<D>[] = [];
/** @internal */ /** @internal */
#reactions: DuxReaction<D>[] = []; #reactions: DuxReaction<D>[] = [];
@ -76,12 +78,21 @@ export default class Updux<D extends DuxConfig> {
/** @internal */ /** @internal */
#mutations: any[] = []; #mutations: any[] = [];
/** @internal */
#inheritedReducer: (state: DuxState<D> | undefined, action: AnyAction) => DuxState<D>;
constructor(private readonly duxConfig: D) { constructor(private readonly duxConfig: D) {
if (duxConfig.subduxes) if (duxConfig.subduxes)
this.#subduxes = D.map(duxConfig.subduxes, (s) => this.#subduxes = D.map(duxConfig.subduxes, (s) =>
s instanceof Updux ? s : new Updux(s), s instanceof Updux ? s : new Updux(s),
); );
this.#inheritedReducer = duxConfig.reducer;
this.#effects = duxConfig.effects ?? [];
this.#reactions = (duxConfig.reactions as any) ?? [];
this.#actions = buildActions(duxConfig.actions, this.#subduxes) as any; this.#actions = buildActions(duxConfig.actions, this.#subduxes) as any;
} }
@ -223,6 +234,7 @@ export default class Updux<D extends DuxConfig> {
this.#mutations, this.#mutations,
this.#defaultMutation, this.#defaultMutation,
this.#subduxes, this.#subduxes,
this.#inheritedReducer,
); );
} }
@ -282,11 +294,11 @@ export default class Updux<D extends DuxConfig> {
return this as any; return this as any;
} }
get effects(): any { get effects(): any[] {
return this.#memoBuildEffects(this.#effects, this.#subduxes); return this.#memoBuildEffects(this.#effects, this.#subduxes);
} }
get upreducer() { get upreducer(): (action: AnyAction) => (state?: DuxState<D>) => DuxState<D> {
return (action: AnyAction) => (state?: DuxState<D>) => this.reducer(state, action); return (action: AnyAction) => (state?: DuxState<D>) => this.reducer(state, action);
} }
@ -318,4 +330,29 @@ export default class Updux<D extends DuxConfig> {
return this.#memoBuildReactions(this.#reactions, this.#subduxes); return this.#memoBuildReactions(this.#reactions, this.#subduxes);
} }
get defaultMutation() {
return this.#defaultMutation;
}
/**
* @description Returns an object holding the Updux reducer and all its
* paraphernalia.
*/
get asDux(): {
initialState: DuxState<D>,
actions: DuxActions<D>,
reducer: (state: DuxState<D>, action: AnyAction) => DuxState<D>,
effects: EffectMiddleware<D>[],
reactions: DuxReaction<D>[],
} {
return {
initialState: this.initialState,
actions: this.actions,
effects: this.effects,
reactions: this.reactions,
reducer: this.reducer,
} as any;
}
} }

37
src/asDux.test.ts Normal file
View File

@ -0,0 +1,37 @@
import Updux, { createAction } from './index.js';
test('asDux', () => {
const actionA = createAction('actionA');
const defaultMutation = () => (state: number) => state + 1;
const dux = new Updux({
initialState: 13,
actions: { actionA },
})
.setDefaultMutation(defaultMutation)
.addReaction((api) => (state) => { })
.addEffect((api) => (next) => (action) => next(action));
const asDux = dux.asDux;
expect(asDux.initialState).toEqual(13);
expect(asDux.reducer).toBeTypeOf('function');
expect(asDux.actions.actionA()).toMatchObject({ type: 'actionA' });
expect(asDux.effects).toHaveLength(1);
expect(asDux.reactions).toHaveLength(1);
const newDux = new Updux(asDux);
expect(newDux.initialState).toEqual(13);
expect(newDux.reducer).toBeTypeOf('function');
expect(newDux.actions.actionA()).toMatchObject({ type: 'actionA' });
expect(newDux.effects).toHaveLength(1);
expect(newDux.reactions).toHaveLength(1);
});

View File

@ -4,6 +4,7 @@ import * as rtk from '@reduxjs/toolkit';
import { DuxConfig, Mutation } from './types.js'; import { DuxConfig, Mutation } from './types.js';
import { D } from '@mobily/ts-belt'; import { D } from '@mobily/ts-belt';
import Updux from './Updux.js'; import Updux from './Updux.js';
import { AnyAction } from '@reduxjs/toolkit';
export type MutationCase = { export type MutationCase = {
matcher: (action: rtk.AnyAction) => boolean; matcher: (action: rtk.AnyAction) => boolean;
@ -16,12 +17,10 @@ export function buildReducer(
mutations: MutationCase[] = [], mutations: MutationCase[] = [],
defaultMutation?: Omit<MutationCase, 'matcher'>, defaultMutation?: Omit<MutationCase, 'matcher'>,
subduxes: Record<string, Updux<any>> = {}, subduxes: Record<string, Updux<any>> = {},
inheritedReducer?: (state: any, action: AnyAction) => any,
) { ) {
const subReducers = D.map(subduxes, D.getUnsafe('reducer')); const subReducers = D.map(subduxes, D.getUnsafe('reducer'));
// TODO matcherMutation
// TODO defaultMutation
//
const reducer = (state = initialStateState, action: rtk.AnyAction) => { const reducer = (state = initialStateState, action: rtk.AnyAction) => {
if (!action?.type) throw new Error('reducer called with a bad action'); if (!action?.type) throw new Error('reducer called with a bad action');
@ -30,6 +29,13 @@ export function buildReducer(
if (active.length === 0 && defaultMutation) if (active.length === 0 && defaultMutation)
active.push(defaultMutation as any); active.push(defaultMutation as any);
if (!active.some(R.prop<any, any>('terminal')) && inheritedReducer) {
active.push({
mutation: (_payload, action) => (state) => {
return u(state, inheritedReducer(state, action));
},
} as any);
}
if ( if (
!active.some(R.prop<any, any>('terminal')) && !active.some(R.prop<any, any>('terminal')) &&
D.values(subReducers).length > 0 D.values(subReducers).length > 0
@ -49,10 +55,6 @@ export function buildReducer(
} as any); } as any);
} }
// frozen objects don't play well with immer
// if (Object.isFrozen(state)) {
// state = { ...(state as any) };
// }
return active.reduce( return active.reduce(
(state, { mutation }) => (state, { mutation }) =>
mutation((action as any).payload, action)(state), mutation((action as any).payload, action)(state),

View File

@ -5,6 +5,7 @@ import {
Dispatch, Dispatch,
MiddlewareAPI, MiddlewareAPI,
} from '@reduxjs/toolkit'; } from '@reduxjs/toolkit';
import { EffectMiddleware } from './effects.js';
import Updux from './Updux.js'; import Updux from './Updux.js';
export type DuxConfig = Partial<{ export type DuxConfig = Partial<{
@ -12,6 +13,9 @@ export type DuxConfig = Partial<{
subduxes: Record<string, DuxConfig>; subduxes: Record<string, DuxConfig>;
actions: Record<string, ActionCreator<string> | Function>; actions: Record<string, ActionCreator<string> | Function>;
selectors: Record<string, Function>; selectors: Record<string, Function>;
effects: EffectMiddleware<any>[];
reactions: DuxReaction<any>[];
reducer: (state: any, action: AnyAction) => any;
}>; }>;
type UpduxConfig<D> = D extends Updux<infer T> ? T : D; type UpduxConfig<D> = D extends Updux<infer T> ? T : D;
@ -62,6 +66,12 @@ export type DuxActions<D> = ResolveActions<
export type Subduxes = Record<string, Updux<any> | DuxConfig>; export type Subduxes = Record<string, Updux<any> | DuxConfig>;
export type MutationEntry<S = any> = {
terminal: boolean;
matcher?: (action: AnyAction) => boolean;
mutation: Mutation<AnyAction, S>;
};
export type Mutation<A = AnyAction, S = any> = ( export type Mutation<A = AnyAction, S = any> = (
payload: A extends { payload: A extends {
payload: infer P; payload: infer P;
@ -89,14 +99,16 @@ export type AugmentedMiddlewareAPI<D> = MiddlewareAPI<
selectors: DuxSelectors<D>; selectors: DuxSelectors<D>;
}; };
export type DuxSelectors<D> = ForceResolveObject<(D extends { selectors: infer S } ? S : {}) & export type DuxSelectors<D> = ForceResolveObject<
(D extends { selectors: infer S } ? S : {}) &
(D extends { subduxes: infer SUB } (D extends { subduxes: infer SUB }
? UnionToIntersection< ? UnionToIntersection<
Values<{ Values<{
[key in keyof SUB]: RebaseSelectors<key, SUB[key]>; [key in keyof SUB]: RebaseSelectors<key, SUB[key]>;
}> }>
> >
: {})>; : {})
>;
export type UnionToIntersection<U> = ( export type UnionToIntersection<U> = (
U extends any ? (k: U) => void : never U extends any ? (k: U) => void : never
@ -117,9 +129,10 @@ type RebaseSelector<SLICE, S> = SLICE extends string
: never; : never;
type Values<X> = X[keyof X]; type Values<X> = X[keyof X];
export type DuxReaction<D extends DuxConfig> = (
export type DuxReaction<D extends DuxConfig> = api: AugmentedMiddlewareAPI<D>,
(api: AugmentedMiddlewareAPI<D>) => (state: DuxState<D>, ) => (
previousState: DuxState<D> | undefined, state: DuxState<D>,
unsubscribe: () => void) => void; previousState: DuxState<D> | undefined,
unsubscribe: () => void,
) => void;