From b7378a9994724750198e0d165c575be8538334fb Mon Sep 17 00:00:00 2001 From: Wojtek Kosior Date: Thu, 16 Dec 2021 14:37:09 +0100 Subject: facilitate tracking of IndexedDB item store contents --- common/broadcast.js | 14 +++++++ common/entities.js | 2 +- common/indexeddb.js | 115 ++++++++++++++++++++++++++++++++++++++++------------ 3 files changed, 105 insertions(+), 26 deletions(-) (limited to 'common') 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 */ -- cgit v1.2.3