UNPKG

typedux

Version:

Slightly adjusted Redux (awesome by default) for TS

390 lines (341 loc) 11.2 kB
import * as Immutable from "immutable" import { getLogger } from "@3fv/logger-proxy" import { Action as ReduxAction, Reducer as ReduxReducer } from "redux" import type { ObjectAsMap, RootState, State } from "./State" import type { ActionMessage } from "../actions" import type { ILeafReducer } from "./LeafReducer" import { Flag, isFunction } from "../util" // import {getGlobalStateProvider} from '../actions/Actions' import isEqualShallow from "shallowequal" import _get from "lodash/get" import _clone from "lodash/clone" import { INTERNAL_ACTION, INTERNAL_ACTIONS } from "../constants" import type { ObservableStore } from "../store/ObservableStore" import { Option } from "@3fv/prelude-ts" import { isDev } from "../dev" import { isString } from "@3fv/guard" import { createLeafActionType } from "../actions" const ActionIdCacheMax = 500, log = getLogger(__filename) /** * Get leaf value *} * @param rootValue * @param leaf * @return {any} */ function getLeafValue< S extends Partial<S> | ObjectAsMap<S>, K, R = K extends keyof S ? S[K] : unknown >(rootValue: S, leaf: string): R { if (Immutable.Map.isMap(rootValue)) { return (rootValue as Immutable.Map<any, any>).get(leaf) } else { return _get(rootValue, leaf) } } /** * Error handler type for root reducer */ export type RootReducerErrorHandler = ( err: Error, reducer?: ILeafReducer<any, any> ) => void /** * RootReducer for typedux apps * * Maps leaf reducers and decorated reducers * to the appropriate state functions */ export class RootReducer<S extends RootState = any> { // Internal list of all leaf reducers private reducers: ILeafReducer<any, ActionMessage<any>>[] = [] // handled actions ids to avoid duplication private handledActionIds = [] /** * onError ref, allows an error handler to * be assigned to the reducer */ private onError: RootReducerErrorHandler setOnError(onError: RootReducerErrorHandler) { this.onError = onError return this } /** * Create reducer * * @param rootStateType - type of root state, must be immutable map or record * @param reducers - list of all child reducers * @param store */ constructor( private store: ObservableStore<S>, private rootStateType: { new (): S } = null, ...reducers: ILeafReducer<any, any>[] ) { const leafs = [] reducers .filter(reducer => isFunction(reducer.leaf)) .forEach(reducer => { const leaf = reducer.leaf() if (leafs.includes(leaf)) { return } leafs.push(leaf) this.reducers.push(reducer) }) reducers .filter(reducer => !isFunction(reducer.leaf)) .forEach(reducer => this.reducers.push(reducer)) } /** * Create default state * * @param defaultStateValue - if provided then its used as base for inflation * @returns {State} */ defaultState(defaultStateValue: Partial<S> = null): S { // LOAD THE STATE AND VERIFY IT IS Immutable.Map/Record let state: Partial<S> = defaultStateValue || ({ type: "ROOT" } as any) // ITERATE REDUCERS & CREATE LEAF STATES this.reducers .filter(reducer => isFunction(reducer.leaf)) .forEach(reducer => { const leaf = reducer.leaf(), leafDefaultState = getLeafValue(defaultStateValue, leaf) state = { ...state, [leaf]: reducer.defaultState(leafDefaultState || {}) } }) return state as any } /** * Create a generic handler for dispatches * * @returns {(state:S, action:ReduxAction)=>S} */ makeGenericHandler(): ReduxReducer<S> { return (state: S, action: ReduxAction): S => this.handle(state, action as ActionMessage<any>) as S } /** * Handle action message * * @param state * @param action * @returns {State} */ handle(state: S, action: ActionMessage<any>): S { // Check if action has already been processed if (action.id && this.handledActionIds.includes(action.id)) { if (typeof console !== "undefined" && console.trace) { console.trace( `Duplicate action received: ${action.leaf}/${action.type}, ${action.id}`, action ) } return state as S } // Push action id to the handled list else if (action.id) { this.handledActionIds.unshift(action.id) if (this.handledActionIds.length > ActionIdCacheMax) { this.handledActionIds.length = ActionIdCacheMax } } try { /** * Tracks whether the overall state has changed */ let hasChanged = false // Guard state type as immutable if (!state) { state = this.defaultState(state) as any hasChanged = true } /** * Create a change detector func that evaluates * a leaf for changes and updates (and sets changed flags) * as need for tracking * * @param leaf * @param currentState * @param updateState * @param changed */ const createChangeDetector = <CS extends State>( leaf: string, currentState: CS, updateState: (newState: CS) => any, changed: Flag ) => (newReducerState: CS) => { if (!newReducerState) { throw new Error(`New reducer state is null for leaf ${leaf}`) } const noMatch = !isEqualShallow(currentState, newReducerState) if (noMatch) { changed.set() updateState(_clone(newReducerState)) } } // Store a ref to the original state object const stateMap: State = state as any // Hold the interim state in `tempState` let tempState = { ...stateMap } // Find the action registration const actionReg = this.store?.actionContainer?.getAction( action.leaf, action.type ) // Is the reg invalid, i.e. reg not found and leaf + type set const actionRegInvalid = !actionReg && [action.leaf, action.type].every(isString) if (isDev && log.isDebugEnabled() && actionRegInvalid) { log.warn( `Unable to find action registration for: ${createLeafActionType( action.leaf, action.type )}`, action ) } if (isFunction(actionReg?.action)) { Option.ofNullable(tempState[action.leaf]).ifSome(reducerState => { const { leaf } = action, changed = new Flag(), checkReducerStateChange = createChangeDetector( leaf, reducerState, newState => { tempState = { ...tempState, [leaf]: newState } hasChanged = true }, changed ) // ActionMessage.reducers PROVIDED if (log.isDebugEnabled() && isDev) { log.debug("Action type supported", action.leaf, action.type) } if (action.stateType && reducerState instanceof action.stateType) { _get(action, "reducers", []).forEach(actionReducer => checkReducerStateChange(actionReducer(reducerState, action)) ) } // IF @ActionReducer REGISTERED if (actionReg?.options?.isReducer === true) { Option.ofNullable(actionReg.action(null, ...action.args)) .filter(isFunction) .match({ None: () => { throw new Error( `Action reducer did not return a function: ${actionReg.type}` ) }, Some: reducerFn => { if (log.isDebugEnabled() && isDev) { log.debug(`Calling action reducer: ${actionReg.fullName}`) } checkReducerStateChange(reducerFn(reducerState, tempState)) } }) } }) } // Iterate leaves and execute actions for (let reducer of this.reducers) { if (isFunction(reducer)) { const simpleReducer = reducer as any const simpleState = simpleReducer(tempState, action) if (simpleState !== tempState) { tempState = simpleState hasChanged = true } continue } const // Get the reducer leaf leaf = reducer.leaf(), // Get Current RAW state rawLeafState = tempState[leaf], // Shape it for the reducer startReducerState = rawLeafState, stateChangeDetected = new Flag() let reducerState = startReducerState try { /** * Check the returned state from every handler for changes * * @param newReducerState */ const checkReducerStateChange = createChangeDetector( leaf, reducerState, newState => { reducerState = newState }, stateChangeDetected ) // Check internal actions if (INTERNAL_ACTIONS.includes(action.type)) { if (log.isDebugEnabled() && isDev) { log.debug( `Sending init event to ${leaf} - internal action received ${action.type}` ) } if (INTERNAL_ACTION.INIT === action.type && reducer.init) { checkReducerStateChange(reducer.init(startReducerState)) } } // Check leaf of reducer and action to see if this reducer handles the supplied action if (action.leaf && action.leaf !== leaf) { continue } // CHECK REDUCER.HANDLE if (reducer.handle) { checkReducerStateChange(reducer.handle(reducerState, action)) } // CHECK ACTUAL REDUCER FOR SUPPORT if (isFunction(reducer[action.type])) { checkReducerStateChange( reducer[action.type](reducerState, ...action.args) ) } } catch (err) { log.error(`Error occurred on reducer leaf ${leaf}`, err) if (reducer.handleError) { reducer.handleError(startReducerState, action, err) } this.onError && this.onError(err, reducer) } if (stateChangeDetected) { tempState = { ...tempState, [leaf]: reducerState } hasChanged = true } } if (log.isDebugEnabled() && isDev) { log.debug( "Has changed after all reducers", hasChanged, "states equal", isEqualShallow(tempState, state) ) } return (hasChanged ? tempState : state) as S } catch (err) { log.error("Error bubbled to root reducer", err) // If error handler exists then use it if (this.onError) { this.onError && this.onError(err) return state as S } // Otherwise throw throw err } } } // Export the RootReducer class as the default export default RootReducer // export default (state:any,action:any):any => { // return rootReducer.handle(state as DefaultStateType, action as ActionMessage<any>) // }