Project

General

Profile

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

haketilo / background / storage.js @ 263d03d5

1
/**
2
 * This file is part of Haketilo.
3
 *
4
 * Function: Storage manager.
5
 *
6
 * Copyright (C) 2021 Wojtek Kosior
7
 *
8
 * This program is free software: you can redistribute it and/or modify
9
 * it under the terms of the GNU General Public License as published by
10
 * the Free Software Foundation, either version 3 of the License, or
11
 * (at your option) any later version.
12
 *
13
 * This program is distributed in the hope that it will be useful,
14
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16
 * GNU General Public License for more details.
17
 *
18
 * As additional permission under GNU GPL version 3 section 7, you
19
 * may distribute forms of that code without the copy of the GNU
20
 * GPL normally required by section 4, provided you include this
21
 * license notice and, in case of non-source distribution, a URL
22
 * through which recipients can access the Corresponding Source.
23
 * If you modify file(s) with this exception, you may extend this
24
 * exception to your version of the file(s), but you are not
25
 * obligated to do so. If you do not wish to do so, delete this
26
 * exception statement from your version.
27
 *
28
 * As a special exception to the GPL, any HTML file which merely
29
 * makes function calls to this code, and for that purpose
30
 * includes it by reference shall be deemed a separate work for
31
 * copyright law purposes. If you modify this code, you may extend
32
 * this exception to your version of the code, but you are not
33
 * obligated to do so. If you do not wish to do so, delete this
34
 * exception statement from your version.
35
 *
36
 * You should have received a copy of the GNU General Public License
37
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
38
 *
39
 * I, Wojtek Kosior, thereby promise not to sue for violation of this file's
40
 * license. Although I request that you do not make use this code in a
41
 * proprietary program, I am not going to enforce this in court.
42
 */
43

    
44
/*
45
 * IMPORTS_START
46
 * IMPORT raw_storage
47
 * IMPORT TYPE_NAME
48
 * IMPORT list_prefixes
49
 * IMPORT make_lock
50
 * IMPORT lock
51
 * IMPORT unlock
52
 * IMPORT make_once
53
 * IMPORT browser
54
 * IMPORT observables
55
 * IMPORTS_END
56
 */
57

    
58
var exports = {};
59

    
60
/* A special case of persisted variable is one that contains list of items. */
61

    
62
async function get_list_var(name)
63
{
64
    let list = await raw_storage.get_var(name);
65

    
66
    return list === undefined ? [] : list;
67
}
68

    
69
/* We maintain in-memory copies of some stored lists. */
70

    
71
async function list(prefix)
72
{
73
    let name = TYPE_NAME[prefix] + "s"; /* Make plural. */
74
    let map = new Map();
75

    
76
    for (let item of await get_list_var(name))
77
	map.set(item, await raw_storage.get(prefix + item));
78

    
79
    return {map, prefix, name, observable: observables.make(),
80
	    lock: make_lock()};
81
}
82

    
83
var list_by_prefix = {};
84

    
85
async function init()
86
{
87
    for (let prefix of list_prefixes)
88
	list_by_prefix[prefix] = await list(prefix);
89

    
90
    return exports;
91
}
92

    
93
/*
94
 * Facilitate listening to changes
95
 */
96

    
97
exports.add_change_listener = function (cb, prefixes=list_prefixes)
98
{
99
    if (typeof(prefixes) === "string")
100
	prefixes = [prefixes];
101

    
102
    for (let prefix of prefixes)
103
	observables.subscribe(list_by_prefix[prefix].observable, cb);
104
}
105

    
106
exports.remove_change_listener = function (cb, prefixes=list_prefixes)
107
{
108
    if (typeof(prefixes) === "string")
109
	prefixes = [prefixes];
110

    
111
    for (let prefix of prefixes)
112
	observables.unsubscribe(list_by_prefix[prefix].observable, cb);
113
}
114

    
115
/* Prepare some hepler functions to get elements of a list */
116

    
117
function list_items_it(list, with_values=false)
118
{
119
    return with_values ? list.map.entries() : list.map.keys();
120
}
121

    
122
function list_entries_it(list)
123
{
124
    return list_items_it(list, true);
125
}
126

    
127
function list_items(list, with_values=false)
128
{
129
    let array = [];
130

    
131
    for (let item of list_items_it(list, with_values))
132
	array.push(item);
133

    
134
    return array;
135
}
136

    
137
function list_entries(list)
138
{
139
    return list_items(list, true);
140
}
141

    
142
/*
143
 * Below we make additional effort to update map of given kind of items
144
 * every time an item is added/removed to keep everything coherent.
145
 */
