summaryrefslogtreecommitdiff
path: root/common
diff options
context:
space:
mode:
authorWojtek Kosior <wk@koszkonutek-tmp.pl.eu.org>2021-05-10 18:07:05 +0200
committerWojtek Kosior <wk@koszkonutek-tmp.pl.eu.org>2021-05-10 18:18:52 +0200
commit01937dc9d5215ef96ce756e3ccda51bf29032f58 (patch)
tree609ec5bb48c692796520f7982c06b30633038588 /common
downloadbrowser-extension-01937dc9d5215ef96ce756e3ccda51bf29032f58.tar.gz
browser-extension-01937dc9d5215ef96ce756e3ccda51bf29032f58.zip
initial commit
Diffstat (limited to 'common')
-rw-r--r--common/browser.mjs18
-rw-r--r--common/connection_types.mjs21
-rw-r--r--common/is_background.mjs17
-rw-r--r--common/lock.mjs55
-rw-r--r--common/once.mjs42
-rw-r--r--common/storage_client.mjs186
-rw-r--r--common/stored_types.mjs38
7 files changed, 377 insertions, 0 deletions
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};