Project

General

Profile

Download (8.1 KB) Statistics
| Branch: | Tag: | Revision:

haketilo / background / storage.js @ d42dadca

1
/**
2
 * Hachette storage manager
3
 *
4
 * Copyright (C) 2021 Wojtek Kosior
5
 * Redistribution terms are gathered in the `copyright' file.
6
 */
7

    
8
/*
9
 * IMPORTS_START
10
 * IMPORT TYPE_PREFIX
11
 * IMPORT TYPE_NAME
12
 * IMPORT list_prefixes
13
 * IMPORT make_lock
14
 * IMPORT lock
15
 * IMPORT unlock
16
 * IMPORT make_once
17
 * IMPORT browser
18
 * IMPORT is_chrome
19
 * IMPORT observables
20
 * IMPORTS_END
21
 */
22

    
23
var exports = {};
24

    
25
/* We're yet to decide how to handle errors... */
26

    
27
/* Here are some basic wrappers for storage API functions */
28

    
29
async function get(key)
30
{
31
    try {
32
	/* Fix for fact that Chrome does not use promises here */
33
	let promise = is_chrome ?
34
	    new Promise((resolve, reject) =>
35
			chrome.storage.local.get(key,
36
						 val => resolve(val))) :
37
	    browser.storage.local.get(key);
38

    
39
	return (await promise)[key];
40
    } catch (e) {
41
	console.log(e);
42
    }
43
}
44

    
45
async function set(key, value)
46
{
47
    try {
48
	return browser.storage.local.set({[key]: value});
49
    } catch (e) {
50
	console.log(e);
51
    }
52
}
53

    
54
async function setn(keys_and_values)
55
{
56
    let obj = Object();
57
    while (keys_and_values.length > 1) {
58
	let value = keys_and_values.pop();
59
	let key = keys_and_values.pop();
60
	obj[key] = value;
61
    }
62

    
63
    try {
64
	return browser.storage.local.set(obj);
65
    } catch (e) {
66
	console.log(e);
67
    }
68
}
69

    
70
async function set_var(name, value)
71
{
72
    return set(TYPE_PREFIX.VAR + name, value);
73
}
74

    
75
async function get_var(name)
76
{
77
    return get(TYPE_PREFIX.VAR + name);
78
}
79

    
80
/*
81
 * A special case of persisted variable is one that contains list
82
 * of items.
83
 */
84

    
85
async function get_list_var(name)
86
{
87
    let list = await get_var(name);
88

    
89
    return list === undefined ? [] : list;
90
}
91

    
92
/* We maintain in-memory copies of some stored lists. */
93

    
94
async function list(prefix)
95
{
96
    let name = TYPE_NAME[prefix] + "s"; /* Make plural. */
97
    let map = new Map();
98

    
99
    for (let item of await get_list_var(name))
100
	map.set(item, await get(prefix + item));
101

    
102
    return {map, prefix, name, observable: observables.make(),
103
	    lock: make_lock()};
104
}
105

    
106
var list_by_prefix = {};
107

    
108
async function init()
109
{
110
    for (let prefix of list_prefixes)
111
	list_by_prefix[prefix] = await list(prefix);
112

    
113
    return exports;
114
}
115

    
116
/*
117
 * Facilitate listening to changes
118
 */
119

    
120
exports.add_change_listener = function (cb, prefixes=list_prefixes)
121
{
122
    if (typeof(prefixes) === "string")
123
	prefixes = [prefixes];
124

    
125
    for (let prefix of prefixes)
126
	observables.subscribe(list_by_prefix[prefix].observable, cb);
127
}
128

    
129
exports.remove_change_listener = function (cb, prefixes=list_prefixes)
130
{
131
    if (typeof(prefixes) === "string")
132
	prefixes = [prefixes];
133

    
134
    for (let prefix of prefixes)
135
	observables.unsubscribe(list_by_prefix[prefix].observable, cb);
136
}
137

    
138
/* Prepare some hepler functions to get elements of a list */
139

    
140
function list_items_it(list, with_values=false)
141
{
142
    return with_values ? list.map.entries() : list.map.keys();
143
}
144

    
145
function list_entries_it(list)
146
{
147
    return list_items_it(list, true);
148
}
149

    
150
function list_items(list, with_values=false)
151
{
152
    let array = [];
153

    
154
    for (let item of list_items_it(list, with_values))
155
	array.push(item);
156

    
157
    return array;
158
}
159

    
160
function list_entries(list)
161
{
162
    return list_items(list, true);
163
}
164

    
165
/*
166
 * Below we make additional effort to update map of given kind of items
167
 * every time an item is added/removed to keep everything coherent.
168
 */
