diff background.js @ 1:b0827360b8e4

Store favicons and thumbnails in local database
author Guido Berhoerster <guido+set-aside@berhoerster.name>
date Mon, 19 Nov 2018 17:26:17 +0100
parents d13d59494613
children 9d699dc7823d
line wrap: on
line diff
--- a/background.js	Sat Nov 17 10:44:16 2018 +0100
+++ b/background.js	Mon Nov 19 17:26:17 2018 +0100
@@ -58,22 +58,84 @@
     };
 }
 
+class ObjectStoreDB {
+    constructor(dbName = 'defaultDatabase', objectStoreName = 'objectStore') {
+        this.dbName = dbName;
+        this.objectStoreName = objectStoreName;
+        this._db = undefined;
+        this.openingDB = new Promise((resolve, reject) => {
+            let request = indexedDB.open(this.dbName);
+            request.addEventListener('error', ev => {
+                reject(request.error);
+            });
+            request.addEventListener('success', ev => {
+                resolve(request.result);
+            });
+            request.addEventListener('upgradeneeded', ev => {
+                request.result.createObjectStore(this.objectStoreName);
+            });
+        });
+    }
+
+    async _execTransaction(method, ...methodArguments) {
+        if (typeof this._db === 'undefined') {
+            this._db = await this.openingDB;
+        }
+        return new Promise((resolve, reject) => {
+            let transaction = this._db.transaction(this.objectStoreName,
+                    method.startsWith('get') ? 'readonly' : 'readwrite');
+            let objectStore = transaction.objectStore(this.objectStoreName);
+            let request = objectStore[method](...methodArguments);
+            transaction.addEventListener('complete', ev =>
+                    method.startsWith('get') ?
+                    resolve(request.result) :
+                    resolve());
+            transaction.addEventListener('abort', ev =>
+                    reject(transaction.error));
+            transaction.addEventListener('error', ev =>
+                    reject(transaction.error));
+        });
+    }
+
+    async get(key) {
+        return key === null || typeof key === 'undefined' ?
+                this._execTransaction('getAll') :
+                this._execTransaction('get', key);
+    }
+
+    async keys() {
+        return this._execTransaction('getAllKeys');
+    }
+
+    async set(key, value) {
+        return this._execTransaction('put', value, key)
+    }
+
+    async delete(key) {
+        return this._execTransaction('delete', key)
+    }
+
+    async clear(key) {
+        return this._execTransaction('clear')
+    }
+}
+
 class Tab {
     static deserialize(object) {
         return new Tab(object);
     }
 
-    constructor({url, title, uuid = generateUuidV4String(), favIconUrl = null,
-            thumbnailUrl = null}) {
+    constructor({url, title, uuid = generateUuidV4String(), favIcon = null,
+            thumbnail = null}) {
         this.uuid = uuid;
         this.url = url;
         this.title = title;
-        this.favIconUrl = favIconUrl;
-        this.thumbnailUrl = thumbnailUrl;
+        this.favIcon = favIcon;
+        this.thumbnail = thumbnail;
     }
 
     serialize() {
-        return Object.assign({}, this);
+        return Object.assign({}, this, {favIcon: null, thumbnail: null});
     }
 }
 
@@ -110,6 +172,7 @@
 class TabCollectionsStorageProxy {
     constructor() {
         this.tabCollections = new Map();
+        this.objectStoreDB = new ObjectStoreDB('tabCollections');
         this.ports = new Set();
         this.browserVersion = undefined;
         this.messageQueue = [];
@@ -133,6 +196,22 @@
         console.groupEnd();
         browser.storage.onChanged.addListener(this.onStorageChanged.bind(this));
 
+        // get favicon and thumbnail data from local database
+        let updatingTabData = [];
+        for (let tabCollectionUuid of this.tabCollections.keys()) {
+            updatingTabData.push(this.updateTabData(tabCollectionUuid));
+        }
+        await Promise.all(updatingTabData);
+
+        // remove stale data from local database
+        for (let tabCollectionUuid of await this.objectStoreDB.keys()) {
+            if (!this.tabCollections.has(tabCollectionUuid)) {
+                console.log('removing data for stale tab collection',
+                    tabCollectionUuid);
+                this.objectStoreDB.delete(tabCollectionUuid);
+            }
+        }
+
         this.isInitialized = true;
 
         while (this.messageQueue.length > 0) {
@@ -145,7 +224,7 @@
 
     async createTabThumbnail(tabId) {
         let captureUrl = await browser.tabs.captureTab(tabId);
-        let thumbnailUrl = await new Promise((resolve, reject) => {
+        let thumbnailBlob = await new Promise((resolve, reject) => {
             let image = new Image();
             image.addEventListener('load', ev => {
                 let canvas = document.createElement('canvas');
@@ -159,14 +238,14 @@
                 ctx.fillRect(0, 0, canvas.width, canvas.height);
                 ctx.drawImage(image, 0, 0, dWidth, dHeight);
 
-                resolve(canvas.toDataURL('image/jpeg', 0.75));
+                canvas.toBlob(resolve, 'image/jpeg', 0.75);
             });
             image.addEventListener('error', e => {
                 reject(e);
             });
             image.src = captureUrl;
         });
-        return thumbnailUrl;
+        return thumbnailBlob;
     }
 
     async createTabCollection(windowId) {
@@ -188,14 +267,27 @@
             return;
         }
 
-        let tabs = browserTabs.map(browserTab => {
+        let tabs = await Promise.all(browserTabs.map(async browserTab => {
+            // convert favicon data URI to blob
+            let favIcon = null;
+            if (!browserTab.discarded) {
+                try {
+                    let response = await fetch(browserTab.favIconUrl);
+                    favIcon = await response.blob();
+                } catch (e) {
+                    if (!(e instanceof AbortError)) {
+                        throw e;
+                    }
+                }
+            }
+
             let tab = new Tab({
                 url: browserTab.url,
                 title: browserTab.title,
-                favIconUrl: browserTab.favIconUrl
+                favIcon
             });
             return [tab.uuid, tab];
-        });
+        }));
 
         // create empty tab which becomes the new active tab
         await browser.tabs.create({active: true});
@@ -207,12 +299,25 @@
                 !browserTab.discarded ?
                 this.createTabThumbnail(browserTab.id) : null));
         for (let [, tab] of tabs) {
-            tab.thumbnailUrl = thumbnails.shift();
+            tab.thumbnail = thumbnails.shift();
         }
 
         let tabCollection = new TabCollection({tabs});
         console.log('created tab collection:', tabCollection);
 
+        // store tab favicons and thumbnails
+        let tabCollectionData = {
+            uuid: tabCollection.uuid,
+            tabs: new Map()
+        };
+        for (let [uuid, tab] of tabs) {
+            tabCollectionData.tabs.set(uuid, {
+                favIcon: tab.favIcon,
+                thumbnail: tab.thumbnail
+            });
+        }
+        await this.objectStoreDB.set(tabCollectionData.uuid, tabCollectionData);
+
         // store tab collection
         console.log('storing tab collection:', tabCollection);
         await browser.storage.sync.set({
@@ -258,6 +363,7 @@
     async removeTabCollection(tabCollectionUuid) {
         console.log('removing tab collection %s', tabCollectionUuid);
         await browser.storage.sync.remove(`collection:${tabCollectionUuid}`);
+        this.objectStoreDB.delete(tabCollectionUuid);
     }
 
     async restoreTabCollection(tabCollectionUuid, windowId) {
@@ -279,7 +385,35 @@
         await this.removeTabCollection(tabCollectionUuid);
     }
 
-    onStorageChanged(changes, areaName) {
+    async updateTabData(tabCollectionUuid) {
+        let tabCollectionDataObject;
+        try {
+            tabCollectionDataObject =
+                    await this.objectStoreDB.get(tabCollectionUuid);
+        } catch (e) {
+            console.error(`Failed to get data from database: e.message`);
+            return;
+        }
+        if (typeof tabCollectionDataObject === 'undefined') {
+            // does not exist in database
+            console.log('no data stored for tab collection', tabCollectionUuid);
+            return;
+        }
+
+        console.log(`updating tab collection ${tabCollectionUuid} with data`,
+                tabCollectionDataObject);
+        let tabCollection = this.tabCollections.get(tabCollectionUuid);
+        for (let [tabUuid, tab] of tabCollection.tabs) {
+            let tabDataObject = tabCollectionDataObject.tabs.get(tabUuid);
+            if (typeof tabDataObject === 'undefined') {
+                continue;
+            }
+            tab.favIcon = tabDataObject.favIcon;
+            tab.thumbnail = tabDataObject.thumbnail;
+        }
+    }
+
+    async onStorageChanged(changes, areaName) {
         if (areaName !== 'sync') {
             return;
         }
@@ -298,6 +432,8 @@
             // a new collection was created
             let newTabCollection = TabCollection.deserialize(newValue);
             this.tabCollections.set(tabCollectionUuid, newTabCollection);
+            // try to get tab favicons and thumbnails
+            await this.updateTabData(tabCollectionUuid);
 
             this.broadcastMessage({
                 type: 'tabCollectionCreated',
@@ -306,6 +442,7 @@
         } else if (typeof newValue === 'undefined') {
             // a collection has been removed
             this.tabCollections.delete(tabCollectionUuid);
+            this.objectStoreDB.delete(tabCollectionUuid);
 
             this.broadcastMessage({
                 type: 'tabCollectionRemoved',
@@ -315,6 +452,8 @@
             // a collection has changed
             let newTabCollection = TabCollection.deserialize(newValue);
             this.tabCollections.set(tabCollectionUuid, newTabCollection);
+            // try to get tab favicons and thumbnails
+            await this.updateTabData(tabCollectionUuid);
 
             this.broadcastMessage({
                 type: 'tabCollectionChanged',