generic actions

This commit is contained in:
Yanick Champoux 2021-10-18 09:02:20 -04:00
parent 27958a6d14
commit 4e4fa13d90
13 changed files with 103 additions and 96 deletions

4
src/Updux.d.ts vendored
View File

@ -8,11 +8,9 @@ type UpduxConfig<TState> = Partial<{
effects: Record<string, Function>; effects: Record<string, Function>;
reactions: Record<string, Function>; reactions: Record<string, Function>;
mappedReaction: Function; mappedReaction: Function;
}> }>;
export class Updux<TState = unknown> { export class Updux<TState = unknown> {
constructor(config: UpduxConfig<TState>); constructor(config: UpduxConfig<TState>);
get initial(): TState; get initial(): TState;

View File

@ -9,12 +9,9 @@ import { buildActions } from './buildActions';
import { buildSelectors } from './buildSelectors'; import { buildSelectors } from './buildSelectors';
import { action } from './actions'; import { action } from './actions';
import { buildUpreducer } from './buildUpreducer'; import { buildUpreducer } from './buildUpreducer';
import { import { buildMiddleware, augmentMiddlewareApi } from './buildMiddleware';
buildMiddleware,
augmentMiddlewareApi,
} from './buildMiddleware';
import { AggregateDuxState, Dict } from './types'; import { AggregateDuxActions, AggregateDuxState, Dict } from './types';
/** /**
* Configuration object typically passed to the constructor of the class Updux. * Configuration object typically passed to the constructor of the class Updux.
@ -23,7 +20,7 @@ export interface UpduxConfig<
TState = any, TState = any,
TActions = {}, TActions = {},
TSelectors = {}, TSelectors = {},
TSubduxes = {}, TSubduxes = {}
> { > {
/** /**
* Local initial state. * Local initial state.
@ -39,7 +36,7 @@ export interface UpduxConfig<
/** /**
* Local actions. * Local actions.
*/ */
actions?: Record<string, any>; actions?: TActions;
/** /**
* Local selectors. * Local selectors.
@ -77,9 +74,9 @@ export interface UpduxConfig<
export class Updux< export class Updux<
TState extends any = {}, TState extends any = {},
TActions = {}, TActions extends object = {},
TSelectors = {}, TSelectors = {},
TSubduxes extends object = {}, TSubduxes extends object = {}
> { > {
/** @type { unknown } */ /** @type { unknown } */
#initial = {}; #initial = {};
@ -145,7 +142,7 @@ export class Updux<
this.#mappedSelectors = { this.#mappedSelectors = {
...this.#mappedSelectors, ...this.#mappedSelectors,
[name]: f, [name]: f,
} };
} }
get middleware() { get middleware() {
@ -162,8 +159,8 @@ export class Updux<
return this.#memoInitial(this.#initial, this.#subduxes); return this.#memoInitial(this.#initial, this.#subduxes);
} }
get actions(): Record<string, Function> { get actions(): AggregateDuxActions<TActions, TSubduxes> {
return this.#memoActions(this.#actions, this.#subduxes); return this.#memoActions(this.#actions, this.#subduxes) as any;
} }
get selectors() { get selectors() {
@ -333,14 +330,15 @@ export class Updux<
} }
createStore(initial?: unknown, enhancerGenerator?: Function) { createStore(initial?: unknown, enhancerGenerator?: Function) {
const enhancer = (enhancerGenerator ?? applyMiddleware)(
const enhancer = (enhancerGenerator ?? applyMiddleware)(this.middleware); this.middleware
);
const store: { const store: {
getState: Function & Record<string,Function>, getState: Function & Record<string, Function>;
dispatch: Function & Record<string,Function>, dispatch: Function & Record<string, Function>;
selectors: Record<string,Function>, selectors: Record<string, Function>;
actions: Record<string,Function>, actions: AggregateDuxActions<TActions, TSubduxes>;
} = reduxCreateStore( } = reduxCreateStore(
this.reducer, this.reducer,
initial ?? this.initial, initial ?? this.initial,

View File

@ -1,9 +1,14 @@
export type Action<T extends string = string, TPayload = unknown> = { export type Action<T extends string = string, TPayload = unknown> = {
type: T; meta?: Record<string,unknown>; } & ( type: T;
{ payload?: TPayload } meta?: Record<string, unknown>;
) } & {
payload?: TPayload;
};
export type ActionGenerator<TType extends string = string, TPayloadGen = undefined> = { export type ActionGenerator<
TType extends string = string,
TPayloadGen = undefined
> = {
type: TType; type: TType;
} & (TPayloadGen extends (...args: any) => any } & (TPayloadGen extends (...args: any) => any
? (...args: Parameters<TPayloadGen>) => { ? (...args: Parameters<TPayloadGen>) => {

View File

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

View File

@ -1,11 +1,18 @@
import { map, mapValues, merge } from 'lodash'; import { map, mapValues, merge } from 'lodash';
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) return {}; if (!selectors) return {};
if (slice === '*') return {}; if (slice === '*') return {};
return mapValues(selectors, (func: Function) => (state) => func(state[slice])); return mapValues(
selectors,
(func: Function) => (state) => func(state[slice])
);
}); });
let splat = {}; let splat = {};

View File

@ -8,7 +8,8 @@ export function buildUpreducer(initial, mutations, subduxes = {}) {
: null; : null;
return (action) => (state) => { return (action) => (state) => {
if( !action?.type ) throw new Error("upreducer called with a bad action"); if (!action?.type)
throw new Error('upreducer called with a bad action');
let newState = state ?? initial; let newState = state ?? initial;

View File

@ -32,7 +32,7 @@ test('README.md', () => {
}); });
test('tutorial', () => { test('tutorial', () => {
const todosDux = new Updux({ const todosDux = new Updux<any, any>({
initial: { initial: {
next_id: 1, next_id: 1,
todos: [], todos: [],

View File

@ -16,7 +16,9 @@ test('basic', () => {
expect(dux.reducer(undefined, dux.actions.doIt())).toEqual('bingo'); expect(dux.reducer(undefined, dux.actions.doIt())).toEqual('bingo');
expect(dux.reducer(undefined, dux.actions.thisToo())).toEqual( 'straight type'); expect(dux.reducer(undefined, dux.actions.thisToo())).toEqual(
'straight type'
);
}); });
test('override', () => { test('override', () => {
@ -48,8 +50,7 @@ test('override', () => {
undefined undefined
); );
expect(state).toMatchObject( expect(state).toMatchObject({
{
alpha: ['foo', 'bar'], alpha: ['foo', 'bar'],
subbie: 1, subbie: 1,
}); });
@ -77,8 +78,9 @@ test('order of processing', () => {
}, },
}); });
expect(dux.reducer(undefined, foo())) expect(dux.reducer(undefined, foo())).toMatchObject({
.toMatchObject({ x: ['subdux', 'main'] }); x: ['subdux', 'main'],
});
}); });
test('setMutation', () => { test('setMutation', () => {
@ -94,7 +96,6 @@ test('setMutation', () => {
dux.setMutation('foo', () => () => 'foo'); dux.setMutation('foo', () => () => 'foo');
expect(dux.reducer(undefined, foo())).toEqual('foo'); expect(dux.reducer(undefined, foo())).toEqual('foo');
}); });
test('setMutation, name as function', () => { test('setMutation, name as function', () => {

View File

@ -135,8 +135,7 @@ test('splat subscriptions, more', () => {
initial: { a: 1 }, initial: { a: 1 },
actions: { foo: null, incAll: null }, actions: { foo: null, incAll: null },
mutations: { mutations: {
foo: (id) => (state) => foo: (id) => (state) => state.a === id ? { ...state, b: 1 } : state,
state.a === id ? { ...state, b: 1 } : state,
incAll: () => (state) => ({ ...state, a: state.a + 1 }), incAll: () => (state) => ({ ...state, a: state.a + 1 }),
}, },
reactions: [() => snitch], reactions: [() => snitch],
@ -163,16 +162,8 @@ test('splat subscriptions, more', () => {
expect(snitch).toHaveBeenCalledTimes(2); expect(snitch).toHaveBeenCalledTimes(2);
expect(snitch).toHaveBeenCalledWith( expect(snitch).toHaveBeenCalledWith({ a: 1 }, undefined, expect.anything());
{ a: 1 }, expect(snitch).toHaveBeenCalledWith({ a: 2 }, undefined, expect.anything());
undefined,
expect.anything()
);
expect(snitch).toHaveBeenCalledWith(
{ a: 2 },
undefined,
expect.anything()
);
snitch.mockReset(); snitch.mockReset();
@ -194,11 +185,7 @@ test('splat subscriptions, more', () => {
expect(snitch).toHaveBeenCalledTimes(1); expect(snitch).toHaveBeenCalledTimes(1);
expect(snitch).toHaveBeenCalledWith( expect(snitch).toHaveBeenCalledWith(undefined, { a: 1 }, expect.anything());
undefined,
{ a: 1 },
expect.anything()
);
// only one subscriber left // only one subscriber left
snitch.mockReset(); snitch.mockReset();
@ -246,9 +233,7 @@ test('many levels down', () => {
mutations: { mutations: {
add: () => (x) => x + 1, add: () => (x) => x + 1,
}, },
reactions: [ reactions: [(store) => (state) => snitch(state, store)],
(store) => (state) => snitch(state, store),
],
}, },
}, },
}, },

View File

@ -109,8 +109,7 @@ test('subscription within subduxes', () => {
let innerState = jest.fn(() => null); let innerState = jest.fn(() => null);
let outerState = jest.fn(() => null); let outerState = jest.fn(() => null);
const resetMocks = () => const resetMocks = () => [innerState, outerState].map((f) => f.mockReset());
[innerState, outerState].map((f) => f.mockReset());
const inner = new Updux({ const inner = new Updux({
initial: 1, initial: 1,

View File

@ -1,11 +1,19 @@
export type Dict<T> = Record<string, T>; export type Dict<T> = Record<string, T>;
export type UnionToIntersection<U> = (
U extends any ? (k: U) => void : never
) extends (k: infer I) => void
? I
: never;
type Subdux<TState = any> = { type Subdux<TState = any> = {
initial: TState initial: TState;
}; };
type StateOf<D> = D extends { initial: infer I } ? I : unknown; type StateOf<D> = D extends { initial: infer I } ? I : unknown;
type ActionsOf<C> = C extends { actions: infer A } ? {
[K in keyof A]: Function
} : {};
type Subduxes = Record<string, Subdux>; type Subduxes = Record<string, Subdux>;
@ -16,5 +24,12 @@ export type DuxStateSubduxes<C> = C extends { '*': infer I }
} }
: { [K in keyof C]: StateOf<C[K]> }; : { [K in keyof C]: StateOf<C[K]> };
export type AggregateDuxState<TState, TSubduxes> = TState & DuxStateSubduxes<TSubduxes>; export type AggregateDuxState<TState, TSubduxes> = TState &
DuxStateSubduxes<TSubduxes>;
type DuxActionsSubduxes<C> = C extends object ? ActionsOf<C[keyof C]> : unknown;
type ItemsOf<C> = C extends object ? C[keyof C] : unknown;
export type AggregateDuxActions<TActions, TSubduxes> = TActions &
UnionToIntersection<ActionsOf<ItemsOf<TSubduxes>>>;