import {IS_CLIENT} from "@gongt/ts-stl-library/check-environment"; import {createLogger} from "@gongt/ts-stl-library/debug/create-logger"; import {LOG_LEVEL} from "@gongt/ts-stl-library/debug/levels"; import {AnyAction, Reducer} from "redux"; import {IAction, IAData, ISingleReducer} from "./action"; export type AnyStore = any; export type AnyActionData = any; export interface IReducerInfo { callback: ISingleReducer; actionName: string; // action name storeName: string; // store name global: boolean; } export interface IReducerMap { [actionName: string]: { [storeName: string]: { local: ISingleReducer[]; global: ISingleReducer[]; }; }; } export interface ISingleReducerChanging { (part: string, state: ValueInterface, rawAction: IAction): boolean; displayName?: string; } export interface IReducerMapCombined { [actionName: string]: { [storeName: string]: ISingleReducerChanging; }; } function callAll(reducers: ISingleReducerChanging[]): ISingleReducerChanging { return function callAllWrapper(part: string, state: any, action) { let changed = false; for (const reducer of reducers) { debug('[all] call reducer (%s): %s', reducers.length, reducer.displayName || reducer.name); const r = reducer(part, state, action); if (r) { debug(' state changed.'); changed = true; } } return changed; }; } function callOnlyOne(reducers: ISingleReducer[], storeName: string): ISingleReducerChanging { return function callOneWrapper(part: string, state: any, action) { for (const reducer of reducers) { debug('[one] call reducer: %s -> %s', storeName, reducer.displayName || reducer.name); const ret = reducer(state[storeName], action.payload, action); if (ret) { debug(' action handled: %O', ret); if (state[storeName] !== ret) { state[storeName] = ret; } return true; } } return false; }; } function combineAll(reducers: IReducerMap): IReducerMapCombined { const ret: IReducerMapCombined = {}; debugStart('Init Actions'); const debugEnd = debugStart.enabled? console.groupEnd.bind(console) : () => null; const statics = []; for (const act of Object.keys(reducers)) { ret[act] = {}; const allCallbackCurrentAct = []; for (const sto of Object.keys(reducers[act])) { const globals = reducers[act][sto].global; const locals = reducers[act][sto].local; if (locals.length) { ret[act][sto] = callOnlyOne(locals, sto); if (debugStart.enabled) { statics.push({action: act, store: sto, type: 'local', listeners: locals.length,}); } } if (globals.length) { allCallbackCurrentAct.push(callOnlyOne(globals, sto)); if (debugStart.enabled) { statics.push({action: act, store: sto, type: 'global', listeners: globals.length,}); } } } if (allCallbackCurrentAct.length) { ret[act]['*'] = callAll(allCallbackCurrentAct); if (debugStart.enabled) { statics.push({action: act, store: '*', type: 'global', listeners: allCallbackCurrentAct.length,}); } } } if (debugStart.enabled) { console.table(statics); } debugEnd(); return ret; } function createStructure(reducers: IReducerInfo[]): IReducerMap { const data: IReducerMap = {}; for (const info of reducers) { const {actionName: act, storeName: sto} = info; if (!data[act]) { data[act] = {}; } if (!data[act][sto]) { data[act][sto] = {local: [], global: []}; } const mod = info.global? 'global' : 'local'; data[act][sto][mod].push(info.callback); } return data; } export function reducerFromNative(native: Reducer): ISingleReducer { if (native['__wrappedNativeReducer']) { return native; } const nativeReducerWrapper = function (state, action, rawAction) { debug('call native reducer: %s[%O]', native['displayName'] || native.name, native); const ret = native(state, rawAction); if (ret === state) { return undefined; } return ret; }; nativeReducerWrapper['displayName'] = `nativeReduce(${native['displayName'] || native.name})`; nativeReducerWrapper['__wrappedNativeReducer'] = true; return nativeReducerWrapper; } const debugStart = createLogger(LOG_LEVEL.DEBUG, 'reducer'); if (IS_CLIENT) { debugStart.fn = console.groupCollapsed.bind(console); } const debug = createLogger(LOG_LEVEL.INFO, 'reducer'); export function MyCombineReducers(reducers: IReducerInfo[], toplevel?: Reducer): Reducer { for (const item of reducers) { if (!item.actionName || !item.storeName || !item.callback) { console.error(item); throw new TypeError('final check: invalid reducer. more info see console.'); } } const reducersData = createStructure(reducers); const reducersCombine = combineAll(reducersData); // const storeList = reducers.map(info => info.storeName); return (state: AppState, action: IAction) => { if (!action || !action.type) { throw new TypeError('dispatching action without type.'); } debugStart('action: %s @ %s', action.type, action.virtualStorage || 'Nil'); const debugEnd = debugStart.enabled? console.groupEnd.bind(console) : () => null; debug(action); try { if (toplevel) { // topLevel: original redux top level style reducer const newState = toplevel(state, action); if (state !== newState) { debug('handled by top level reducer - finish'); debugEnd(); return newState; } } let {type: actionType, virtualStorage: store, payload} = action; if (!reducersCombine[actionType]) { debug('unknown action - finish'); debugEnd(); return state; } if (!action.hasOwnProperty('payload')) { debug('action has no payload'); debugEnd(); return state; // this is normal action } let changed = false; const currentReducers = reducersCombine[actionType]; const storeName = store; debug('sub store name: %s', storeName); /** * global reducers: * only call reducer witch: * 1. action is watching by reducer * if any reducer modified the state: * NO any more reducer will called within this virtual store * every action may or may-not call multiple global reducer. * every action will call at most only one global reducer on each virtual store */ const global = currentReducers['*']; if (global) { // global: call any store's reducer, but debug(' -> call global reducers'); const thisChanged = global(storeName, state, action); changed = thisChanged || changed; } else { debug(' -> no global reducer'); } /** * local reducers: * only call reducer witch: * 1. action is watching by reducer * 2. action.store equals to reducer.store * if any reducer modified the state: * NO any more reducer will called * every action will only call one local reducer. */ const local = currentReducers[store]; if (local) { debug(' -> call local reducers'); const thisChanged = local(storeName, state, action); changed = thisChanged || changed; } else { debug(' -> no local reducer'); } debug('state %s', changed? 'changed' : 'NOT changed'); debugEnd(); return changed? Object.assign({}, state) : state; } catch (e) { debugEnd(); debugEnd(); debugEnd(); debug('error while processing action: %s', e? e.message : 'no info'); throw e; } }; }