@gongt/ts-stl-client
Version:
247 lines (222 loc) • 7.67 kB
text/typescript
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<T=AnyStore> {
callback: ISingleReducer<T, AnyActionData>;
actionName: string; // action name
storeName: string; // store name
global: boolean;
}
export interface IReducerMap {
[actionName: string]: {
[storeName: string]: {
local: ISingleReducer<AnyStore, AnyActionData>[];
global: ISingleReducer<AnyStore, AnyActionData>[];
};
};
}
export interface ISingleReducerChanging<ValueInterface, IData extends IAData> {
(part: string, state: ValueInterface, rawAction: IAction<IData>): boolean;
displayName?: string;
}
export interface IReducerMapCombined {
[actionName: string]: {
[storeName: string]: ISingleReducerChanging<AnyStore, AnyActionData>;
};
}
function callAll<T>(reducers: ISingleReducerChanging<T, AnyActionData>[]): ISingleReducerChanging<T, AnyAction> {
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<T>(reducers: ISingleReducer<T, AnyActionData>[],
storeName: string): ISingleReducerChanging<T, AnyAction> {
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<T>(native: Reducer<T>): ISingleReducer<T, any> {
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<AppState, T=keyof AppState>(reducers: IReducerInfo<T>[],
toplevel?: Reducer<AppState>): Reducer<AppState> {
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<any>) => {
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;
}
};
}