266 lines
7.1 KiB
Plaintext
266 lines
7.1 KiB
Plaintext
|
import * as R from 'remeda';
|
||
|
import {
|
||
|
createStore as reduxCreateStore,
|
||
|
applyMiddleware,
|
||
|
DeepPartial,
|
||
|
Action,
|
||
|
MiddlewareAPI,
|
||
|
AnyAction,
|
||
|
Middleware,
|
||
|
Dispatch,
|
||
|
} from 'redux';
|
||
|
import {
|
||
|
configureStore,
|
||
|
Reducer,
|
||
|
ActionCreator,
|
||
|
ActionCreatorWithoutPayload,
|
||
|
ActionCreatorWithPreparedPayload,
|
||
|
} from '@reduxjs/toolkit';
|
||
|
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 { ToolkitStore } from '@reduxjs/toolkit/dist/configureStore.js';
|
||
|
import { produce } from 'immer';
|
||
|
|
||
|
type MyActionCreator = { type: string } & ((...args: any) => any);
|
||
|
|
||
|
|
||
|
type ResolveAction<
|
||
|
ActionType extends string,
|
||
|
ActionArg extends any,
|
||
|
> = ActionArg extends MyActionCreator
|
||
|
? ActionArg
|
||
|
: ActionArg extends (...args: any) => any
|
||
|
? ActionCreatorWithPreparedPayload<
|
||
|
Parameters<ActionArg>,
|
||
|
ReturnType<ActionArg>,
|
||
|
ActionType
|
||
|
>
|
||
|
: ActionCreatorWithoutPayload<ActionType>;
|
||
|
|
||
|
type ResolveActions<
|
||
|
A extends {
|
||
|
[key: string]: any;
|
||
|
},
|
||
|
> = {
|
||
|
[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 SelectorForState<S> = (state: S) => unknown;
|
||
|
type SelectorsForState<S> = {
|
||
|
[key: string]: SelectorForState<S>;
|
||
|
};
|
||
|
|
||
|
export default class Updux<
|
||
|
T_LocalState = Record<any, any>,
|
||
|
T_LocalActions extends {
|
||
|
[actionType: string]: any;
|
||
|
} = {},
|
||
|
T_Subduxes extends Record<string, Dux> = {},
|
||
|
T_LocalSelectors extends SelectorsForState<
|
||
|
AggregateState<T_LocalState, T_Subduxes>
|
||
|
> = {},
|
||
|
> {
|
||
|
#localInitial: T_LocalState;
|
||
|
#localActions: T_LocalActions;
|
||
|
#localMutations: MutationCase[] = [];
|
||
|
#defaultMutation: Omit<MutationCase, 'matcher'>;
|
||
|
#subduxes: T_Subduxes;
|
||
|
|
||
|
#name: string;
|
||
|
|
||
|
#actions: AggregateActions<ResolveActions<T_LocalActions>, T_Subduxes>;
|
||
|
|
||
|
#initialState: AggregateState<T_LocalState, T_Subduxes>;
|
||
|
|
||
|
#localSelectors: Record<
|
||
|
string,
|
||
|
(state: AggregateState<T_LocalState, T_Subduxes>) => any
|
||
|
>;
|
||
|
#selectors: any;
|
||
|
|
||
|
#localEffects: Middleware[] = [];
|
||
|
|
||
|
constructor(
|
||
|
config: Partial<{
|
||
|
initialState: T_LocalState;
|
||
|
actions: T_LocalActions;
|
||
|
subduxes: T_Subduxes;
|
||
|
selectors: T_LocalSelectors;
|
||
|
}>,
|
||
|
) {
|
||
|
// TODO check that we can't alter the initialState after the fact
|
||
|
this.#localInitial = config.initialState ?? ({} as T_LocalState);
|
||
|
this.#localActions = config.actions ?? ({} as T_LocalActions);
|
||
|
this.#subduxes = config.subduxes ?? ({} as T_Subduxes);
|
||
|
|
||
|
this.#actions = buildActions(this.#localActions, this.#subduxes);
|
||
|
|
||
|
this.#initialState = buildInitial(this.#localInitial, this.#subduxes);
|
||
|
this.#localSelectors = config.selectors;
|
||
|
|
||
|
const basedSelectors = R.mergeAll(
|
||
|
Object.entries(this.#subduxes)
|
||
|
.filter(([slice, { selectors }]) => selectors)
|
||
|
.map(([slice, { selectors }]) =>
|
||
|
R.mapValues(selectors, (s) => (state = {}) => {
|
||
|
return s(state?.[slice]);
|
||
|
}),
|
||
|
),
|
||
|
);
|
||
|
|
||
|
this.#selectors = R.merge(basedSelectors, this.#localSelectors);
|
||
|
}
|
||
|
|
||
|
get actions() {
|
||
|
return this.#actions;
|
||
|
}
|
||
|
|
||
|
// TODO memoize?
|
||
|
get initialState() {
|
||
|
return this.#initialState;
|
||
|
}
|
||
|
|
||
|
|
||
|
|
||
|
createStore(
|
||
|
options: Partial<{
|
||
|
preloadedState: T_LocalState;
|
||
|
}> = {},
|
||
|
) {
|
||
|
const preloadedState: any = options.preloadedState;
|
||
|
|
||
|
const effects = buildEffectsMiddleware(
|
||
|
this.effects,
|
||
|
this.actions,
|
||
|
this.selectors,
|
||
|
);
|
||
|
|
||
|
const store = configureStore({
|
||
|
reducer: this.reducer as Reducer<
|
||
|
AggregateState<T_LocalState, T_Subduxes>,
|
||
|
AnyAction
|
||
|
>,
|
||
|
preloadedState,
|
||
|
middleware: [effects],
|
||
|
});
|
||
|
|
||
|
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;
|
||
|
};
|
||
|
}
|
||
|
|
||
|
store.getState = augmentGetState(store.getState, this.selectors);
|
||
|
|
||
|
for (const reaction of this.reactions) {
|
||
|
let unsub;
|
||
|
const r = reaction(store);
|
||
|
|
||
|
unsub = store.subscribe(() => r(unsub));
|
||
|
}
|
||
|
|
||
|
(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>
|
||
|
>
|
||
|
>;
|
||
|
}
|
||
|
|
||
|
get selectors(): AggregateSelectors<
|
||
|
T_LocalSelectors,
|
||
|
T_Subduxes,
|
||
|
AggregateState<T_LocalState, T_Subduxes>
|
||
|
> {
|
||
|
return this.#selectors as any;
|
||
|
}
|
||
|
|
||
|
// TODO memoize this sucker
|
||
|
get reducer() {
|
||
|
return buildReducer(
|
||
|
this.initialState,
|
||
|
this.#localMutations,
|
||
|
this.#defaultMutation,
|
||
|
this.#subduxes,
|
||
|
) as any as (
|
||
|
state: undefined | typeof this.initialState,
|
||
|
action: Action,
|
||
|
) => typeof this.initialState;
|
||
|
}
|
||
|
|
||
|
|
||
|
addDefaultMutation(
|
||
|
mutation: Mutation<
|
||
|
Action<any>,
|
||
|
AggregateState<T_LocalState, T_Subduxes>
|
||
|
>,
|
||
|
terminal = false,
|
||
|
) {
|
||
|
this.#defaultMutation = { mutation, terminal };
|
||
|
}
|
||
|
|
||
|
|
||
|
get effectsMiddleware() {
|
||
|
return buildEffectsMiddleware(
|
||
|
this.effects,
|
||
|
this.actions,
|
||
|
this.selectors,
|
||
|
);
|
||
|
}
|
||
|
|
||
|
#localReactions: any[] = [];
|
||
|
|
||
|
// internal method REMOVE
|
||
|
subscribeTo(store, subscription) {
|
||
|
const localStore = augmentMiddlewareApi(
|
||
|
{
|
||
|
...store,
|
||
|
subscribe: (subscriber) => this.subscribeTo(store, subscriber), // TODO not sure
|
||
|
},
|
||
|
this.actions,
|
||
|
this.selectors,
|
||
|
);
|
||
|
|
||
|
const subscriber = subscription(localStore);
|
||
|
|
||
|
let previous;
|
||
|
let unsub;
|
||
|
|
||
|
const memoSub = () => {
|
||
|
const state = store.getState();
|
||
|
if (state === previous) return;
|
||
|
let p = previous;
|
||
|
previous = state;
|
||
|
subscriber(state, p, unsub);
|
||
|
};
|
||
|
|
||
|
return store.subscribe(memoSub);
|
||
|
}
|
||
|
}
|