146
async function set_item(item, value, list)
147
{
148
    await lock(list.lock);
149
    let result = await _set_item(...arguments);
150
    unlock(list.lock)
151
    return result;
152
}
153
async function _set_item(item, value, list)
154
{
155
    const key = list.prefix + item;
156
    const old_val = list.map.get(item);
157
    const set_obj = {[key]: value};
158
    if (old_val === undefined) {
159
	const items = list_items(list);
160
	items.push(item);
161
	set_obj["_" + list.name] = items;
162
    }
163

    
164
    await raw_storage.set(set_obj);
165
    list.map.set(item, value);
166

    
167
    const change = {
168
	prefix : list.prefix,
169
	item,
170
	old_val,
171
	new_val : value
172
    };
173

    
174
    observables.broadcast(list.observable, change);
175

    
176
    return old_val;
177
}
178

    
179
// TODO: The actual idea to set value to undefined is good - this way we can
180
//       also set a new list of items in the same API call. But such key
181
//       is still stored in the storage. We need to somehow remove it later.
182
//       For that, we're going to have to store 1 more list of each kind.
183
async function remove_item(item, list)
184
{
185
    await lock(list.lock);
186
    let result = await _remove_item(...arguments);
187
    unlock(list.lock)
188
    return result;
189
}
190
async function _remove_item(item, list)
191
{
192
    const old_val = list.map.get(item);
193
    if (old_val === undefined)
194
	return;
195

    
196
    const items = list_items(list);
197
    const index = items.indexOf(item);
198
    items.splice(index, 1);
199

    
200
    await raw_storage.set({
201
	[list.prefix + item]: undefined,
202
	["_" + list.name]: items
203
    });
204
    list.map.delete(item);
205

    
206
    const change = {
207
	prefix : list.prefix,
208
	item,
209
	old_val,
210
	new_val : undefined
211
    };
212

    
213
    observables.broadcast(list.observable, change);
214

    
215
    return old_val;
216
}
217

    
218
// TODO: same as above applies here
219
async function replace_item(old_item, new_item, list, new_val=undefined)
220
{
221
    await lock(list.lock);
222
    let result = await _replace_item(...arguments);
223
    unlock(list.lock)
224
    return result;
225
}
226
async function _replace_item(old_item, new_item, list, new_val=undefined)
227
{
228
    const old_val = list.map.get(old_item);
229
    if (new_val === undefined) {
230
	if (old_val === undefined)
231
	    return;
232
	new_val = old_val;
233
    } else if (new_val === old_val && new_item === old_item) {
234
	return old_val;
235
    }
236

    
237
    if (old_item === new_item || old_val === undefined) {
238
	await _set_item(new_item, new_val, list);
239
	return old_val;
240
    }
241

    
242
    const items = list_items(list);
243
    const index = items.indexOf(old_item);
244
    items[index] = new_item;
245

    
246
    await raw_storage.set({
247
	[list.prefix + old_item]: undefined,
248
	[list.prefix + new_item]: new_val,
249
	["_" + list.name]: items
250
    });
251
    list.map.delete(old_item);
252

    
253
    const change = {
254
	prefix : list.prefix,
255
	item : old_item,
256
	old_val,
257
	new_val : undefined
258
    };
259

    
260
    observables.broadcast(list.observable, change);
261

    
262
    list.map.set(new_item, new_val);
263

    
264
    change.item = new_item;
265
    change.old_val = undefined;
266
    change.new_val = new_val;
267

    
268
    observables.broadcast(list.observable, change);
269

    
270
    return old_val;
271
}
272

    
273
/*
274
 * For scripts, item name is chosen by user, data should be
275
 * an object containing:
276
 * - script's url and hash or
277
 * - script's text or
278
 * - all three
279
 */
280

    
281
/*
282
 * For bags, item name is chosen by user, data is an array of 2-element
283
 * arrays with type prefix and script/bag names.
284
 */
285

    
286
/*
287
 * For pages data argument is an object with properties `allow'
288
 * and `components'. Item name is url.
289
 */
290

    
291
exports.set = async function (prefix, item, data)
292
{
293
    return set_item(item, data, list_by_prefix[prefix]);
294
}
295

    
296
exports.get = function (prefix, item)
297
{
298
    return list_by_prefix[prefix].map.get(item);
299
}
300

    
301
exports.remove = async function (prefix, item)
302
{
303
    return remove_item(item, list_by_prefix[prefix]);
304
}
305

    
306
exports.replace = async function (prefix, old_item, new_item,
307
				  new_data=undefined)
308
{
309
    return replace_item(old_item, new_item, list_by_prefix[prefix],
310
			new_data);
311
}
312

    
313
exports.get_all_names = function (prefix)
314
{
315
    return list_items(list_by_prefix[prefix]);
316
}
317

    
318
exports.get_all_names_it = function (prefix)
319
{
320
    return list_items_it(list_by_prefix[prefix]);
321
}
322

    
323
exports.get_all = function (prefix)
324
{
325
    return list_entries(list_by_prefix[prefix]);
326
}
327

    
328
exports.get_all_it = function (prefix)
329
{
330
    return list_entries_it(list_by_prefix[prefix]);
331
}
332

    
333
/* Finally, a quick way to wipe all the data. */
334
// TODO: maybe delete items in such order that none of them ever references
335
// an already-deleted one?
336
exports.clear = async function ()
337
{
338
    let lists = list_prefixes.map((p) => list_by_prefix[p]);
339

    
340
    for (let list of lists)
341
	await lock(list.lock);
342

    
343
    for (let list of lists) {
344

    
345
	let change = {
346
	    prefix : list.prefix,
347
	    new_val : undefined
348
	};
349

    
350
	for (let [item, val] of list_entries_it(list)) {
351
	    change.item = item;
352
	    change.old_val = val;
353
	    observables.broadcast(list.observable, change);
354
	}
355

    
356
	list.map = new Map();
357
    }
358

    
359
    await browser.storage.local.clear();
360

    
361
    for (let list of lists)
362
	unlock(list.lock);
363
}
364

    
365
const get_storage = make_once(init);
366

    
367
/*
368
 * EXPORTS_START
369
 * EXPORT get_storage
370
 * EXPORTS_END
371
 */
(5-5/7)