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'
vars:
PARENT_BRANCH: main
tasks:
build: tsc
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:dev: vitest src

View File

@ -1,15 +1,7 @@
import { test, expect } from 'vitest';
import Updux from './Updux';
import Updux from './Updux.js';
test('subdux idempotency', () => {
// const c = new Updux({
// initialState: 2,
// });
// const b = new Updux({
// subduxes: {
// c,
// },
// });
const foo = new Updux({
subduxes: {
a: new Updux({ initialState: 2 }),
@ -18,21 +10,4 @@ test('subdux idempotency', () => {
let fooState = foo.reducer(undefined, { type: 'noop' });
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 { buildInitial, AggregateState } from './initial.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 prepare from 'immer';
import { produce } from 'immer';
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 CurriedSelectors<S> = {
[key in keyof S]: CurriedSelector<S[key]>
}
[key in keyof S]: CurriedSelector<S[key]>;
};
type ResolveAction<
ActionType extends string,
@ -40,10 +45,10 @@ type ResolveAction<
? ActionArg
: ActionArg extends (...args: any) => any
? ActionCreatorWithPreparedPayload<
Parameters<ActionArg>,
ReturnType<ActionArg>,
ActionType
>
Parameters<ActionArg>,
ReturnType<ActionArg>,
ActionType
>
: ActionCreatorWithoutPayload<ActionType>;
type ResolveActions<
@ -51,21 +56,24 @@ type ResolveActions<
[key: string]: any;
},
> = {
[ActionType in keyof A]: ActionType extends string
[ActionType in keyof A]: ActionType extends string
? ResolveAction<ActionType, A[ActionType]>
: never;
};
};
type Reaction<S = any, M extends MiddlewareAPI = MiddlewareAPI> =
(api: M) => (state: S, previousState: S, unsubscribe: () => void) => any;
type Reaction<S = any, M extends MiddlewareAPI = MiddlewareAPI> = (
api: M,
) => (state: S, previousState: S, unsubscribe: () => void) => any;
type AugmentedMiddlewareAPI<S, A, SELECTORS> =
MiddlewareAPI<Dispatch<AnyAction>, S> & {
dispatch: A,
getState: CurriedSelectors<SELECTORS>,
actions: A,
selectors: SELECTORS,
};
type AugmentedMiddlewareAPI<S, A, SELECTORS> = MiddlewareAPI<
Dispatch<AnyAction>,
S
> & {
dispatch: A;
getState: CurriedSelectors<SELECTORS>;
actions: A;
selectors: SELECTORS;
};
export type Mutation<A extends Action<any> = Action<any>, S = any> = (
payload: A extends {
@ -134,7 +142,7 @@ export default class Updux<
.filter(([slice, { selectors }]) => selectors)
.map(([slice, { selectors }]) =>
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(
([slice, { effects }]) => {
if (!effects) return [];
return effects.map(effect => (api) => effect({
...api,
getState: () => api.getState()[slice],
}))
}
)
]
return effects.map(
(effect) => (api) =>
effect({
...api,
getState: () => api.getState()[slice],
}),
);
},
),
];
}
get reactions(): any {
return [...this.#localReactions,
...Object.entries(this.#subduxes).flatMap(
([slice, { reactions }]) => reactions.map(
(r) => (api, unsub) => r({
...api,
getState: () => api.getState()[slice],
}, unsub)
)
)
return [
...this.#localReactions,
...Object.entries(this.#subduxes).flatMap(
([slice, { reactions }]) =>
reactions.map(
(r) => (api, unsub) =>
r(
{
...api,
getState: () => api.getState()[slice],
},
unsub,
),
),
),
];
}
@ -192,7 +209,6 @@ export default class Updux<
this.selectors,
);
const store = configureStore({
reducer: this.reducer as Reducer<
AggregateState<T_LocalState, T_Subduxes>,
@ -208,7 +224,7 @@ export default class Updux<
const action = (this.actions as any)[a](...args);
dispatch(action);
return action;
}
};
}
store.getState = augmentGetState(store.getState, this.selectors);
@ -223,20 +239,16 @@ export default class Updux<
(store as any).actions = this.actions;
(store as any).selectors = this.selectors;
return store as ToolkitStore<
AggregateState<T_LocalState, T_Subduxes>
> & AugmentedMiddlewareAPI<
AggregateState<T_LocalState, T_Subduxes>,
AggregateActions<
ResolveActions<T_LocalActions>,
T_Subduxes
>, AggregateSelectors<
T_LocalSelectors,
T_Subduxes,
AggregateState<T_LocalState, T_Subduxes>
>
>;
return store as ToolkitStore<AggregateState<T_LocalState, T_Subduxes>> &
AugmentedMiddlewareAPI<
AggregateState<T_LocalState, T_Subduxes>,
AggregateActions<ResolveActions<T_LocalActions>, T_Subduxes>,
AggregateSelectors<
T_LocalSelectors,
T_Subduxes,
AggregateState<T_LocalState, T_Subduxes>
>
>;
}
get selectors(): AggregateSelectors<
@ -280,7 +292,7 @@ export default class Updux<
matcher = matcher.match;
}
const immerMutation = (...args) => prepare(mutation(...args));
const immerMutation = (...args) => produce(mutation(...args));
this.#localMutations.push({
terminal,
@ -309,31 +321,27 @@ export default class Updux<
this.actions,
this.selectors,
);
}
#localReactions: any[] = [];
addReaction(reaction: Reaction<AggregateState<T_LocalState, T_Subduxes>,
AugmentedMiddlewareAPI<
addReaction(
reaction: Reaction<
AggregateState<T_LocalState, T_Subduxes>,
AggregateActions<
ResolveActions<T_LocalActions>,
T_Subduxes
>, AggregateSelectors<
T_LocalSelectors,
T_Subduxes,
AggregateState<T_LocalState, T_Subduxes>
AugmentedMiddlewareAPI<
AggregateState<T_LocalState, T_Subduxes>,
AggregateActions<ResolveActions<T_LocalActions>, T_Subduxes>,
AggregateSelectors<
T_LocalSelectors,
T_Subduxes,
AggregateState<T_LocalState, T_Subduxes>
>
>
>
>) {
>,
) {
let previous: any;
const memoized = (api: any) => {
api = augmentMiddlewareApi(api,
this.actions,
this.selectors
);
api = augmentMiddlewareApi(api, this.actions, this.selectors);
const r = reaction(api);
return (unsub: () => void) => {
const state = api.getState();
@ -341,19 +349,21 @@ export default class Updux<
let p = previous;
previous = state;
r(state, p, unsub);
}
}
;
};
};
this.#localReactions.push(memoized);
}
// internal method REMOVE
subscribeTo(store, subscription) {
const localStore = augmentMiddlewareApi({
...store,
subscribe: (subscriber) => this.subscribeTo(store, subscriber), // TODO not sure
}, this.actions, this.selectors);
const localStore = augmentMiddlewareApi(
{
...store,
subscribe: (subscriber) => this.subscribeTo(store, subscriber), // TODO not sure
},
this.actions,
this.selectors,
);
const subscriber = subscription(localStore);
@ -371,4 +381,3 @@ export default class Updux<
return store.subscribe(memoSub);
}
}

View File

@ -1,19 +1,28 @@
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) => {
if (!selectors)
return {};
if (slice === '*')
return {};
if (!selectors) return {};
if (slice === '*') return {};
return mapValues(selectors, (func) => (state) => func(state[slice]));
});
let splat = {};
for (const name in splatSelector) {
splat[name] =
(state) => (...args) => {
(state) =>
(...args) => {
const value = splatSelector[name](state)(...args);
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);

View File

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

View File

@ -34,7 +34,9 @@ test('initialState to createStore', () => {
initialState,
});
expect(dux.createStore({ initialState: { a: 3, b: 4 } }).getState()).toEqual({
expect(
dux.createStore({ initialState: { a: 3, b: 4 } }).getState(),
).toEqual({
a: 3,
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 { Mutation } from './Updux.js';
import u from '@yanick/updeep-remeda';
import prepare from 'immer';
import { produce } from 'immer';
export type MutationCase = {
matcher: (action: Action) => boolean;
@ -36,7 +36,7 @@ export function buildReducer(
.forEach(({ mutation, terminal: t }) => {
if (t) terminal = true;
didSomething = true;
state = prepare(
state = produce(
state,
mutation((action as any).payload, action),
);

View File

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