/** * Hachette storage manager * * Copyright (C) 2021 Wojtek Kosior * Redistribution terms are gathered in the `copyright' file. */ /* * 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 */