/**
* 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)
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
*/