import { compose, mapKeys, mapValues, omit, values } from 'lodash/fp'; import { applyMiddleware, combineReducers, createStore, Reducer, Middleware, Action } from 'redux'; import { combineEpics, createEpicMiddleware, Epic } from 'redux-observable'; import { EMPTY, Observable } from 'rxjs'; import { PageTransitioner } from 'wdk-client/Utils/PageTransitioner'; import { ParamValueStore } from 'wdk-client/Utils/ParamValueStore'; import WdkService from 'wdk-client/Service/WdkService'; import { wdkMiddleware } from 'wdk-client/Core/WdkMiddleware'; import { catchError, startWith } from 'rxjs/operators'; import { notifyUnhandledError } from 'wdk-client/Actions/UnhandledErrorActions'; declare global{ interface Window { __REDUX_DEVTOOLS_EXTENSION_COMPOSE__?: Function; } } export type EpicDependencies = { paramValueStore: ParamValueStore; transitioner: PageTransitioner; wdkService: WdkService; } export type ModuleReducer = (state: T | undefined, action: A) => T; export type ModuleEpic = Epic; export type StoreModule = { key: string; reduce: ModuleReducer; observe?: ModuleEpic; } type StoreModuleRecord, A extends Action> = { [K in keyof T]: StoreModule }; type RootReducer = Reducer; export function createWdkStore( storeModules: StoreModuleRecord, dependencies: E, // FIXME Figure out how to allow the order of middleware to be configured additionalMiddleware: Middleware[] = [] ) { const rootReducer = makeRootReducer(storeModules); const rootEpic = makeRootEpic(storeModules); const epicMiddleware = createEpicMiddleware({ dependencies }); const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({ name: 'WDKClient' }) : compose; const enhancer = composeEnhancers( applyMiddleware( ...additionalMiddleware, wdkMiddleware(dependencies), epicMiddleware, ) ); const store = createStore(rootReducer, enhancer); epicMiddleware.run(rootEpic); return store; } function makeRootReducer, A extends Action>(storeModules: StoreModuleRecord): RootReducer { const reducers = mapValues(m => m.reduce, storeModules); const keyedReducers = mapKeys(moduleKey => storeModules[moduleKey].key, reducers); return combineReducers(keyedReducers); } function makeRootEpic, A extends Action>(storeModules: StoreModuleRecord): ModuleEpic { const epics = values(storeModules) .map(({ observe }: StoreModule): ModuleEpic => (action$, state$, deps) => { return observe ? observe(action$, state$, deps).pipe( catchError((error, caught) => { return caught.pipe( startWith(notifyUnhandledError(error)) ) as Observable; }) ) : EMPTY; }); return combineEpics(...epics); }