1 | import { createLogger } from '@gongt/ts-stl-library/debug/create-logger';
|
2 | import { LOG_LEVEL } from '@gongt/ts-stl-library/debug/levels';
|
3 | import { GlobalVariable } from '@gongt/ts-stl-library/pattern/global-page-data';
|
4 | import { applyMiddleware, compose, createStore, Dispatch, Middleware, Reducer, Store, Unsubscribe, } from 'redux';
|
5 | import { ArrayOrSingle } from '../global';
|
6 | import { IAction, IAData } from './action';
|
7 | import { MyCombineReducers } from './combine-reducers';
|
8 | import { IState } from './preload-state';
|
9 | import { defaultMiddlewares } from './store.internal-middleware';
|
10 | import { IVirtualStore, IVirtualStoreConstructor } from './virtual-store';
|
11 |
|
12 | export type AppStore<StateInterface extends IState> = Store<StateInterface>;
|
13 |
|
14 | export type ReduxStoreType = ReduxStore<any>;
|
15 | export type LogicFunction<T> = (store: ReduxStore<T>) => void;
|
16 | export type BorderLogicFunction<T> = (
|
17 | subscribe: (listener: (state: T) => void) => Unsubscribe,
|
18 | dispatch: Dispatch<T>,
|
19 | ) => void;
|
20 |
|
21 | export type SimpleMiddleware<T, D extends IAData> = (store: AppStore<T>, action: IAction<D>)
|
22 | => void|IAction<D>|Promise<void|IAction<D>>;
|
23 |
|
24 | export const REDUX_PRELOAD_NAME = '__REDUX_PRELOAD_DATA__';
|
25 |
|
26 | const debug = createLogger(LOG_LEVEL.SILLY, 'redux-store');
|
27 |
|
28 | export interface MiddlewareCreator {
|
29 | (global: GlobalVariable): Middleware;
|
30 | }
|
31 |
|
32 | export interface IPlugin<State extends IState> {
|
33 | __redux_plugin(reduxStore: ReduxStore<State>): void;
|
34 | }
|
35 |
|
36 |
|
37 |
|
38 |
|
39 | export class ReduxStore<StateInterface extends IState> {
|
40 | public middlewares: Middleware[];
|
41 | public processObject: BorderLogicFunction<StateInterface>[] = [];
|
42 |
|
43 | public subStore: {[id: string]: IVirtualStore<any>} = {};
|
44 | protected readonly composeEnhancers = compose;
|
45 | private finished = false;
|
46 | private middleware_callbacks: MiddlewareCreator[] = [];
|
47 |
|
48 | constructor(logicRegister?: ArrayOrSingle<LogicFunction<StateInterface>>) {
|
49 | this.middlewares = defaultMiddlewares.slice();
|
50 | if (logicRegister) {
|
51 | if (Array.isArray(logicRegister)) {
|
52 | logicRegister.forEach((fn) => {
|
53 | fn(this);
|
54 | });
|
55 | } else {
|
56 | logicRegister(this);
|
57 | }
|
58 | }
|
59 | }
|
60 |
|
61 | createStore(global: GlobalVariable): AppStore<StateInterface> {
|
62 | if (global.has(REDUX_PRELOAD_NAME)) {
|
63 | const preloadOrStoreObject = global.get(REDUX_PRELOAD_NAME);
|
64 | if (typeof preloadOrStoreObject.getState === 'function') {
|
65 | debug(' create store: re-call, return last object.');
|
66 | return preloadOrStoreObject;
|
67 | }
|
68 | }
|
69 | this.finished = true;
|
70 | const applicationLogic = this.combineReducers();
|
71 | const dynamicMiddleware = this.middleware_callbacks.map((cb) => {
|
72 | return cb(global);
|
73 | });
|
74 | const appliedMiddleware = applyMiddleware(...this.middlewares, ...dynamicMiddleware);
|
75 |
|
76 | const createStoreEnhancer = this.composeEnhancers(appliedMiddleware);
|
77 | const enhancedCreateStore = createStoreEnhancer<any>(createStore);
|
78 |
|
79 | const preloadState = this.getPreloadState(global);
|
80 |
|
81 | Object.keys(this.subStore).forEach((key) => {
|
82 | const st = this.subStore[key];
|
83 |
|
84 | if (!preloadState.hasOwnProperty(key) && st.hasOwnProperty('defaultValue')) {
|
85 | preloadState[key] = st.defaultValue;
|
86 | }
|
87 | });
|
88 |
|
89 | if (debug.enabled) {
|
90 | debug(' preloadState=');
|
91 | for (let name of Object.keys(preloadState)) {
|
92 | const v = preloadState[name];
|
93 | debug(' %s -> %s', name, v.name || v.constructor.name || v);
|
94 | }
|
95 | }
|
96 |
|
97 | const store: Store<StateInterface> = enhancedCreateStore(applicationLogic, preloadState);
|
98 | store['toJSON'] = store.getState;
|
99 |
|
100 | this.processObject.forEach((fn) => {
|
101 | fn((f) => {
|
102 | f(store.getState());
|
103 | return store.subscribe(() => {
|
104 | f(store.getState());
|
105 | });
|
106 | }, (act: any) => {
|
107 | if (act && act['toJSON']) {
|
108 | act = act['toJSON']();
|
109 | }
|
110 | return store.dispatch(act);
|
111 | });
|
112 | });
|
113 |
|
114 | global.set(REDUX_PRELOAD_NAME, store);
|
115 | return store;
|
116 | }
|
117 |
|
118 |
|
119 | public logicDisplayBorder(middleware: BorderLogicFunction<StateInterface>) {
|
120 | if (this.finished) {
|
121 | throw new Error('ReduxStore: use middleware too late');
|
122 | }
|
123 | this.processObject.push(middleware);
|
124 | }
|
125 |
|
126 | public plugin(plugin: IPlugin<Partial<StateInterface>>) {
|
127 | plugin.__redux_plugin(this);
|
128 | }
|
129 |
|
130 |
|
131 | public register<T>(Constructor: IVirtualStoreConstructor<T>) {
|
132 | if (this.finished) {
|
133 | throw new Error('ReduxStore: register sub store too late');
|
134 | }
|
135 | const name = Constructor.name;
|
136 |
|
137 | if (this.subStore[name]) {
|
138 | throw new TypeError('duplicate store: ' + name);
|
139 | }
|
140 | this.subStore[name] = new Constructor();
|
141 | }
|
142 |
|
143 |
|
144 | public use(middleware: Middleware) {
|
145 | if (this.finished) {
|
146 | throw new Error('ReduxStore: use middleware too late');
|
147 | }
|
148 | this.middlewares.push(middleware);
|
149 | }
|
150 |
|
151 | public useDynamic(callback: MiddlewareCreator) {
|
152 | if (this.finished) {
|
153 | throw new Error('ReduxStore: use middleware too late');
|
154 | }
|
155 | this.middleware_callbacks.push(callback);
|
156 | }
|
157 |
|
158 | public useSimple<D extends IAData>(middleware: SimpleMiddleware<StateInterface, D>) {
|
159 | if (this.finished) {
|
160 | throw new Error('ReduxStore: use middleware too late');
|
161 | }
|
162 | const md: Middleware = <StateInterface>(state) => next => action => {
|
163 | const ret = middleware(state, action);
|
164 | if (!ret) {
|
165 | console.log('an action has been dropped: %s', action.type);
|
166 | return null;
|
167 | } else if (ret instanceof Promise) {
|
168 | ret.then((act) => {
|
169 | if (act) {
|
170 | next(act);
|
171 | } else {
|
172 | console.log('%can action has been dropped (after sync): %s', 'color:grey', action.type);
|
173 | }
|
174 | }, (e) => {
|
175 | console.error(e);
|
176 | throw new Error('simple middleware promise rejected.');
|
177 | });
|
178 | return null;
|
179 | } else {
|
180 | return next(action);
|
181 | }
|
182 | };
|
183 | this.middlewares.push(md);
|
184 | }
|
185 |
|
186 | protected combineReducers(): Reducer<StateInterface> {
|
187 | const pass = [];
|
188 | for (const key of Object.keys(this.subStore)) {
|
189 | const st = this.subStore[key];
|
190 | pass.push(...st.getReducers());
|
191 | }
|
192 | return MyCombineReducers(pass);
|
193 | }
|
194 |
|
195 | protected getPreloadState(global: GlobalVariable) {
|
196 | if (global.has(REDUX_PRELOAD_NAME)) {
|
197 | const pl = global.get(REDUX_PRELOAD_NAME);
|
198 | global.unset(REDUX_PRELOAD_NAME);
|
199 | return pl;
|
200 | }
|
201 | return {};
|
202 | }
|
203 | }
|