UNPKG

typedux

Version:

Slightly adjusted Redux (awesome by default) for TS

274 lines 11.7 kB
import * as Immutable from "immutable"; import { getLogger } from "@3fv/logger-proxy"; 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 { 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(rootValue, leaf) { if (Immutable.Map.isMap(rootValue)) { return rootValue.get(leaf); } else { return _get(rootValue, leaf); } } /** * RootReducer for typedux apps * * Maps leaf reducers and decorated reducers * to the appropriate state functions */ export class RootReducer { /** * Create reducer * * @param rootStateType - type of root state, must be immutable map or record * @param reducers - list of all child reducers * @param store */ constructor(store, rootStateType = null, ...reducers) { this.store = store; this.rootStateType = rootStateType; // Internal list of all leaf reducers this.reducers = []; // handled actions ids to avoid duplication this.handledActionIds = []; 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)); } setOnError(onError) { this.onError = onError; return this; } /** * Create default state * * @param defaultStateValue - if provided then its used as base for inflation * @returns {State} */ defaultState(defaultStateValue = null) { // LOAD THE STATE AND VERIFY IT IS Immutable.Map/Record let state = defaultStateValue || { type: "ROOT" }; // ITERATE REDUCERS & CREATE LEAF STATES this.reducers .filter(reducer => isFunction(reducer.leaf)) .forEach(reducer => { const leaf = reducer.leaf(), leafDefaultState = getLeafValue(defaultStateValue, leaf); state = Object.assign(Object.assign({}, state), { [leaf]: reducer.defaultState(leafDefaultState || {}) }); }); return state; } /** * Create a generic handler for dispatches * * @returns {(state:S, action:ReduxAction)=>S} */ makeGenericHandler() { return (state, action) => this.handle(state, action); } /** * Handle action message * * @param state * @param action * @returns {State} */ handle(state, action) { var _a, _b; // 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; } // 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); 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 = (leaf, currentState, updateState, changed) => (newReducerState) => { 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; // Hold the interim state in `tempState` let tempState = Object.assign({}, stateMap); // Find the action registration const actionReg = (_b = (_a = this.store) === null || _a === void 0 ? void 0 : _a.actionContainer) === null || _b === void 0 ? void 0 : _b.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 === null || actionReg === void 0 ? void 0 : actionReg.action)) { Option.ofNullable(tempState[action.leaf]).ifSome(reducerState => { var _a; const { leaf } = action, changed = new Flag(), checkReducerStateChange = createChangeDetector(leaf, reducerState, newState => { tempState = Object.assign(Object.assign({}, 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 (((_a = actionReg === null || actionReg === void 0 ? void 0 : actionReg.options) === null || _a === void 0 ? void 0 : _a.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; 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 = Object.assign(Object.assign({}, 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); } 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; } // 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>) // } //# sourceMappingURL=RootReducer.js.map