aboutsummaryrefslogtreecommitdiff
path: root/common
diff options
context:
space:
mode:
Diffstat (limited to 'common')
-rw-r--r--common/broadcast.js14
-rw-r--r--common/entities.js2
-rw-r--r--common/indexeddb.js115
3 files changed, 105 insertions, 26 deletions
diff --git a/common/broadcast.js b/common/broadcast.js
index bc18103..cc11a20 100644
--- a/common/broadcast.js
+++ b/common/broadcast.js
@@ -59,6 +59,20 @@ function out(sender_conn, channel_name, value)
sender_conn.port.postMessage(["broadcast", channel_name, value]);
}
+/*
+ * prepare()'d message will be broadcasted if the connection is closed or when
+ * flush() is called. All messages prepared by given sender will be discarded
+ * when discard() is called.
+ *
+ * Timeout will cause the prepared message to be broadcasted after given number
+ * of miliseconds unless discard() is called in the meantime. It is mostly
+ * useful as a fallback in case this connection end gets nuked (e.g. because
+ * browser window is closed) and onDisconnect event is not dispatched to
+ * background script's Port object. This is only an issue for browsers as old as
+ * IceCat 60: https://bugzilla.mozilla.org/show_bug.cgi?id=1392067
+ *
+ * Timeout won't be scheduled at all if its value is given as 0.
+ */
function prepare(sender_conn, channel_name, value, timeout=5000)
{
sender_conn.port.postMessage(["prepare", channel_name, value, timeout]);
diff --git a/common/entities.js b/common/entities.js
index 46836b5..3a1346a 100644
--- a/common/entities.js
+++ b/common/entities.js
@@ -45,7 +45,7 @@
* Convert ver_str into an array representation, e.g. for ver_str="4.6.13.0"
* return [4, 6, 13, 0].
*/
-const parse_version = ver_str => ver_str.split(".").map(parseInt);
+const parse_version = ver_str => ver_str.split(".").map(n => parseInt(n));
/*
* ver is an array of integers. rev is an optional integer. Produce string
diff --git a/common/indexeddb.js b/common/indexeddb.js
index 96d19b7..1741c91 100644
--- a/common/indexeddb.js
+++ b/common/indexeddb.js
@@ -45,6 +45,7 @@
* IMPORTS_START
* IMPORT initial_data
* IMPORT entities
+ * IMPORT broadcast
* IMPORTS_END
*/
@@ -124,22 +125,43 @@ async function get_db()
await _save_items(initial_data.resources, initial_data.mappings, ctx);
}
- db = opened_db;
+ if (db)
+ opened_db.close();
+ else
+ db = opened_db;
return db;
}
+/* Helper function used by make_context(). */
+function reject_discard(context)
+{
+ broadcast.discard(context.sender);
+ broadcast.close(context.sender);
+ context.reject();
+}
+
+/* Helper function used by make_context(). */
+function resolve_flush(context)
+{
+ broadcast.close(context.sender);
+ context.resolve();
+}
+
/* Helper function used by start_items_transaction() and get_db(). */
function make_context(transaction, files)
{
- files = files || {};
- const context = {transaction, files, file_uses: {}};
+ const sender = broadcast.sender_connection();
+ files = files || {};
let resolve, reject;
- context.result = new Promise((...cbs) => [resolve, reject] = cbs);
+ const result = new Promise((...cbs) => [resolve, reject] = cbs);
+
+ const context =
+ {sender, transaction, resolve, reject, result, files, file_uses: {}};
- context.transaction.oncomplete = resolve;
- context.transaction.onerror = reject;
+ transaction.oncomplete = () => resolve_flush(context);
+ transaction.onerror = () => reject_discard(context);
return context;
}
@@ -221,13 +243,6 @@ async function finalize_items_transaction(context)
return context.result;
}
-async function with_items_transaction(cb, item_store_names, files={})
-{
- const context = await start_items_transaction(item_store_names, files);
- await cb(context);
- await finalize_items_transaction(context);
-}
-
/*
* How a sample data argument to the function below might look like:
*
@@ -265,10 +280,10 @@ async function with_items_transaction(cb, item_store_names, files={})
* }
* }
*/
-async function save_items(transaction, data)
+async function save_items(data)
{
- const items_store_names = ["resources", "mappings"];
- const context = start_items_transaction(items_store_names, data.files);
+ const item_store_names = ["resources", "mappings"];
+ const context = await start_items_transaction(item_store_names, data.files);
return _save_items(data.resources, data.mappings, context);
}
@@ -302,6 +317,8 @@ async function save_item(item, context)
for (const file_ref of entities.get_files(item))
await incr_file_uses(context, file_ref);
+ broadcast.prepare(context.sender, `idb_changes_${store_name}`,
+ item.identifier);
await _remove_item(store_name, item.identifier, context, false);
await idb_put(context.transaction, store_name, item);
}
@@ -326,30 +343,78 @@ async function _remove_item(store_name, identifier, context)
*/
async function remove_item(store_name, identifier, context)
{
+ broadcast.prepare(context.sender, `idb_changes_${store_name}`, identifier);
await _remove_item(store_name, identifier, context);
await idb_del(context.transaction, store_name, identifier);
}
-const remove_resource =
- (identifier, ctx) => remove_item("resources", identifier, ctx);
-const remove_mapping =
- (identifier, ctx) => remove_item("mappings", identifier, ctx);
+/* Callback used when listening to broadcasts while tracking db changes. */
+async function track_change(tracking, identifier)
+{
+ const transaction =
+ (await haketilodb.get()).transaction([tracking.store_name]);
+ const new_val = await idb_get(transaction, tracking.store_name, identifier);
+
+ tracking.onchange({identifier, new_val});
+}
+
+/*
+ * Monitor changes to `store_name` IndexedDB object store.
+ *
+ * `store_name` should be either "resources" or "mappings".
+ *
+ * `onchange` should be a callback that will be called when an item is added,
+ * modified or removed from the store. The callback will be passed an object
+ * representing the change as its first argument. This object will have the
+ * form:
+ * {
+ * identifier: "the identifier of modified resource/mapping",
+ * new_val: undefined // `undefined` if item removed, item object otherwise
+ * }
+ *
+ * Returns a [tracking, all_current_items] array where `tracking` is an object
+ * that can be later passed to haketilodb.untrack() to stop tracking changes and
+ * `all_current_items` is an array of items currently present in the object
+ * store.
+ *
+ * It is possible that `onchange` gets spuriously fired even when an item is not
+ * actually modified or that it only gets called once after multiple quick
+ * changes to an item.
+ */
+async function track(store_name, onchange)
+{
+ const tracking = {store_name, onchange};
+ tracking.listener =
+ broadcast.listener_connection(msg => track_change(tracking, msg[1]));
+ broadcast.subscribe(tracking.listener, `idb_changes_${store_name}`);
+
+ const transaction = (await haketilodb.get()).transaction([store_name]);
+ const all_req = transaction.objectStore(store_name).getAll();
+
+ return [tracking, (await wait_request(all_req)).target.result];
+}
+
+function untrack(tracking)
+{
+ broadcast.close(tracking.listener);
+}
const haketilodb = {
get: get_db,
save_items,
save_item,
- remove_resource,
- remove_mapping,
+ remove_resource: (id, ctx) => remove_item("resources", id, ctx),
+ remove_mapping: (id, ctx) => remove_item("mappings", id, ctx),
start_items_transaction,
- finalize_items_transaction
+ finalize_items_transaction,
+ track_resources: onchange => track("resources", onchange),
+ track_mappings: onchange => track("mappings", onchange),
+ untrack
};
/*
* EXPORTS_START
* EXPORT haketilodb
* EXPORT idb_get
- * EXPORT idb_put
- * EXPORT idb_del
* EXPORTS_END
*/