aboutsummaryrefslogtreecommitdiff
/**
 * This file is part of Haketilo.
 *
 * Function: 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)
	observ                         
                        
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 */