From 01937dc9d5215ef96ce756e3ccda51bf29032f58 Mon Sep 17 00:00:00 2001 From: Wojtek Kosior Date: Mon, 10 May 2021 18:07:05 +0200 Subject: initial commit --- common/browser.mjs | 18 +++++ common/connection_types.mjs | 21 +++++ common/is_background.mjs | 17 ++++ common/lock.mjs | 55 +++++++++++++ common/once.mjs | 42 ++++++++++ common/storage_client.mjs | 186 ++++++++++++++++++++++++++++++++++++++++++++ common/stored_types.mjs | 38 +++++++++ 7 files changed, 377 insertions(+) create mode 100644 common/browser.mjs create mode 100644 common/connection_types.mjs create mode 100644 common/is_background.mjs create mode 100644 common/lock.mjs create mode 100644 common/once.mjs create mode 100644 common/storage_client.mjs create mode 100644 common/stored_types.mjs (limited to 'common') diff --git a/common/browser.mjs b/common/browser.mjs new file mode 100644 index 0000000..0d1b233 --- /dev/null +++ b/common/browser.mjs @@ -0,0 +1,18 @@ +/** +* Myext WebExtension API access normalization +* +* Copyright (C) 2021 Wojtek Kosior +* +* Dual-licensed under: +* - 0BSD license +* - GPLv3 or (at your option) any later version +*/ + +"use strict"; + +/* + * This module normalizes access to WebExtension apis between + * chrome-based and firefox-based browsers. + */ + +export default (window.browser === undefined) ? chrome : browser; diff --git a/common/connection_types.mjs b/common/connection_types.mjs new file mode 100644 index 0000000..12d6de3 --- /dev/null +++ b/common/connection_types.mjs @@ -0,0 +1,21 @@ +/** +* Myext background scripts message connection types "enum" +* +* Copyright (C) 2021 Wojtek Kosior +* +* Dual-licensed under: +* - 0BSD license +* - GPLv3 or (at your option) any later version +*/ + +/* + * Those need to be strings so they can be used as 'name' parameter + * to browser.runtime.connect() + */ + +const CONNECTION_TYPE = { + REMOTE_STORAGE : "0", + PAGE_ACTIONS : "1" +}; + +export default CONNECTION_TYPE; diff --git a/common/is_background.mjs b/common/is_background.mjs new file mode 100644 index 0000000..ef728a7 --- /dev/null +++ b/common/is_background.mjs @@ -0,0 +1,17 @@ +/** +* Myext programmatic check of where the script is being run +* +* Copyright (C) 2021 Wojtek Kosior +* +* Dual-licensed under: +* - 0BSD license +* - GPLv3 or (at your option) any later version +*/ + +/* This needs to be changed if we ever modify the html file path. */ + +export default function is_background() +{ + return window.location.protocol === "moz-extension:" && + window.location.pathname === "/background/background.html"; +} diff --git a/common/lock.mjs b/common/lock.mjs new file mode 100644 index 0000000..596dd9c --- /dev/null +++ b/common/lock.mjs @@ -0,0 +1,55 @@ +/** +* Myext lock (aka binary semaphore aka mutex) +* +* Copyright (C) 2021 Wojtek Kosior +* +* Dual-licensed under: +* - 0BSD license +* - GPLv3 or (at your option) any later version +*/ + +/* + * Javascript runs single-threaded, with an event loop. Because of that, + * explicit synchronization is often not needed. An exception is when we use + * an API function that must wait. Ajax is an example. Callback passed to ajax + * call doesn't get called immediately, but after some time. In the meantime + * some other piece of code might get to execute and modify some variables. + * Access to WebExtension local storage is another situation where this problem + * can occur. + * + * This is a solution. A lock object, that can be used to delay execution of + * some code until other code finishes its critical work. Locking is wrapped + * in a promise. + */ + +"use strict"; + +export function make_lock() { + return {free: true, queue: []}; +} + +function _lock(lock, cb) { + if (lock.free) { + lock.free = false; + setTimeout(cb); + } else { + lock.queue.push(cb); + } +} + +export function lock(lock) { + return new Promise((resolve, reject) => _lock(lock, resolve)); +} + +export function unlock(lock) { + if (lock.free) + throw new Exception("Attempting to release a free lock"); + + if (lock.queue.length === 0) { + lock.free = true; + } else { + let cb = lock.queue[0]; + lock.queue.splice(0, 1); + setTimeout(cb); + } +} diff --git a/common/once.mjs b/common/once.mjs new file mode 100644 index 0000000..0f76366 --- /dev/null +++ b/common/once.mjs @@ -0,0 +1,42 @@ +/** +* Myext feature initialization promise +* +* Copyright (C) 2021 Wojtek Kosior +* +* Dual-licensed under: +* - 0BSD license +* - GPLv3 or (at your option) any later version +*/ + +"use strict"; + +/* + * This module provides an easy way to wrap an async function into a promise + * so that it only gets executed once. + */ + +async function assign_result(state, result_producer) +{ + state.result = await result_producer(); + state.ready = true; + for (let cb of state.waiting) + setTimeout(cb, 0, state.result); + state.waiting = undefined; +} + +async function get_result(state) +{ + if (state.ready) + return state.result; + + return new Promise((resolve, reject) => state.waiting.push(resolve)); +} + +export function make_once(result_producer) +{ + let state = {waiting : [], ready : false, result : undefined}; + assign_result(state, result_producer); + return () => get_result(state); +} + +export default make_once; diff --git a/common/storage_client.mjs b/common/storage_client.mjs new file mode 100644 index 0000000..8260ad7 --- /dev/null +++ b/common/storage_client.mjs @@ -0,0 +1,186 @@ +/** +* Myext storage through connection (client side) +* +* Copyright (C) 2021 Wojtek Kosior +* +* Dual-licensed under: +* - 0BSD license +* - GPLv3 or (at your option) any later version +*/ + +"use strict"; + +import CONNECTION_TYPE from './connection_types.mjs'; +import {TYPE_PREFIX, list_prefixes} from '/common/stored_types.mjs'; +import make_once from './once.mjs'; +import browser from '/common/browser.mjs'; + +var call_id = 0; +var port; +var calls_waiting = new Map(); + +function set_call_callback(resolve, reject, func, args) +{ + port.postMessage([call_id, func, args]); + calls_waiting.set(call_id++, [resolve, reject]); +} + +async function remote_call(func, args) +{ + return new Promise((resolve, reject) => + set_call_callback(resolve, reject, func, args)); +} + +function handle_message(message) +{ + let callbacks = calls_waiting.get(message.call_id); + if (callbacks === undefined) { + handle_change(message); + return; + } + + let [resolve, reject] = callbacks; + calls_waiting.delete(message.call_id); + if (message.error !== undefined) + setTimeout(reject, 0, message.error); + else + setTimeout(resolve, 0, message.result); +} + +function list(name, prefix) +{ + return {prefix, name, listeners : new Set()}; +} + +var scripts = list("scripts", TYPE_PREFIX.SCRIPT); +var bundles = list("bundles", TYPE_PREFIX.BUNDLE); +var pages = list("pages", TYPE_PREFIX.PAGE); + +const list_by_prefix = { + [TYPE_PREFIX.SCRIPT] : scripts, + [TYPE_PREFIX.BUNDLE] : bundles, + [TYPE_PREFIX.PAGE] : pages +}; + +var resolve_init; + +function handle_first_message(message) +{ + for (let prefix of Object.keys(message)) + list_by_prefix[prefix].map = new Map(message[prefix]); + + port.onMessage.removeListener(handle_first_message); + port.onMessage.addListener(handle_message); + + resolve_init(); +} + +function handle_change(change) +{ + let list = list_by_prefix[change.prefix]; + + if (change.new_val === undefined) + list.map.delete(change.item); + else + list.map.set(change.item, change.new_val); + + for (let listener_callback of list.listeners) + listener_callback(change); +} + +var exports = {}; + +function start_connection(resolve) +{ + resolve_init = resolve; + port = browser.runtime.connect({name : CONNECTION_TYPE.REMOTE_STORAGE}); + port.onMessage.addListener(handle_first_message); +} + +async function init() { + await new Promise((resolve, reject) => start_connection(resolve)); + return exports; +} + +for (let call_name of ["set", "remove", "replace", "clear"]) + exports [call_name] = (...args) => remote_call(call_name, args); + +// TODO: Much of the code below is copy-pasted from /background/storage.mjs. +// This should later be refactored into a separate module +// to avoid duplication. + +/* + * 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); +} + +/* 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); +} + +exports.get = function (prefix, item) +{ + return list_by_prefix[prefix].map.get(item); +} + +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]); +} + +export default make_once(init); diff --git a/common/stored_types.mjs b/common/stored_types.mjs new file mode 100644 index 0000000..8545d44 --- /dev/null +++ b/common/stored_types.mjs @@ -0,0 +1,38 @@ +/** +* Myext stored item types "enum" +* +* Copyright (C) 2021 Wojtek Kosior +* +* Dual-licensed under: +* - 0BSD license +* - GPLv3 or (at your option) any later version +*/ + +/* + * Key for item that is stored in quantity (script, page) is constructed by + * prepending its name with first letter of its list name. However, we also + * need to store some items that don't belong to any list. Let's call them + * persisted variables. In such case item's key is its "name" prepended with + * an underscore. + */ + +const TYPE_PREFIX = { + PAGE : "p", + BUNDLE : "b", + SCRIPT : "s", + VAR : "_" +}; + +const TYPE_NAME = { + [TYPE_PREFIX.PAGE] : "page", + [TYPE_PREFIX.BUNDLE] : "bundle", + [TYPE_PREFIX.SCRIPT] : "script" +} + +const list_prefixes = [ + TYPE_PREFIX.PAGE, + TYPE_PREFIX.BUNDLE, + TYPE_PREFIX.SCRIPT +]; + +export {TYPE_PREFIX, TYPE_NAME, list_prefixes}; -- cgit v1.2.3