From 01937dc9d5215ef96ce756e3ccda51bf29032f58 Mon Sep 17 00:00:00 2001 From: Wojtek Kosior Date: Mon, 10 May 2021 18:07:05 +0200 Subject: initial commit --- background/storage.mjs | 380 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 380 insertions(+) create mode 100644 background/storage.mjs (limited to 'background/storage.mjs') diff --git a/background/storage.mjs b/background/storage.mjs new file mode 100644 index 0000000..00b1ace --- /dev/null +++ b/background/storage.mjs @@ -0,0 +1,380 @@ +/** +* Myext storage manager +* +* Copyright (C) 2021 Wojtek Kosior +* +* Dual-licensed under: +* - 0BSD license +* - GPLv3 or (at your option) any later version +*/ + +import {TYPE_PREFIX, TYPE_NAME, list_prefixes} from '/common/stored_types.mjs'; +import {make_lock, lock, unlock} from '/common/lock.mjs'; +import make_once from '/common/once.mjs'; +import browser from '/common/browser.mjs'; + +"use strict"; + +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 = window.browser === undefined ? + 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 a 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 same as for bundles. 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); +} + +export default make_once(init); -- cgit v1.2.3