diff options
Diffstat (limited to 'background/storage.js')
-rw-r--r-- | background/storage.js | 397 |
1 files changed, 397 insertions, 0 deletions
diff --git a/background/storage.js b/background/storage.js new file mode 100644 index 0000000..ea390ef --- /dev/null +++ b/background/storage.js @@ -0,0 +1,397 @@ +/** +* Myext storage manager +* +* Copyright (C) 2021 Wojtek Kosior +* +* Dual-licensed under: +* - 0BSD license +* - GPLv3 or (at your option) any later version +*/ + +"use strict"; + +(() => { + const TYPE_PREFIX = window.TYPE_PREFIX; + const TYPE_NAME = window.TYPE_NAME; + const list_prefixes = window.list_prefixes; + const make_lock = window.make_lock; + const lock = window.lock; + const unlock = window.unlock; + const make_once = window.make_once; + const browser = window.browser; + const is_chrome = window.is_chrome; + + var exports = {}; + + /* We're yet to decide how to handle errors... */ + + /* Here are some basic wrappers for storage API functions */ + + async function get(key) + { + try { + /* Fix for fact that Chrome does not use promises here */ + let promise = is_chrome ? + new Promise((resolve, reject) => + chrome.storage.local.get(key, + val => resolve(val))) : + browser.storage.local.get(key); + + return (await promise)[key]; + } catch (e) { + console.log(e); + } + } + + async function set(key, value) + { + try { + return browser.storage.local.set({[key]: value}); + } catch (e) { + console.log(e); + } + } + + async function setn(keys_and_values) + { + let obj = Object(); + while (keys_and_values.length > 1) { + let value = keys_and_values.pop(); + let key = keys_and_values.pop(); + obj[key] = value; + } + + try { + return browser.storage.local.set(obj); + } catch (e) { + console.log(e); + } + } + + async function set_var(name, value) + { + return set(TYPE_PREFIX.VAR + name, value); + } + + async function get_var(name) + { + return get(TYPE_PREFIX.VAR + name); + } + + /* + * A special case of persisted variable is one that contains list + * of items. + */ + + async function get_list_var(name) + { + let list = await get_var(name); + + return list === undefined ? [] : list; + } + + /* We maintain in-memory copies of some stored lists. */ + + async function list(prefix) + { + let name = TYPE_NAME[prefix] + "s"; /* Make plural. */ + let map = new Map(); + + for (let item of await get_list_var(name)) + map.set(item, await get(prefix + item)); + + return {map, prefix, name, listeners : new Set(), lock : make_lock()}; + } + + var pages; + var bundles; + var scripts; + + var list_by_prefix = {}; + + async function init() + { + for (let prefix of list_prefixes) + list_by_prefix[prefix] = await list(prefix); + + return exports; + } + + /* + * Facilitate listening to changes + */ + + exports.add_change_listener = function (cb, prefixes=list_prefixes) + { + if (typeof(prefixes) === "string") + prefixes = [prefixes]; + + for (let prefix of prefixes) + list_by_prefix[prefix].listeners.add(cb); + } + + exports.remove_change_listener = function (cb, prefixes=list_prefixes) + { + if (typeof(prefixes) === "string") + prefixes = [prefixes]; + + for (let prefix of prefixes) + list_by_prefix[prefix].listeners.delete(cb); + } + + function broadcast_change(change, list) + { + for (let listener_callback of list.listeners) + listener_callback(change); + } + + /* Prepare some hepler functions to get elements of a list */ + + function list_items_it(list, with_values=false) + { + return with_values ? list.map.entries() : list.map.keys(); + } + + function list_entries_it(list) + { + return list_items_it(list, true); + } + + function list_items(list, with_values=false) + { + let array = []; + + for (let item of list_items_it(list, with_values)) + array.push(item); + + return array; + } + + function list_entries(list) + { + return list_items(list, true); + } + + /* + * Below we make additional effort to update map of given kind of items + * every time an item is added/removed to keep everything coherent. + */ + async function set_item(item, value, list) + { + await lock(list.lock); + let result = await _set_item(...arguments); + unlock(list.lock) + return result; + } + async function _set_item(item, value, list) + { + let key = list.prefix + item; + let old_val = list.map.get(item); + if (old_val === undefined) { + let items = list_items(list); + items.push(item); + await setn([key, value, "_" + list.name, items]); + } else { + await set(key, value); + } + + list.map.set(item, value) + + let change = { + prefix : list.prefix, + item, + old_val, + new_val : value + }; + + broadcast_change(change, list); + + return old_val; + } + + // TODO: The actual idea to set value to undefined is good - this way we can + // also set a new list of items in the same API call. But such key + // is still stored in the storage. We need to somehow remove it later. + // For that, we're going to have to store 1 more list of each kind. + async function remove_item(item, list) + { + await lock(list.lock); + let result = await _remove_item(...arguments); + unlock(list.lock) + return result; + } + async function _remove_item(item, list) + { + let old_val = list.map.get(item); + if (old_val === undefined) + return; + + let key = list.prefix + item; + let items = list_items(list); + let index = items.indexOf(item); + items.splice(index, 1); + + await setn([key, undefined, "_" + list.name, items]); + + list.map.delete(item); + + let change = { + prefix : list.prefix, + item, + old_val, + new_val : undefined + }; + + broadcast_change(change, list); + + return old_val; + } + + // TODO: same as above applies here + async function replace_item(old_item, new_item, list, new_val=undefined) + { + await lock(list.lock); + let result = await _replace_item(...arguments); + unlock(list.lock) + return result; + } + async function _replace_item(old_item, new_item, list, new_val=undefined) + { + let old_val = list.map.get(old_item); + if (new_val === undefined) { + if (old_val === undefined) + return; + new_val = old_val + } else if (new_val === old_val && new_item === old_item) { + return old_val; + } + + if (old_item === new_item || old_val === undefined) { + await _set_item(new_item, new_val, list); + return old_val; + } + + let new_key = list.prefix + new_item; + let old_key = list.prefix + old_item; + let items = list_items(list); + let index = items.indexOf(old_item); + items[index] = new_item; + await setn([old_key, undefined, new_key, new_val, + "_" + list.name, items]); + + list.map.delete(old_item); + + let change = { + prefix : list.prefix, + item : old_item, + old_val, + new_val : undefined + }; + + broadcast_change(change, list); + + list.map.set(new_item, new_val); + + change.item = new_item; + change.old_val = undefined; + change.new_val = new_val; + + broadcast_change(change, list); + + return old_val; + } + + /* + * For scripts, item name is chosen by user, data should be + * an object containing: + * - script's url and hash or + * - script's text or + * - all three + */ + + /* + * For bundles, item name is chosen by user, data is an array of 2-element + * arrays with type prefix and script/bundle names. + */ + + /* + * For pages data argument is an object with properties `allow' + * and `components'. Item name is url. + */ + + exports.set = async function (prefix, item, data) + { + return set_item(item, data, list_by_prefix[prefix]); + } + + exports.get = function (prefix, item) + { + return list_by_prefix[prefix].map.get(item); + } + + exports.remove = async function (prefix, item) + { + return remove_item(item, list_by_prefix[prefix]); + } + + exports.replace = async function (prefix, old_item, new_item, + new_data=undefined) + { + return replace_item(old_item, new_item, list_by_prefix[prefix], + new_data); + } + + exports.get_all_names = function (prefix) + { + return list_items(list_by_prefix[prefix]); + } + + exports.get_all_names_it = function (prefix) + { + return list_items_it(list_by_prefix[prefix]); + } + + exports.get_all = function (prefix) + { + return list_entries(list_by_prefix[prefix]); + } + + exports.get_all_it = function (prefix) + { + return list_entries_it(list_by_prefix[prefix]); + } + + /* Finally, a quick way to wipe all the data. */ + // TODO: maybe delete items in such order that none of them ever references + // an already-deleted one? + exports.clear = async function () + { + let lists = list_prefixes.map((p) => list_by_prefix[p]); + + for (let list of lists) + await lock(list.lock); + + for (let list of lists) { + + let change = { + prefix : list.prefix, + new_val : undefined + }; + + for (let [item, val] of list_entries_it(list)) { + change.item = item; + change.old_val = val; + broadcast_change(change, list); + } + + list.map = new Map(); + } + + await browser.storage.local.clear(); + + for (let list of lists) + unlock(list.lock); + } + + window.get_storage = make_once(init); +})(); |