Project

General

Profile

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

haketilo / background / storage.js @ 53891495

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 raw_storage
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 observables
19
 * IMPORTS_END
20
 */
21

    
22
var exports = {};
23

    
24
/* A special case of persisted variable is one that contains list of items. */
25

    
26
async function get_list_var(name)
27
{
28
    let list = await raw_storage.get_var(name);
29

    
30
    return list === undefined ? [] : list;
31
}
32

    
33
/* We maintain in-memory copies of some stored lists. */
34

    
35
async function list(prefix)
36
{
37
    let name = TYPE_NAME[prefix] + "s"; /* Make plural. */
38
    let map = new Map();
39

    
40
    for (let item of await get_list_var(name))
41
	map.set(item, await raw_storage.get(prefix + item));
42

    
43
    return {map, prefix, name, observable: observables.make(),
44
	    lock: make_lock()};
45
}
46

    
47
var list_by_prefix = {};
48

    
49
async function init()
50
{
51
    for (let prefix of list_prefixes)
52
	list_by_prefix[prefix] = await list(prefix);
53

    
54
    return exports;
55
}
56

    
57
/*
58
 * Facilitate listening to changes
59
 */
60

    
61
exports.add_change_listener = function (cb, prefixes=list_prefixes)
62
{
63
    if (typeof(prefixes) === "string")
64
	prefixes = [prefixes];
65

    
66
    for (let prefix of prefixes)
67
	observables.subscribe(list_by_prefix[prefix].observable, cb);
68
}
69

    
70
exports.remove_change_listener = function (cb, prefixes=list_prefixes)
71
{
72
    if (typeof(prefixes) === "string")
73
	prefixes = [prefixes];
74

    
75
    for (let prefix of prefixes)
76
	observables.unsubscribe(list_by_prefix[prefix].observable, cb);
77
}
78

    
79
/* Prepare some hepler functions to get elements of a list */
80

    
81
function list_items_it(list, with_values=false)
82
{
83
    return with_values ? list.map.entries() : list.map.keys();
84
}
85

    
86
function list_entries_it(list)
87
{
88
    return list_items_it(list, true);
89
}
90

    
91
function list_items(list, with_values=false)
92
{
93
    let array = [];
94

    
95
    for (let item of list_items_it(list, with_values))
96
	array.push(item);
97

    
98
    return array;
99
}
100

    
101
function list_entries(list)
102
{
103
    return list_items(list, true);
104
}
105

    
106
/*
107
 * Below we make additional effort to update map of given kind of items
108
 * every time an item is added/removed to keep everything coherent.
109
 */
