UNPKG

7.67 kBPlain TextView Raw
1import {IS_CLIENT} from "@gongt/ts-stl-library/check-environment";
2import {createLogger} from "@gongt/ts-stl-library/debug/create-logger";
3import {LOG_LEVEL} from "@gongt/ts-stl-library/debug/levels";
4import {AnyAction, Reducer} from "redux";
5import {IAction, IAData, ISingleReducer} from "./action";
6
7export type AnyStore = any;
8export type AnyActionData = any;
9
10export interface IReducerInfo<T=AnyStore> {
11 callback: ISingleReducer<T, AnyActionData>;
12 actionName: string; // action name
13 storeName: string; // store name
14 global: boolean;
15}
16
17export interface IReducerMap {
18 [actionName: string]: {
19 [storeName: string]: {
20 local: ISingleReducer<AnyStore, AnyActionData>[];
21 global: ISingleReducer<AnyStore, AnyActionData>[];
22 };
23 };
24}
25
26export interface ISingleReducerChanging<ValueInterface, IData extends IAData> {
27 (part: string, state: ValueInterface, rawAction: IAction<IData>): boolean;
28
29 displayName?: string;
30}
31
32export interface IReducerMapCombined {
33 [actionName: string]: {
34 [storeName: string]: ISingleReducerChanging<AnyStore, AnyActionData>;
35 };
36}
37
38function callAll<T>(reducers: ISingleReducerChanging<T, AnyActionData>[]): ISingleReducerChanging<T, AnyAction> {
39 return function callAllWrapper(part: string, state: any, action) {
40 let changed = false;
41 for (const reducer of reducers) {
42 debug('[all] call reducer (%s): %s', reducers.length, reducer.displayName || reducer.name);
43 const r = reducer(part, state, action);
44 if (r) {
45 debug(' state changed.');
46 changed = true;
47 }
48 }
49 return changed;
50 };
51}
52
53function callOnlyOne<T>(reducers: ISingleReducer<T, AnyActionData>[],
54 storeName: string): ISingleReducerChanging<T, AnyAction> {
55 return function callOneWrapper(part: string, state: any, action) {
56 for (const reducer of reducers) {
57 debug('[one] call reducer: %s -> %s', storeName, reducer.displayName || reducer.name);
58 const ret = reducer(state[storeName], action.payload, action);
59 if (ret) {
60 debug(' action handled: %O', ret);
61 if (state[storeName] !== ret) {
62 state[storeName] = ret;
63 }
64 return true;
65 }
66 }
67 return false;
68 };
69}
70
71function combineAll(reducers: IReducerMap): IReducerMapCombined {
72 const ret: IReducerMapCombined = {};
73 debugStart('Init Actions');
74 const debugEnd = debugStart.enabled? console.groupEnd.bind(console) : () => null;
75 const statics = [];
76 for (const act of Object.keys(reducers)) {
77 ret[act] = {};
78 const allCallbackCurrentAct = [];
79 for (const sto of Object.keys(reducers[act])) {
80 const globals = reducers[act][sto].global;
81 const locals = reducers[act][sto].local;
82 if (locals.length) {
83 ret[act][sto] = callOnlyOne(locals, sto);
84 if (debugStart.enabled) {
85 statics.push({action: act, store: sto, type: 'local', listeners: locals.length,});
86 }
87 }
88 if (globals.length) {
89 allCallbackCurrentAct.push(callOnlyOne(globals, sto));
90 if (debugStart.enabled) {
91 statics.push({action: act, store: sto, type: 'global', listeners: globals.length,});
92 }
93 }
94 }
95 if (allCallbackCurrentAct.length) {
96 ret[act]['*'] = callAll(allCallbackCurrentAct);
97
98 if (debugStart.enabled) {
99 statics.push({action: act, store: '*', type: 'global', listeners: allCallbackCurrentAct.length,});
100 }
101 }
102 }
103
104 if (debugStart.enabled) {
105 console.table(statics);
106 }
107 debugEnd();
108 return ret;
109}
110
111function createStructure(reducers: IReducerInfo[]): IReducerMap {
112 const data: IReducerMap = {};
113 for (const info of reducers) {
114 const {actionName: act, storeName: sto} = info;
115 if (!data[act]) {
116 data[act] = {};
117 }
118 if (!data[act][sto]) {
119 data[act][sto] = {local: [], global: []};
120 }
121 const mod = info.global? 'global' : 'local';
122 data[act][sto][mod].push(info.callback);
123 }
124 return data;
125}
126
127export function reducerFromNative<T>(native: Reducer<T>): ISingleReducer<T, any> {
128 if (native['__wrappedNativeReducer']) {
129 return native;
130 }
131 const nativeReducerWrapper = function (state, action, rawAction) {
132 debug('call native reducer: %s[%O]', native['displayName'] || native.name, native);
133 const ret = native(state, rawAction);
134 if (ret === state) {
135 return undefined;
136 }
137 return ret;
138 };
139 nativeReducerWrapper['displayName'] = `nativeReduce(${native['displayName'] || native.name})`;
140 nativeReducerWrapper['__wrappedNativeReducer'] = true;
141 return nativeReducerWrapper;
142}
143
144const debugStart = createLogger(LOG_LEVEL.DEBUG, 'reducer');
145if (IS_CLIENT) {
146 debugStart.fn = console.groupCollapsed.bind(console);
147}
148const debug = createLogger(LOG_LEVEL.INFO, 'reducer');
149
150export function MyCombineReducers<AppState, T=keyof AppState>(reducers: IReducerInfo<T>[],
151 toplevel?: Reducer<AppState>): Reducer<AppState> {
152
153 for (const item of reducers) {
154 if (!item.actionName || !item.storeName || !item.callback) {
155 console.error(item);
156 throw new TypeError('final check: invalid reducer. more info see console.');
157 }
158 }
159 const reducersData = createStructure(reducers);
160 const reducersCombine = combineAll(reducersData);
161 // const storeList = reducers.map(info => info.storeName);
162
163 return (state: AppState, action: IAction<any>) => {
164 if (!action || !action.type) {
165 throw new TypeError('dispatching action without type.');
166 }
167 debugStart('action: %s @ %s', action.type, action.virtualStorage || 'Nil');
168 const debugEnd = debugStart.enabled? console.groupEnd.bind(console) : () => null;
169 debug(action);
170
171 try {
172 if (toplevel) { // topLevel: original redux top level style reducer
173 const newState = toplevel(state, action);
174 if (state !== newState) {
175 debug('handled by top level reducer - finish');
176 debugEnd();
177 return newState;
178 }
179 }
180
181 let {type: actionType, virtualStorage: store, payload} = action;
182 if (!reducersCombine[actionType]) {
183 debug('unknown action - finish');
184 debugEnd();
185 return state;
186 }
187
188 if (!action.hasOwnProperty('payload')) {
189 debug('action has no payload');
190 debugEnd();
191 return state; // this is normal action
192 }
193
194 let changed = false;
195 const currentReducers = reducersCombine[actionType];
196 const storeName = store;
197 debug('sub store name: %s', storeName);
198
199 /**
200 * global reducers:
201 * only call reducer witch:
202 * 1. action is watching by reducer
203 * if any reducer modified the state:
204 * NO any more reducer will called within this virtual store
205 * every action may or may-not call multiple global reducer.
206 * every action will call at most only one global reducer on each virtual store
207 */
208 const global = currentReducers['*'];
209 if (global) { // global: call any store's reducer, but
210 debug(' -> call global reducers');
211 const thisChanged = global(storeName, state, action);
212 changed = thisChanged || changed;
213 } else {
214 debug(' -> no global reducer');
215 }
216
217 /**
218 * local reducers:
219 * only call reducer witch:
220 * 1. action is watching by reducer
221 * 2. action.store equals to reducer.store
222 * if any reducer modified the state:
223 * NO any more reducer will called
224 * every action will only call one local reducer.
225 */
226 const local = currentReducers[store];
227 if (local) {
228 debug(' -> call local reducers');
229 const thisChanged = local(storeName, state, action);
230 changed = thisChanged || changed;
231 } else {
232 debug(' -> no local reducer');
233 }
234
235 debug('state %s', changed? 'changed' : 'NOT changed');
236 debugEnd();
237 return changed? Object.assign({}, state) : state;
238 } catch (e) {
239 debugEnd();
240 debugEnd();
241 debugEnd();
242 debug('error while processing action: %s', e? e.message : 'no info');
243 throw e;
244 }
245 };
246}