-
Notifications
You must be signed in to change notification settings - Fork 5
/
Copy pathstorage.js
260 lines (228 loc) · 10.2 KB
/
storage.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
/** Class to manage storage information
*
*/
// data structure
// index -> metadata
// checklists = array of checklist states
// checklist url
// current checklist state
// ...
const storage = {
INDEX_KEY: 'index',
CHECKLIST_KEY: 'checklists',
EMPTY_CHECKLISTS: {checklists:{}},
/** Load the data for the given URL using the following sequence.
*
* 1. Check to see if the given URL is already in the keys.
* 2. If yes, load that.
* 3. If no, copy master_template.json, create an entry, and pass that the client
*
* @param {String} url URL to load
* @param {Function} checklistCallback callback function that takes the loaded json
*/
loadChecklistForURL: function(url, checklistCallback) {
// load template and then overlay any checked items from storage
fetch(chrome.runtime.getURL('master_template.json')).then(response =>
response.json().then(json => {
// once json is loaded, overlay storage item
storage.getStorageArea().get(url, checklistData => {
const mergedJson = storage.mergeChecklistOntoMaster(checklistData[url], json);
checklistCallback({checklist: mergedJson, notes: checklistData[url] && checklistData[url]['notes'] ? checklistData[url]['notes'] : '' });
});
}));
},
/** Merges checklist information on top of main template json.
* For any given item in template, see if the corresponding key in checklist
* is checked.
* This mutates mergeTarget
*
* @param {Object} checklistData the data for the saved checklist
* @param {Object} mergeTarget the jso to merge into.
*/
mergeChecklistOntoMaster: function(checklistData, mergeTarget) {
if (!checklistData) {
return mergeTarget.checklist;
}
// convert the checklistData to a hash for quick lookup
const nameToChecked = storage.convertChecklistToHash(checklistData.checklist);
// for every name in mergerTarget, find the equivalent value in checklistData
// and set the mergeTarget's json to include a checked flag
const mergedChecklist = mergeTarget.checklist.map( storedData => {
if (storedData.subheading) {
return {subheading: storedData.subheading, checklist: storage.mergeChecklistOntoMaster(checklistData, storedData)};
} else {
return storage.setCheckedFlagFromLookup(nameToChecked, storedData);
}
});
return mergedChecklist;
},
/** Update checklist items with a checked attribute if the passed-in hash
* holds the checklist item's name. Refactored utility code
*
* @param {Object} lookupHash hash of checklist item name to checked status
* @param {Array} checklistItems array of objects to update based on hash value
* @return {Array} array of checklist items that have been updated with checked attributes
*/
setCheckedFlagsFromLookup: function(lookupHash, checklistItems) {
if (!checklistItems) {
return [];
}
return checklistItems.map( item => storage.setCheckedFlagFromLookup(lookupHash, item));
},
/** Return an object with a checked attribute of true if the lookupHash has an appropriate key for the item's name.
*
* @param {Object} lookupHash the lookup table to use for checking checklist items
* @param {Object} checklistItem the single item to check
* @return an object with the same name/description as checklistItem that also has a checked attribute
*/
setCheckedFlagFromLookup: function(lookupHash, checklistItem) {
return {name: checklistItem.name, description: checklistItem.description, checked: (lookupHash[checklistItem.name] ? lookupHash[checklistItem.name] : false)};
},
/** Convert a checklist array to a hash where the key is the item name
* and the value is its checked status.
* @param {Array} checklistItems the items to inspect
* @return {Ojbect} hash where each key is the checklist item name and each value is its checked state
*/
convertChecklistToHash: function(checklistItems) {
if (!checklistItems) {
return {};
}
const hash = checklistItems.reduce( (accumulator, currentItem, _index, _array) => {
if (storage.isObjectValid(currentItem, 'name') && currentItem.checked) {
accumulator[currentItem.name] = currentItem.checked;
}
return accumulator;
}, {});
return hash;
},
/** Determine if the passed in data is valid and has the given key
*
* @param {Object} data the data blob to inspect
* @param {String} key the key to check
* @return {Boolean} whether the rest of the code can make assumptions about it
*/
isObjectValid: function(objectToCheck, key) {
return objectToCheck != undefined && objectToCheck[key] != undefined
},
/** Determine if the given object is a valid stored checklist object.
* In order to be considered valid, it needs to have the given url as a field
* and that field must have a checklist property within it.
*
* @param {Object} objectToCheck to evaluate
* @param {String} url to use for evaluation
* @return {Boolean} if this is a valid checklist item
*/
isValidChecklistObject: function(objectToCheck, url) {
return storage.isObjectValid(objectToCheck, url) && storage.isObjectValid(objectToCheck[url], 'checklist');
},
/** Determine if the passed-in object is a valid index object
* There needs to be an index element with a checklists subelement
* @param {Object} objectToCheck
* @return {Boolean} if the object was a valid index
*/
isValidIndexObject: function(objectToCheck) {
return storage.isObjectValid(objectToCheck,storage.INDEX_KEY) && storage.isObjectValid(objectToCheck[storage.INDEX_KEY], storage.CHECKLIST_KEY);
},
/** Saves the specified checklist into the specified key.
*
* @param {Object} checklist data to save
* @param {String} storageKey the key in which to save the json
*/
saveChecklist: function(checklist, storageKey) {
if (!(checklist || storageKey)) {
return;
}
const storageObject = {};
storageObject[storageKey] = checklist;
storage.getStorageArea().set(storageObject, () => {
if (storage.errorOccurred()) {
storage.purgeAndRetry(checklist, storageKey);
} else {
storage.updateIndex(storageKey);
}
});
},
/** Updates the index for the given checklist. The index is maintained as a way
* to contain information about the storage as a whole.
*
* @param {String} storageKey the storageKey to update
*/
updateIndex: function(storageKey) {
if (!storageKey) {
return
}
const indexEntry = {last_updated_timestamp: Date.now()};
storage.getStorageArea().get(storage.INDEX_KEY, index => {
if (storage.isValidIndexObject(index)) {
// update the existing list and re-save
index[storage.INDEX_KEY].checklists[storageKey] = indexEntry;
storage.getStorageArea().set(index);
} else {
// the index doesn't exist, so create it with this as the first entry
newIndex = {};
newIndex[storage.INDEX_KEY] = storage.EMPTY_CHECKLISTS;
newIndex[storage.INDEX_KEY].checklists[storageKey] = indexEntry;
storage.getStorageArea().set(newIndex);
}
});
},
/** Some error happened when saving the checklist, so we need to purge
* old checklists and try again.
*
* @param {Object} objectToSave the object to save
* @param {String} storageKey the key to store the data under
*/
purgeAndRetry: function(objectToSave, storageKey) {
// get the index and work off of that
storage.getStorageArea().get(storage.INDEX_KEY, index => {
// figure out the item with the oldest timestamp
const indexItems = storage.convertIndexObjectToArray(index[storage.INDEX_KEY].checklists);
const oldestKey = storage.getOldestKeyInChecklists(indexItems);
storage.getStorageArea().remove(oldestKey, () => {
// only remove the checklist index entry if there were no errors
// otherwise, if you removed the index item but not the actual checklist
// you could orphan checklists
if (!storage.errorOccurred()) {
delete index[storage.INDEX_KEY].checklists[storageKey];
storage.getStorageArea().set(index);
}
// and now retry
storage.saveChecklist(objectToSave, storageKey);
});
});
},
/** Convert an index object's keys into an array of items where checklistKey is the object key and last_updated_timestamp is the teimstamp field.
* @param {Object} checklistItems object containing checklist keys and timestamps
* @return {Array} collated data where each element has a checklistKey field and a timestamp field
*/
convertIndexObjectToArray: function(checklistItems) {
if (!checklistItems) {
return [];
}
return Object.keys(checklistItems).map( (key, _index) => ({checklistKey: key, timestamp: checklistItems[key].last_updated_timestamp}));
},
/** Find the oldest key in the array of checklists.
* @param {Array} checklistIndexItems the index of checklist items
* @return {String} the oldest item in the array; the one with the lowest timestamp
*/
getOldestKeyInChecklists: function(checklistIndexItems) {
if (!checklistIndexItems) {
return undefined;
}
checklistIndexItems.sort( (left, right) => left.timestamp <= right.timestamp ? -1 : 1);
return checklistIndexItems[0].checklistKey;
},
/** Return whether there was an error or not in the last chrome operation.
* @return {Boolean} whether chrome.runtime.lasterror is set
*/
errorOccurred: function() {
return chrome.runtime.lastError != undefined;
},
/** Returns the storage engine used by the extension.
*
* @return {Object} Chrome StorageArea
*/
getStorageArea: function() {
return chrome.storage.local;
}
}