169
async function set_item(item, value, list)
170
{
171
    await lock(list.lock);
172
    let result = await _set_item(...arguments);
173
    unlock(list.lock)
174
    return result;
175
}
176
async function _set_item(item, value, list)
177
{
178
    let key = list.prefix + item;
179
    let old_val = list.map.get(item);
180
    if (old_val === undefined) {
181
	let items = list_items(list);
182
	items.push(item);
183
	await setn([key, value, "_" + list.name, items]);
184
    } else {
185
	await set(key, value);
186
    }
187

    
188
    list.map.set(item, value)
189

    
190
    let change = {
191
	prefix : list.prefix,
192
	item,
193
	old_val,
194
	new_val : value
195
    };
196

    
197
    observables.broadcast(list.observable, change);
198

    
199
    return old_val;
200
}
201

    
202
// TODO: The actual idea to set value to undefined is good - this way we can
203
//       also set a new list of items in the same API call. But such key
204
//       is still stored in the storage. We need to somehow remove it later.
205
//       For that, we're going to have to store 1 more list of each kind.
206
async function remove_item(item, list)
207
{
208
    await lock(list.lock);
209
    let result = await _remove_item(...arguments);
210
    unlock(list.lock)
211
    return result;
212
}
213
async function _remove_item(item, list)
214
{
215
    let old_val = list.map.get(item);
216
    if (old_val === undefined)
217
	return;
218

    
219
    let key = list.prefix + item;
220
    let items = list_items(list);
221
    let index = items.indexOf(item);
222
    items.splice(index, 1);
223

    
224
    await setn([key, undefined, "_" + list.name, items]);
225

    
226
    list.map.delete(item);
227

    
228
    let change = {
229
	prefix : list.prefix,
230
	item,
231
	old_val,
232
	new_val : undefined
233
    };
234

    
235
    observables.broadcast(list.observable, change);
236

    
237
    return old_val;
238
}
239

    
240
// TODO: same as above applies here
241
async function replace_item(old_item, new_item, list, new_val=undefined)
242
{
243
    await lock(list.lock);
244
    let result = await _replace_item(...arguments);
245
    unlock(list.lock)
246
    return result;
247
}
248
async function _replace_item(old_item, new_item, list, new_val=undefined)
249
{
250
    let old_val = list.map.get(old_item);
251
    if (new_val === undefined) {
252
	if (old_val === undefined)
253
	    return;
254
	new_val = old_val
255
    } else if (new_val === old_val && new_item === old_item) {
256
	return old_val;
257
    }
258

    
259
    if (old_item === new_item || old_val === undefined) {
260
	await _set_item(new_item, new_val, list);
261
	return old_val;
262
    }
263

    
264
    let new_key = list.prefix + new_item;
265
    let old_key = list.prefix + old_item;
266
    let items = list_items(list);
267
    let index = items.indexOf(old_item);
268
    items[index] = new_item;
269
    await setn([old_key, undefined, new_key, new_val,
270
		"_" + list.name, items]);
271

    
272
    list.map.delete(old_item);
273

    
274
    let change = {
275
	prefix : list.prefix,
276
	item : old_item,
277
	old_val,
278
	new_val : undefined
279
    };
280

    
281
    observables.broadcast(list.observable, change);
282

    
283
    list.map.set(new_item, new_val);
284

    
285
    change.item = new_item;
286
    change.old_val = undefined;
287
    change.new_val = new_val;
288

    
289
    observables.broadcast(list.observable, change);
290

    
291
    return old_val;
292
}
293

    
294
/*
295
 * For scripts, item name is chosen by user, data should be
296
 * an object containing:
297
 * - script's url and hash or
298
 * - script's text or
299
 * - all three
300
 */
301

    
302
/*
303
 * For bags, item name is chosen by user, data is an array of 2-element
304
 * arrays with type prefix and script/bag names.
305
 */
306

    
307
/*
308
 * For pages data argument is an object with properties `allow'
309
 * and `components'. Item name is url.
310
 */
311

    
312
exports.set = async function (prefix, item, data)
313
{
314
    return set_item(item, data, list_by_prefix[prefix]);
315
}
316

    
317
exports.get = function (prefix, item)
318
{
319
    return list_by_prefix[prefix].map.get(item);
320
}
321

    
322
exports.remove = async function (prefix, item)
323
{
324
    return remove_item(item, list_by_prefix[prefix]);
325
}
326

    
327
exports.replace = async function (prefix, old_item, new_item,
328
				  new_data=undefined)
329
{
330
    return replace_item(old_item, new_item, list_by_prefix[prefix],
331
			new_data);
332
}
333

    
334
exports.get_all_names = function (prefix)
335
{
336
    return list_items(list_by_prefix[prefix]);
337
}
338

    
339
exports.get_all_names_it = function (prefix)
340
{
341
    return list_items_it(list_by_prefix[prefix]);
342
}
343

    
344
exports.get_all = function (prefix)
345
{
346
    return list_entries(list_by_prefix[prefix]);
347
}
348

    
349
exports.get_all_it = function (prefix)
350
{
351
    return list_entries_it(list_by_prefix[prefix]);
352
}
353

    
354
/* Finally, a quick way to wipe all the data. */
355
// TODO: maybe delete items in such order that none of them ever references
356
// an already-deleted one?
357
exports.clear = async function ()
358
{
359
    let lists = list_prefixes.map((p) => list_by_prefix[p]);
360

    
361
    for (let list of lists)
362
	await lock(list.lock);
363

    
364
    for (let list of lists) {
365

    
366
	let change = {
367
	    prefix : list.prefix,
368
	    new_val : undefined
369
	};
370

    
371
	for (let [item, val] of list_entries_it(list)) {
372
	    change.item = item;
373
	    change.old_val = val;
374
	    observables.broadcast(list.observable, change);
375
	}
376

    
377
	list.map = new Map();
378
    }
379

    
380
    await browser.storage.local.clear();
381

    
382
    for (let list of lists)
383
	unlock(list.lock);
384
}
385

    
386
const get_storage = make_once(init);
387

    
388
/*
389
 * EXPORTS_START
390
 * EXPORT get_storage
391
 * EXPORTS_END
392
 */
(6-6/7)