-
Notifications
You must be signed in to change notification settings - Fork 4
/
index.ts
244 lines (217 loc) · 7.82 KB
/
index.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
/**
* @author Luke Brandon Farrell
* @description Middleware used for persisting redux state.
*/
import _pickBy from "lodash/pickBy";
import _map from "lodash/map";
import _get from "lodash/get";
import _isNil from "lodash/isNil";
import _startCase from "lodash/startCase";
import _isEqual from "lodash/isEqual";
export type SaveCallback = (key: string, state: object) => void;
export type LoadCallback = (key: string) => Promise<object>;
/**
* Keeps track of the current values of the state.
*
* @type {Array}
*/
let currentValue: Array<any> = [];
/**
* Stores our save method
*
* @type {func}
*/
let saveMethod: SaveCallback;
/**
* Stores our load method
*
* @type {func}
*/
let loadMethod: LoadCallback;
/**
* Middleware to load persisted state data
*
* @param save - Async function used to save the state to the storage
* @param load - Async function used to load the state from the storage
*
* @return {function(*): function(*=): *}
*/
function persistMiddleware() {
return (next: any) => async (action: any) => {
// Gets the our trigger actions
const currentValueActionAndKeys: Array<any> = Object.entries(currentValue)
.map((item: any) => {
return {
action: item[1].action, key: item[1].key
}
});
const targetActionAndKey: any = currentValueActionAndKeys
.filter(item => (item.action === action.type))[0];
// Only run this code for our defined load actions
if (!_isNil(targetActionAndKey)) {
const { key: asyncStorageKey } = targetActionAndKey;
// If target is nil, then no need to attempt to load from async storage
if (!_isNil(asyncStorageKey)) {
// Invoke our load function on the target key
let payload = await loadMethod(asyncStorageKey);
// Merge the payload received from our load function
action.payload = { ...payload, ...action.payload };
// Update our current value target to isLoaded = true
currentValue[asyncStorageKey] = {
..._get(currentValue, asyncStorageKey, {}),
isLoaded: true
};
} else {
action.payload = { ...action.payload };
}
}
return next(action);
};
}
declare namespace persistMiddleware {
// Attaches the listener to the store
// and loads the automatic reducers
let run: (store: any) => void;
}
/**
* Persist Tree - Method to persist state data
*
* @param structure - The Structure describes the parts of the state we want
* to persist.
* @param store - Redux Store
* @param debug - Debug data to the console
*/
export function createPersistMachine(structure: any, save: SaveCallback, load: LoadCallback, debug: boolean) {
// Assign our save and load methods
saveMethod = save;
loadMethod = load;
persistMiddleware.run = (store: any) => {
/**
* Handles state changes
*
* Saves the state values defined in structure when thats
* slice of the state is updated.
*/
async function handleChange() {
/*
* We map the structure to support multiple
* reducers. The `object` contains the keys that are
* declared to be saved. The `name` is the key for
* that reducer.
*/
await _map(structure, async (object: any, name: any) => {
// A key to keep the state mapping static
const { key: asyncStorageKey } = object;
// Get the state values we want to map.
const stateValues = object.values;
// Get the object we want from the currentValue
const currentValueObject = currentValue[asyncStorageKey];
// Gets the current state
const state = select(store.getState(), name);
// Gets the previous state
const previousValue = _get(currentValueObject, "state", null);
/*
* Builds a state only containing the values
* we care about, which are defined in structure.
*/
const newState = _pickBy(state, (value: any, key: any) => {
/**
* If nothing is passed to the `values`
* parameter, all values will be used.
*/
if (_isNil(stateValues)) return value;
if (stateValues.includes(key)) return value;
});
// Merges our newState object into our currentValues by key
currentValue[asyncStorageKey] = {
..._get(currentValue, asyncStorageKey, {}),
key: asyncStorageKey,
state: newState,
};
if (!_isEqual(previousValue, newState)) {
/*
* Each reducers value will have an `isLoaded` property, this
* allows us to keep track on a individual level
* if the reducer has been loaded. We don't want to
* save the reducer if it has not been loaded yet.
*/
if (currentValue[asyncStorageKey].isLoaded) {
if (debug) console.log(`SAVED: ${asyncStorageKey}`, newState);
await saveMethod(asyncStorageKey, newState);
}
}
});
}
/*
* Note that the .subscribe function returns a unsubscribe method if we
* ever need to unsubscribe from state updates.
*/
store.subscribe(handleChange);
loadAutomaticReducers(store);
}
/*
* We do an initial map of the structure
*/
_map(structure, (object: any, name: any) => {
// Catch any errors with the persist configuration
if (_isNil(object.key)) throw new Error("You need to define a `key` value to identify your data in your persist object.");
// Get the static key for mapping
const {
key: asyncStorageKey,
automatic = true
} = object;
// Builds the type from the reducer name, if a type has not been explicitly defined through the `action` value
const action = object.action || getPersistMachineAction(name);
// Initialize and empty currentValue, this is used to keep track of previous values
currentValue[asyncStorageKey] = {
..._get(currentValue, name, {}),
key: asyncStorageKey,
action,
automatic,
isLoaded: false
};
});
// If debug, we to log all the actions for loading the state
if (debug) {
_map(structure, async (object: any, name: any) => {
console.log(object.action || getPersistMachineAction(name));
});
}
return persistMiddleware;
}
/**
* Dispatch actions to automatically load
* all reducers that the `automatic`
* property was set to true.
* @param store Redux store
*/
function loadAutomaticReducers(store: any) {
Object.entries(currentValue)
.forEach((item: any) => {
if (item[1].automatic) {
store.dispatch({
type: item[1].action
})
}
});
}
/**
* Selects our nested property
*
* @param state
* @param key
*
* @return {*}
*/
function select(state: any, key: any): object {
return _get(state, key, {});
}
/**
* Builds an action type.
* e.g. transforms "data.adminAuth" into @ReduxPM/LoadDataAdminAuth
*
* @param {string} key the key to generate the action name
*/
export function getPersistMachineAction(key: string): string {
return `@ReduxPM/Load${_startCase(key).split(" ").join("")}`
}