Project

General

Profile

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

haketilo / background / storage.js @ 6bae771d

1
/**
2
 * Myext storage manager
3
 *
4
 * Copyright (C) 2021 Wojtek Kosior
5
 *
6
 * This code is dual-licensed under:
7
 * - Asshole license 1.0,
8
 * - GPLv3 or (at your option) any later version
9
 *
10
 * "dual-licensed" means you can choose the license you prefer.
11
 *
12
 * This code is released under a permissive license because I disapprove of
13
 * copyright and wouldn't be willing to sue a violator. Despite not putting
14
 * this code under copyleft (which is also kind of copyright), I do not want
15
 * it to be made proprietary. Hence, the permissive alternative to GPL is the
16
 * Asshole license 1.0 that allows me to call you an asshole if you use it.
17
 * This means you're legally ok regardless of how you utilize this code but if
18
 * you make it into something nonfree, you're an asshole.
19
 *
20
 * You should have received a copy of both GPLv3 and Asshole license 1.0
21
 * together with this code. If not, please see:
22
 * - https://www.gnu.org/licenses/gpl-3.0.en.html
23
 * - https://koszko.org/asshole-license.txt
24
 */
25

    
26
"use strict";
27

    
28
(() => {
29
    const TYPE_PREFIX = window.TYPE_PREFIX;
30
    const TYPE_NAME = window.TYPE_NAME;
31
    const list_prefixes = window.list_prefixes;
32
    const make_lock = window.make_lock;
33
    const lock = window.lock;
34
    const unlock = window.unlock;
35
    const make_once = window.make_once;
36
    const browser = window.browser;
37
    const is_chrome = window.is_chrome;
38

    
39
    var exports = {};
40

    
41
    /* We're yet to decide how to handle errors... */
42

    
43
    /* Here are some basic wrappers for storage API functions */
44

    
45
    async function get(key)
46
    {
47
	try {
48
	    /* Fix for fact that Chrome does not use promises here */
49
	    let promise = is_chrome ?
50
		new Promise((resolve, reject) =>
51
			    chrome.storage.local.get(key,
52
						     val => resolve(val))) :
53
		browser.storage.local.get(key);
54

    
55
	    return (await promise)[key];
56
	} catch (e) {
57
	    console.log(e);
58
	}
59
    }
60

    
61
    async function set(key, value)
62
    {
63
	try {
64
	    return browser.storage.local.set({[key]: value});
65
	} catch (e) {
66
	    console.log(e);
67
	}
68
    }
69

    
70
    async function setn(keys_and_values)
71
    {
72
	let obj = Object();
73
	while (keys_and_values.length > 1) {
74
	    let value = keys_and_values.pop();
75
	    let key = keys_and_values.pop();
76
	    obj[key] = value;
77
	}
78

    
79
	try {
80
	    return browser.storage.local.set(obj);
81
	} catch (e) {
82
	    console.log(e);
83
	}
84
    }
85

    
86
    async function set_var(name, value)
87
    {
88
	return set(TYPE_PREFIX.VAR + name, value);
89
    }
90

    
91
    async function get_var(name)
92
    {
93
	return get(TYPE_PREFIX.VAR + name);
94
    }
95

    
96
    /*
97
     * A special case of persisted variable is one that contains list
98
     * of items.
99
     */
100

    
101
    async function get_list_var(name)
102
    {
103
	let list = await get_var(name);
104

    
105
	return list === undefined ? [] : list;
106
    }
107

    
108
    /* We maintain in-memory copies of some stored lists. */
109

    
110
    async function list(prefix)
111
    {
112
	let name = TYPE_NAME[prefix] + "s"; /* Make plural. */
113
	let map = new Map();
114

    
115
	for (let item of await get_list_var(name))
116
	    map.set(item, await get(prefix + item));
117

    
118
	return {map, prefix, name, listeners : new Set(), lock : make_lock()};
119
    }
120

    
121
    var pages;
122
    var bags;
123
    var scripts;
124

    
125
    var list_by_prefix = {};
126

    
127
    async function init()
128
    {
129
	for (let prefix of list_prefixes)
130
	    list_by_prefix[prefix] = await list(prefix);
131

    
132
	return exports;
133
    }
134

    
135
    /*
136
     * Facilitate listening to changes
137
     */
138

    
139
    exports.add_change_listener = function (cb, prefixes=list_prefixes)
140
    {
141
	if (typeof(prefixes) === "string")
142
	    prefixes = [prefixes];
143

    
144
	for (let prefix of prefixes)
145
	    list_by_prefix[prefix].listeners.add(cb);
146
    }
147

    
148
    exports.remove_change_listener = function (cb, prefixes=list_prefixes)
149
    {
150
	if (typeof(prefixes) === "string")
151
	    prefixes = [prefixes];
152

    
153
	for (let prefix of prefixes)
154
	    list_by_prefix[prefix].listeners.delete(cb);
155
    }
156

    
157
    function broadcast_change(change, list)
158
    {
159
	for (let listener_callback of list.listeners)
160
	    listener_callback(change);
161
    }
162

    
163
    /* Prepare some hepler functions to get elements of a list */
164

    
165
    function list_items_it(list, with_values=false)
166
    {
167
	return with_values ? list.map.entries() : list.map.keys();
168
    }
169

    
170
    function list_entries_it(list)
171
    {
172
	return list_items_it(list, true);
173
    }
174

    
175
    function list_items(list, with_values=false)
176
    {
177
	let array = [];
178

    
179
	for (let item of list_items_it(list, with_values))
180
	    array.push(item);
181

    
182
	return array;
183
    }
184

    
185
    function list_entries(list)
186
    {
187
	return list_items(list, true);
188
    }
189

    
190
    /*
191
     * Below we make additional effort to update map of given kind of items
192
     * every time an item is added/removed to keep everything coherent.
193
     */
194
    async function set_item(item, value, list)
195
    {
196
	await lock(list.lock);
197
	let result = await _set_item(...arguments);
198
	unlock(list.lock)
199
	return result;
200
    }
201
    async function _set_item(item, value, list)
202
    {
203
	let key = list.prefix + item;
204
	let old_val = list.map.get(item);
205
	if (old_val === undefined) {
206
	    let items = list_items(list);
207
	    items.push(item);
208
	    await setn([key, value, "_" + list.name, items]);
209
	} else {
210
	    await set(key, value);
211
	}
212

    
213
	list.map.set(item, value)
214

    
215
	let change = {
216
	    prefix : list.prefix,
217
	    item,
218
	    old_val,
219
	    new_val : value
220
	};
221

    
222
	broadcast_change(change, list);
223

    
224
	return old_val;
225
    }
226

    
227
    // TODO: The actual idea to set value to undefined is good - this way we can
228
    //       also set a new list of items in the same API call. But such key
229
    //       is still stored in the storage. We need to somehow remove it later.
230
    //       For that, we're going to have to store 1 more list of each kind.
231
    async function remove_item(item, list)
232
    {
233
	await lock(list.lock);
234
	let result = await _remove_item(...arguments);
235
	unlock(list.lock)
236
	return result;
237
    }
238
    async function _remove_item(item, list)
239
    {
240
	let old_val = list.map.get(item);
241
	if (old_val === undefined)
242
	    return;
243

    
244
	let key = list.prefix + item;
245
	let items = list_items(list);
246
	let index = items.indexOf(item);
247
	items.splice(index, 1);
248

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

    
251
	list.map.delete(item);
252

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

    
260
	broadcast_change(change, list);
261

    
262
	return old_val;
263
    }
264

    
265
    // TODO: same as above applies here
266
    async function replace_item(old_item, new_item, list, new_val=undefined)
267
    {
268
	await lock(list.lock);
269
	let result = await _replace_item(...arguments);
270
	unlock(list.lock)
271
	return result;
272
    }
273
    async function _replace_item(old_item, new_item, list, new_val=undefined)
274
    {
275
	let old_val = list.map.get(old_item);
276
	if (new_val === undefined) {
277
	    if (old_val === undefined)
278
		return;
279
	    new_val = old_val
280
	} else if (new_val === old_val && new_item === old_item) {
281
	    return old_val;
282
	}
283

    
284
	if (old_item === new_item || old_val === undefined) {
285
	    await _set_item(new_item, new_val, list);
286
	    return old_val;
287
	}
288

    
289
	let new_key = list.prefix + new_item;
290
	let old_key = list.prefix + old_item;
291
	let items = list_items(list);
292
	let index = items.indexOf(old_item);
293
	items[index] = new_item;
294
	await setn([old_key, undefined, new_key, new_val,
295
		    "_" + list.name, items]);
296

    
297
	list.map.delete(old_item);
298

    
299
	let change = {
300
	    prefix : list.prefix,
301
	    item : old_item,
302
	    old_val,
303
	    new_val : undefined
304
	};
305

    
306
	broadcast_change(change, list);
307

    
308
	list.map.set(new_item, new_val);
309

    
310
	change.item = new_item;
311
	change.old_val = undefined;
312
	change.new_val = new_val;
313

    
314
	broadcast_change(change, list);
315

    
316
	return old_val;
317
    }
318

    
319
    /*
320
     * For scripts, item name is chosen by user, data should be
321
     * an object containing:
322
     * - script's url and hash or
323
     * - script's text or
324
     * - all three
325
     */
326

    
327
    /*
328
     * For bags, item name is chosen by user, data is an array of 2-element
329
     * arrays with type prefix and script/bag names.
330
     */
331

    
332
    /*
333
     * For pages data argument is an object with properties `allow'
334
     * and `components'. Item name is url.
335
     */
336

    
337
    exports.set = async function (prefix, item, data)
338
    {
339
	return set_item(item, data, list_by_prefix[prefix]);
340
    }
341

    
342
    exports.get = function (prefix, item)
343
    {
344
	return list_by_prefix[prefix].map.get(item);
345
    }
346

    
347
    exports.remove = async function (prefix, item)
348
    {
349
	return remove_item(item, list_by_prefix[prefix]);
350
    }
351

    
352
    exports.replace = async function (prefix, old_item, new_item,
353
				      new_data=undefined)
354
    {
355
	return replace_item(old_item, new_item, list_by_prefix[prefix],
356
			    new_data);
357
    }
358

    
359
    exports.get_all_names = function (prefix)
360
    {
361
	return list_items(list_by_prefix[prefix]);
362
    }
363

    
364
    exports.get_all_names_it = function (prefix)
365
    {
366
	return list_items_it(list_by_prefix[prefix]);
367
    }
368

    
369
    exports.get_all = function (prefix)
370
    {
371
	return list_entries(list_by_prefix[prefix]);
372
    }
373

    
374
    exports.get_all_it = function (prefix)
375
    {
376
	return list_entries_it(list_by_prefix[prefix]);
377
    }
378

    
379
    /* Finally, a quick way to wipe all the data. */
380
    // TODO: maybe delete items in such order that none of them ever references
381
    // an already-deleted one?
382
    exports.clear = async function ()
383
    {
384
	let lists = list_prefixes.map((p) => list_by_prefix[p]);
385

    
386
	for (let list of lists)
387
	    await lock(list.lock);
388

    
389
	for (let list of lists) {
390

    
391
	    let change = {
392
		prefix : list.prefix,
393
		new_val : undefined
394
	    };
395

    
396
	    for (let [item, val] of list_entries_it(list)) {
397
		change.item = item;
398
		change.old_val = val;
399
		broadcast_change(change, list);
400
	    }
401

    
402
	    list.map = new Map();
403
	}
404

    
405
	await browser.storage.local.clear();
406

    
407
	for (let list of lists)
408
	    unlock(list.lock);
409
    }
410

    
411
    window.get_storage = make_once(init);
412
})();
(8-8/9)