-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathstorageHelper.js
333 lines (308 loc) · 10.5 KB
/
storageHelper.js
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
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
/*
* Storage Helper: helper funtions to handle all interactions with browser's localStorage including backup and restore.
*/
import DataStorage, { rxForceUpdateCache } from './DataStorage'
import {
downloadFile,
fallbackIfFails,
generateHash,
hasValue,
isFn,
isMap,
isObj,
isSet,
isStr,
isValidDate,
objClean,
} from './utils'
// Local Storage item key prefix for all items
const PREFIX = fallbackIfFails(() => process.env.STORAGE_HELPER_PREFIX) || 'totem_'
const PREFIX_STATIC = PREFIX + 'static_'
export const CACHE_KEY = PREFIX + 'cache'
export const SETTINGS_KEY = PREFIX + 'settings'
export const MODULE_SETTINGS_KEY = 'module_settings'
const storage = {}
const cache = new DataStorage(CACHE_KEY, true) // directly read and write from storage
const settings = new DataStorage(SETTINGS_KEY)
// LocalStorage items that are essential for the applicaiton to run.
export const essentialKeys = [
'chat-history', // chat history
'contacts',
'history', // user activity history
'identities',
'locations',
// notifications are essential because user may need to respond to them in case they are migrating to a new device.
'notifications',
'partners',
'settings',
].map(x => `${PREFIX}${x}`)
// Storage items that are to include a timestamp after being backed up
export const modulesWithTS = [
'contacts',
'identities',
'locations',
'partners',
].map(x => `${PREFIX}${x}`)
/**
* @name rw
* @summary Read/write to storage
*
* @param {DataStorage} storage
* @param {String} key module/item key
* @param {String|null} propKey name of the property to read/write to.
* If null, will remove all data stored for the @key
* If not supplied, will return value for the @key
* @param {*} value If not specified, will return value for @propKey
* If null, will remove value for @propKey
* If Map or Set supplied, will be converted to array using `Array.from`.
* If Object supplied, will merge with existing values.
* @param {Boolean} override If @value is an Object, whether to override or merge with existing value.
* Default: false
*
* @returns {*}
*/
export const rw = (storage, key, propKey, value, override = false) => {
if (!storage || !key) return {}
const data = storage.get(key) || {}
if (!isStr(propKey) && propKey !== null) return data
if (propKey === null) {
return storage.delete(key)
} else if (value === null) {
// remove from storage
delete data[propKey]
} else if (isMap(value) || isSet(value)) {
// convert map to array. PS: may need to convert back to Map on retrieval
data[propKey] = Array.from(value)
} else if (isObj(value)) {
// merge with existing value
data[propKey] = override
? value
: { ...data[propKey], ...value }
} else if (hasValue(value)) {
data[propKey] = value
} else {
// nothing to save | read-only operation
return data[propKey]
}
storage.set(key, data)
return data[propKey]
}
export const backup = {
/**
* @name backup.download
* @summary download backup of application data
*
* @param {String} filename (optional) Default: generated name with domain and timestamp
* @param {Function} dataModifier function to modify/encrypt downloadable data/object
* Args: Object
* Expected return: String/Object
*
* @returns {Array}
* [
* content String:
* timestamp String:
* fileName String:
* ]
*/
download: (filename, dataModifier = null, encrypted) => {
filename ??= backup.generateFilename(encrypted)
const timestamp = backup.filenameToTS(filename)
let data = backup.generateData(timestamp)
// add filename hash to the backup to force user to upload the exact same file
data._file = generateHash(filename, 'blake2', 32).slice(2)
data = isFn(dataModifier)
? dataModifier(data)
: data
const content = isStr(data)
? data
: JSON.stringify(data)
downloadFile(
content,
filename,
'application/json',
)
return {
data,
hash: generateHash(content, 'blake2', 256),
timestamp,
filename,
}
},
/**
* @name backup.filenameToTS
* @summary extract timestamp from the backup filename
*
* @returns {String}
*/
filenameToTS: (filename) => `${filename || ''}`
.split('backup-')[1]
.replace('-encrypted', '') // remove encrypted indicator from filename
.split('.json')[0],
/**
* @name backup.generateData
* @summary generates an object for backup only using essential data from localStorage
*
* @params {String} timestamp
*
* @returns {Object}
*/
generateData: (timestamp = new Date().toISOString()) => {
// data to be backed up
const data = objClean(localStorage, essentialKeys)
Object
.keys(data)
.forEach(key => {
data[key] = JSON.parse(data[key])
if (!timestamp) return
if (key === SETTINGS_KEY) {
const { messaging = {} } = data[SETTINGS_KEY]
.find(([key]) => key === MODULE_SETTINGS_KEY)[1]
|| {}
messaging.user = {
...messaging.user,
fileBackupTS: timestamp,
}
}
if (!modulesWithTS.includes(key)) return
// update backup timestamp
data[key].forEach(([_, entry]) =>
entry.fileBackupTS = timestamp
)
})
return data
},
/**
* @name backup.generateFileName
* @summary generates a backup filename using current timestamp and URL hostname
*
* @param {Boolean} encrypted whether backup is encrypted
* @param {String} timestamp
*
* @returns {String}
*/
generateFilename: (encrypted = false, timestamp = new Date().toISOString()) => {
const hostname = window.location.hostname === 'localhost'
? 'totem-localhost'
: window.location.hostname
const parts = [
hostname,
'backup',
timestamp,
encrypted && 'encrypted',
]
.filter(Boolean)
.join('-')
const fileName = `${parts}.json`
return fileName
},
/**
* @name backup.updateTS
* @summary update backup timestamps of module data (eg: identities, partners).
* This should only be invoked after backup download has been confirmed.
*
* @param {Object} data parsed replica of localStorage with only the keys that are to be backed up
* @param {String} timestamp ISO timestamp to be set as the backup datetime
*
* @returns {Object} data
*/
updateFileBackupTS: (timestamp) => {
if (!isValidDate(timestamp)) throw new Error('invalid timestamp')
// set timestamp for individual storage entries
modulesWithTS.forEach(moduleKey => {
const moduleStorage = new DataStorage(moduleKey)
const updated = moduleStorage
.map(([key, value]) => ([key, {
...value,
fileBackupTS: timestamp,
}]))
moduleStorage.setAll(new Map(updated), true)
})
// set timestamp on user credentials
const user = {
...storage
.settings
.module('messaging')
.user
|| {},
fileBackupTS: timestamp,
}
!!user.id && storage.settings.module('messaging', { user })
// update modules
rxForceUpdateCache.next(modulesWithTS)
},
}
storage.backup = backup
storage.countries = new DataStorage(PREFIX_STATIC + 'countries', true)
storage.settings = {
// global settings
//
// Params:
// @itemKey string: unique identifier for target module or item (if not part of any module)
// @value object: (optional) settings/value to replace existing.
global: (itemKey, value) => rw(settings, 'global_settings', itemKey, value),
/**
* @name storage.settings.module
* @summary read/write module related settings to localStorage
*
* @param {String} moduleKey a unique identifier for the module
* @param {*} value
* @param {Boolean} override if @value is an Object, whether to override or merge with existing value.
* Default: false
*
* @returns {*} returns the saved value
*/
module: (moduleKey, value, override = false) => rw(settings, MODULE_SETTINGS_KEY, moduleKey, value, override)
}
/**
* @name storage.cache
* @summary read/write to module cache storage
*
* @param {String} moduleKey
* @param {String|null} itemKey
* @param {*|null} value
*
* @returns {*}
*/
storage.cache = (moduleKey, itemKey, value) => rw(
cache,
moduleKey,
itemKey,
value
)
/**
* @name storage.cacheDelete
* @summary remove all cached data for a module
*
* @param {String} moduleKey
*/
storage.cacheDelete = moduleKey => rw(cache, moduleKey, null)
// removes cache and static data
// Caution: can remove
storage.clearNonEssentialData = () => {
const keys = [
CACHE_KEY,
//deprecated
'totem_service_notifications',
'totem_translations',
'totem_sidebar-items-status',
]
const partialKeys = [
'_static_',
'_cache_',
]
const shouldRemove = key => !essentialKeys.includes(key) && (
// makes sure essential keys are not removed
keys.includes(key)
|| partialKeys.reduce((remove, pKey) =>
remove || key.includes(pKey),
false,
)
)
Object
.keys(localStorage)
.forEach(key =>
shouldRemove(key)
&& localStorage.removeItem(key)
)
}
export default storage