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, ReturnType, ActionType > : ActionCreatorWithoutPayload; type ResolveActions< A extends { [key: string]: any; }, > = { [ActionType in keyof A]: ActionType extends string ? ResolveAction : never; }; type Reaction = ( api: M, ) => (state: S, previousState: S, unsubscribe: () => void) => any; type SelectorForState = (state: S) => unknown; type SelectorsForState = { [key: string]: SelectorForState; }; export default class Updux< T_LocalState = Record, T_LocalActions extends { [actionType: string]: any; } = {}, T_Subduxes extends Record = {}, T_LocalSelectors extends SelectorsForState< AggregateState > = {}, > { #localInitial: T_LocalState; #localActions: T_LocalActions; #localMutations: MutationCase[] = []; #defaultMutation: Omit; #subduxes: T_Subduxes; #name: string; #actions: AggregateActions, T_Subduxes>; #initialState: AggregateState; #localSelectors: Record< string, (state: AggregateState) => 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, 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> & AugmentedMiddlewareAPI< AggregateState, AggregateActions, T_Subduxes>, AggregateSelectors< T_LocalSelectors, T_Subduxes, AggregateState > >; } get selectors(): AggregateSelectors< T_LocalSelectors, T_Subduxes, AggregateState > { 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, AggregateState >, 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); } }