1 | import {IS_CLIENT} from "@gongt/ts-stl-library/check-environment";
|
2 | import {createLogger} from "@gongt/ts-stl-library/debug/create-logger";
|
3 | import {LOG_LEVEL} from "@gongt/ts-stl-library/debug/levels";
|
4 | import {AnyAction, Reducer} from "redux";
|
5 | import {IAction, IAData, ISingleReducer} from "./action";
|
6 |
|
7 | export type AnyStore = any;
|
8 | export type AnyActionData = any;
|
9 |
|
10 | export interface IReducerInfo<T=AnyStore> {
|
11 | callback: ISingleReducer<T, AnyActionData>;
|
12 | actionName: string;
|
13 | storeName: string;
|
14 | global: boolean;
|
15 | }
|
16 |
|
17 | export interface IReducerMap {
|
18 | [actionName: string]: {
|
19 | [storeName: string]: {
|
20 | local: ISingleReducer<AnyStore, AnyActionData>[];
|
21 | global: ISingleReducer<AnyStore, AnyActionData>[];
|
22 | };
|
23 | };
|
24 | }
|
25 |
|
26 | export interface ISingleReducerChanging<ValueInterface, IData extends IAData> {
|
27 | (part: string, state: ValueInterface, rawAction: IAction<IData>): boolean;
|
28 |
|
29 | displayName?: string;
|
30 | }
|
31 |
|
32 | export interface IReducerMapCombined {
|
33 | [actionName: string]: {
|
34 | [storeName: string]: ISingleReducerChanging<AnyStore, AnyActionData>;
|
35 | };
|
36 | }
|
37 |
|
38 | function 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 |
|
53 | function 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 |
|
71 | function 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 |
|
111 | function 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 |
|
127 | export 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 |
|
144 | const debugStart = createLogger(LOG_LEVEL.DEBUG, 'reducer');
|
145 | if (IS_CLIENT) {
|
146 | debugStart.fn = console.groupCollapsed.bind(console);
|
147 | }
|
148 | const debug = createLogger(LOG_LEVEL.INFO, 'reducer');
|
149 |
|
150 | export 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 |
|
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) {
|
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;
|
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 |
|
201 |
|
202 |
|
203 |
|
204 |
|
205 |
|
206 |
|
207 |
|
208 | const global = currentReducers['*'];
|
209 | if (global) {
|
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 |
|
219 |
|
220 |
|
221 |
|
222 |
|
223 |
|
224 |
|
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 | }
|