110
async function set_item(item, value, list)
111
{
112
    await lock(list.lock);
113
    let result = await _set_item(...arguments);
114
    unlock(list.lock)
115
    return result;
116
}
117
async function _set_item(item, value, list)
118
{
119
    const key = list.prefix + item;
120
    const old_val = list.map.get(item);
121
    const set_obj = {[key]: value};
122
    if (old_val === undefined) {
123
	const items = list_items(list);
124
	items.push(item);
125
	set_obj["_" + list.name] = items;
126
    }
127

    
128
    await raw_storage.set(set_obj);
129
    list.map.set(item, value);
130

    
131
    const change = {
132
	prefix : list.prefix,
133
	item,
134
	old_val,
135
	new_val : value
136
    };
137

    
138
    observables.broadcast(list.observable, change);
139

    
140
    return old_val;
141
}
142

    
143
// TODO: The actual idea to set value to undefined is good - this way we can
144
//       also set a new list of items in the same API call. But such key
145
//       is still stored in the storage. We need to somehow remove it later.
146
//       For that, we're going to have to store 1 more list of each kind.
147
async function remove_item(item, list)
148
{
149
    await lock(list.lock);
150
    let result = await _remove_item(...arguments);
151
    unlock(list.lock)
152
    return result;
153
}
154
async function _remove_item(item, list)
155
{
156
    const old_val = list.map.get(item);
157
    if (old_val === undefined)
158
	return;
159

    
160
    const items = list_items(list);
161
    const index = items.indexOf(item);
162
    items.splice(index, 1);
163

    
164
    await raw_storage.set({
165
	[list.prefix + item]: undefined,
166
	["_" + list.name]: items
167
    });
168
    list.map.delete(item);
169

    
170
    const change = {
171
	prefix : list.prefix,
172
	item,
173
	old_val,
174
	new_val : undefined
175
    };
176

    
177
    observables.broadcast(list.observable, change);
178

    
179
    return old_val;
180
}
181

    
182
// TODO: same as above applies here
183
async function replace_item(old_item, new_item, list, new_val=undefined)
184
{
185
    await lock(list.lock);
186
    let result = await _replace_item(...arguments);
187
    unlock(list.lock)
188
    return result;
189
}
190
async function _replace_item(old_item, new_item, list, new_val=undefined)
191
{
192
    const old_val = list.map.get(old_item);
193
    if (new_val === undefined) {
194
	if (old_val === undefined)
195
	    return;
196
	new_val = old_val;
197
    } else if (new_val === old_val && new_item === old_item) {
198
	return old_val;
199
    }
200

    
201
    if (old_item === new_item || old_val === undefined) {
202
	await _set_item(new_item, new_val, list);
203
	return old_val;
204
    }
205

    
206
    const items = list_items(list);
207
    const index = items.indexOf(old_item);
208
    items[index] = new_item;
209

    
210
    await raw_storage.set({
211
	[list.prefix + old_item]: undefined,
212
	[list.prefix + new_item]: new_val,
213
	["_" + list.name]: items
214
    });
215
    list.map.delete(old_item);
216

    
217
    const change = {
218
	prefix : list.prefix,
219
	item : old_item,
220
	old_val,
221
	new_val : undefined
222
    };
223

    
224
    observables.broadcast(list.observable, change);
225

    
226
    list.map.set(new_item, new_val);
227

    
228
    change.item = new_item;
229
    change.old_val = undefined;
230
    change.new_val = new_val;
231

    
232
    observables.broadcast(list.observable, change);
233

    
234
    return old_val;
235
}
236

    
237
/*
238
 * For scripts, item name is chosen by user, data should be
239
 * an object containing:
240
 * - script's url and hash or
241
 * - script's text or
242
 * - all three
243
 */
244

    
245
/*
246
 * For bags, item name is chosen by user, data is an array of 2-element
247
 * arrays with type prefix and script/bag names.
248
 */
249

    
250
/*
251
 * For pages data argument is an object with properties `allow'
252
 * and `components'. Item name is url.
253
 */
254

    
255
exports.set = async function (prefix, item, data)
256
{
257
    return set_item(item, data, list_by_prefix[prefix]);
258
}
259

    
260
exports.get = function (prefix, item)
261
{
262
    return list_by_prefix[prefix].map.get(item);
263
}
264

    
265
exports.remove = async function (prefix, item)
266
{
267
    return remove_item(item, list_by_prefix[prefix]);
268
}
269

    
270
exports.replace = async function (prefix, old_item, new_item,
271
				  new_data=undefined)
272
{
273
    return replace_item(old_item, new_item, list_by_prefix[prefix],
274
			new_data);
275
}
276

    
277
exports.get_all_names = function (prefix)
278
{
279
    return list_items(list_by_prefix[prefix]);
280
}
281

    
282
exports.get_all_names_it = function (prefix)
283
{
284
    return list_items_it(list_by_prefix[prefix]);
285
}
286

    
287
exports.get_all = function (prefix)
288
{
289
    return list_entries(list_by_prefix[prefix]);
290
}
291

    
292
exports.get_all_it = function (prefix)
293
{
294
    return list_entries_it(list_by_prefix[prefix]);
295
}
296

    
297
/* Finally, a quick way to wipe all the data. */
298
// TODO: maybe delete items in such order that none of them ever references
299
// an already-deleted one?
300
exports.clear = async function ()
301
{
302
    let lists = list_prefixes.map((p) => list_by_prefix[p]);
303

    
304
    for (let list of lists)
305
	await lock(list.lock);
306

    
307
    for (let list of lists) {
308

    
309
	let change = {
310
	    prefix : list.prefix,
311
	    new_val : undefined
312
	};
313

    
314
	for (let [item, val] of list_entries_it(list)) {
315
	    change.item = item;
316
	    change.old_val = val;
317
	    observables.broadcast(list.observable, change);
318
	}
319

    
320
	list.map = new Map();
321
    }
322

    
323
    await browser.storage.local.clear();
324

    
325
    for (let list of lists)
326
	unlock(list.lock);
327
}
328

    
329
const get_storage = make_once(init);
330

    
331
/*
332
 * EXPORTS_START
333
 * EXPORT get_storage
334
 * EXPORTS_END
335
 */
(5-5/7)