/** * This file is part of Haketilo. * * Function: Storage manager. * * Copyright (C) 2021 Wojtek Kosior * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * As additional permission under GNU GPL version 3 section 7, you * may distribute forms of that code without the copy of the GNU * GPL normally required by section 4, provided you include this * license notice and, in case of non-source distribution, a URL * through which recipients can access the Corresponding Source. * If you modify file(s) with this exception, you may extend this * exception to your version of the file(s), but you are not * obligated to do so. If you do not wish to do so, delete this * exception statement from your version. * * As a special exception to the GPL, any HTML file which merely * makes function calls to this code, and for that purpose * includes it by reference shall be deemed a separate work for * copyright law purposes. If you modify this code, you may extend * this exception to your version of the code, but you are not * obligated to do so. If you do not wish to do so, delete this * exception statement from your version. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. * * I, Wojtek Kosior, thereby promise not to sue for violation of this file's * license. Although I request that you do not make use this code in a * proprietary program, I am not going to enforce this in court. */ /* * IMPORTS_START * IMPORT raw_storage * IMPORT TYPE_NAME * IMPORT list_prefixes * IMPORT make_lock * IMPORT lock * IMPORT unlock * IMPORT make_once * IMPORT browser * IMPORT observables * IMPORTS_END */ var exports = {}; /* A special case of persisted variable is one that contains list of items. */ async function get_list_var(name) { let list = await raw_storage.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 raw_storage.get(prefix + item)); return {map, prefix, name, observable: observables.make(), lock: make_lock()}; } 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) observables.subscribe(list_by_prefix[prefix].observable, cb); } exports.remove_change_listener = function (cb, prefixes=list_prefixes) { if (typeof(prefixes) === "string") prefixes = [prefixes]; for (let prefix of prefixes) observables.unsubscribe(list_by_prefix[prefix].observable, cb); } /* 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) { const key = list.prefix + item; const old_val = list.map.get(item); const set_obj = {[key]: value}; if (old_val === undefined) { const items = list_items(list); items.push(item); set_obj["_" + list.name] = items; } await raw_storage.set(set_obj); list.map.set(item, value); const change = { prefix : list.prefix, item, old_val, new_val : value }; observables.broadcast(list.observable, change); 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) { const old_val = list.map.get(item); if (old_val === undefined) return; const items = list_items(list); const index = items.indexOf(item); items.splice(index, 1); await raw_storage.set({ [list.prefix + item]: undefined, ["_" + list.name]: items }); list.map.delete(item); const change = { prefix : list.prefix, item, old_val, new_val : undefined }; observables.broadcast(list.observable, change); 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) { const 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; } const items = list_items(list); const index = items.indexOf(old_item); items[index] = new_item; await raw_storage.set({ [list.prefix + old_item]: undefined, [list.prefix + new_item]: new_val, ["_" + list.name]: items }); list.map.delete(old_item); const change = { prefix : list.prefix, item : old_item, old_val, new_val : undefined }; observables.broadcast(list.observable, change); list.map.set(new_item, new_val); change.item = new_item; change.old_val = undefined; change.new_val = new_val; observables.broadcast(list.observable, change); 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 bags, item name is chosen by user, data is an array of 2-element * arrays with type prefix and script/bag 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; observables.broadcast(list.observable, change); } list.map = new Map(); } await browser.storage.local.clear(); for (let list of lists) unlock(list.lock); } const get_storage = make_once(init); /* * EXPORTS_START * EXPORT get_storage * EXPORTS_END */