-
Notifications
You must be signed in to change notification settings - Fork 580
/
persistence.ts
101 lines (88 loc) · 2.98 KB
/
persistence.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
import AsyncStorage from "@react-native-async-storage/async-storage"
import { State } from "easy-peasy"
import { isArray, isBoolean, isNull, isNumber, isPlainObject, isString, throttle } from "lodash"
import { Middleware } from "redux"
import { GlobalStoreModel } from "./GlobalStoreModel"
import { migrate } from "./migration"
export const SESSION_KEY = "sessionState"
export const STORAGE_KEY = "artsy-app-state"
export const LEGACY_SEARCH_STORAGE_KEY = "SEARCH/RECENT_SEARCHES"
/**
* Removes sessionState and computed properties.
*/
export function sanitize(object: unknown, path: Array<string | number> = []): unknown {
if (isPlainObject(object)) {
const result = {} as any
for (const key of Object.keys(object as any)) {
// ignore computed properties, sessionState
if (key !== SESSION_KEY && !Object.getOwnPropertyDescriptor(object, key)?.get) {
result[key] = sanitize((object as any)[key], [...path, key])
}
}
return result
} else if (isArray(object)) {
return object.map((elem, i) => sanitize(elem, [...path, i]))
} else if (isNumber(object) || isString(object) || isNull(object) || isBoolean(object)) {
return object
} else {
console.error(`Cannot serialize value at path ${path.join(".")}: ${object}`)
return null
}
}
export function assignDeep(object: any, otherObject: any) {
for (const k of Object.keys(otherObject)) {
if (!(k in object) || !(isPlainObject(object[k]) && isPlainObject(otherObject[k]))) {
object[k] = otherObject[k]
} else {
assignDeep(object[k], otherObject[k])
}
}
}
export async function persist(globalStoreState: State<GlobalStoreModel>) {
return await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(sanitize(globalStoreState)))
}
async function loadLegacySearchState() {
const json = await AsyncStorage.getItem(LEGACY_SEARCH_STORAGE_KEY)
if (json) {
await AsyncStorage.removeItem(LEGACY_SEARCH_STORAGE_KEY)
try {
const result = JSON.parse(json)
if (Array.isArray(result)) {
return result
}
} catch (e) {
// noop
}
}
return null
}
export async function unpersist(): Promise<DeepPartial<State<GlobalStoreModel>>> {
const json = await AsyncStorage.getItem(STORAGE_KEY)
try {
const result = (json ? migrate({ state: JSON.parse(json) }) : {}) as State<GlobalStoreModel>
const recentSearches = await loadLegacySearchState()
if (recentSearches) {
result.search = {
...result.search,
recentSearches,
}
}
return result
} catch (e) {
if (!__TEST__) {
console.error(e)
}
return {}
}
}
export const persistenceMiddleware: Middleware = (store) => {
const throttledPersist = throttle(persist, 1000, { leading: false, trailing: true })
return (next) => (action) => {
const result = next(action)
// use requestAnimationFrame to make doubly sure we avoid blocking UI updates
requestAnimationFrame(() => {
throttledPersist(store.getState())
})
return result
}
}