comparison 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
comparison
equal deleted inserted replaced
0:d13d59494613 1:b0827360b8e4
56 releaseType, 56 releaseType,
57 releaseNumber, 57 releaseNumber,
58 }; 58 };
59 } 59 }
60 60
61 class ObjectStoreDB {
62 constructor(dbName = 'defaultDatabase', objectStoreName = 'objectStore') {
63 this.dbName = dbName;
64 this.objectStoreName = objectStoreName;
65 this._db = undefined;
66 this.openingDB = new Promise((resolve, reject) => {
67 let request = indexedDB.open(this.dbName);
68 request.addEventListener('error', ev => {
69 reject(request.error);
70 });
71 request.addEventListener('success', ev => {
72 resolve(request.result);
73 });
74 request.addEventListener('upgradeneeded', ev => {
75 request.result.createObjectStore(this.objectStoreName);
76 });
77 });
78 }
79
80 async _execTransaction(method, ...methodArguments) {
81 if (typeof this._db === 'undefined') {
82 this._db = await this.openingDB;
83 }
84 return new Promise((resolve, reject) => {
85 let transaction = this._db.transaction(this.objectStoreName,
86 method.startsWith('get') ? 'readonly' : 'readwrite');
87 let objectStore = transaction.objectStore(this.objectStoreName);
88 let request = objectStore[method](...methodArguments);
89 transaction.addEventListener('complete', ev =>
90 method.startsWith('get') ?
91 resolve(request.result) :
92 resolve());
93 transaction.addEventListener('abort', ev =>
94 reject(transaction.error));
95 transaction.addEventListener('error', ev =>
96 reject(transaction.error));
97 });
98 }
99
100 async get(key) {
101 return key === null || typeof key === 'undefined' ?
102 this._execTransaction('getAll') :
103 this._execTransaction('get', key);
104 }
105
106 async keys() {
107 return this._execTransaction('getAllKeys');
108 }
109
110 async set(key, value) {
111 return this._execTransaction('put', value, key)
112 }
113
114 async delete(key) {
115 return this._execTransaction('delete', key)
116 }
117
118 async clear(key) {
119 return this._execTransaction('clear')
120 }
121 }
122
61 class Tab { 123 class Tab {
62 static deserialize(object) { 124 static deserialize(object) {
63 return new Tab(object); 125 return new Tab(object);
64 } 126 }
65 127
66 constructor({url, title, uuid = generateUuidV4String(), favIconUrl = null, 128 constructor({url, title, uuid = generateUuidV4String(), favIcon = null,
67 thumbnailUrl = null}) { 129 thumbnail = null}) {
68 this.uuid = uuid; 130 this.uuid = uuid;
69 this.url = url; 131 this.url = url;
70 this.title = title; 132 this.title = title;
71 this.favIconUrl = favIconUrl; 133 this.favIcon = favIcon;
72 this.thumbnailUrl = thumbnailUrl; 134 this.thumbnail = thumbnail;
73 } 135 }
74 136
75 serialize() { 137 serialize() {
76 return Object.assign({}, this); 138 return Object.assign({}, this, {favIcon: null, thumbnail: null});
77 } 139 }
78 } 140 }
79 141
80 class TabCollection { 142 class TabCollection {
81 static deserialize(object) { 143 static deserialize(object) {
108 } 170 }
109 171
110 class TabCollectionsStorageProxy { 172 class TabCollectionsStorageProxy {
111 constructor() { 173 constructor() {
112 this.tabCollections = new Map(); 174 this.tabCollections = new Map();
175 this.objectStoreDB = new ObjectStoreDB('tabCollections');
113 this.ports = new Set(); 176 this.ports = new Set();
114 this.browserVersion = undefined; 177 this.browserVersion = undefined;
115 this.messageQueue = []; 178 this.messageQueue = [];
116 this.isInitialized = false; 179 this.isInitialized = false;
117 180
131 console.log('tab collections from storage'); 194 console.log('tab collections from storage');
132 console.table(this.tabCollections); 195 console.table(this.tabCollections);
133 console.groupEnd(); 196 console.groupEnd();
134 browser.storage.onChanged.addListener(this.onStorageChanged.bind(this)); 197 browser.storage.onChanged.addListener(this.onStorageChanged.bind(this));
135 198
199 // get favicon and thumbnail data from local database
200 let updatingTabData = [];
201 for (let tabCollectionUuid of this.tabCollections.keys()) {
202 updatingTabData.push(this.updateTabData(tabCollectionUuid));
203 }
204 await Promise.all(updatingTabData);
205
206 // remove stale data from local database
207 for (let tabCollectionUuid of await this.objectStoreDB.keys()) {
208 if (!this.tabCollections.has(tabCollectionUuid)) {
209 console.log('removing data for stale tab collection',
210 tabCollectionUuid);
211 this.objectStoreDB.delete(tabCollectionUuid);
212 }
213 }
214
136 this.isInitialized = true; 215 this.isInitialized = true;
137 216
138 while (this.messageQueue.length > 0) { 217 while (this.messageQueue.length > 0) {
139 let [message, port] = this.messageQueue.pop(); 218 let [message, port] = this.messageQueue.pop();
140 if (this.ports.has(port)) { 219 if (this.ports.has(port)) {
143 } 222 }
144 } 223 }
145 224
146 async createTabThumbnail(tabId) { 225 async createTabThumbnail(tabId) {
147 let captureUrl = await browser.tabs.captureTab(tabId); 226 let captureUrl = await browser.tabs.captureTab(tabId);
148 let thumbnailUrl = await new Promise((resolve, reject) => { 227 let thumbnailBlob = await new Promise((resolve, reject) => {
149 let image = new Image(); 228 let image = new Image();
150 image.addEventListener('load', ev => { 229 image.addEventListener('load', ev => {
151 let canvas = document.createElement('canvas'); 230 let canvas = document.createElement('canvas');
152 canvas.width = THUMBNAIL_WIDTH; 231 canvas.width = THUMBNAIL_WIDTH;
153 canvas.height = THUMBNAIL_HEIGHT; 232 canvas.height = THUMBNAIL_HEIGHT;
157 let ctx = canvas.getContext('2d'); 236 let ctx = canvas.getContext('2d');
158 ctx.fillStyle = '#fff'; 237 ctx.fillStyle = '#fff';
159 ctx.fillRect(0, 0, canvas.width, canvas.height); 238 ctx.fillRect(0, 0, canvas.width, canvas.height);
160 ctx.drawImage(image, 0, 0, dWidth, dHeight); 239 ctx.drawImage(image, 0, 0, dWidth, dHeight);
161 240
162 resolve(canvas.toDataURL('image/jpeg', 0.75)); 241 canvas.toBlob(resolve, 'image/jpeg', 0.75);
163 }); 242 });
164 image.addEventListener('error', e => { 243 image.addEventListener('error', e => {
165 reject(e); 244 reject(e);
166 }); 245 });
167 image.src = captureUrl; 246 image.src = captureUrl;
168 }); 247 });
169 return thumbnailUrl; 248 return thumbnailBlob;
170 } 249 }
171 250
172 async createTabCollection(windowId) { 251 async createTabCollection(windowId) {
173 let browserTabs = await browser.tabs.query({ 252 let browserTabs = await browser.tabs.query({
174 windowId, 253 windowId,
186 SUPPORTED_PROTOCOLS.includes(new URL(browserTab.url).protocol)); 265 SUPPORTED_PROTOCOLS.includes(new URL(browserTab.url).protocol));
187 if (browserTabs.length === 0) { 266 if (browserTabs.length === 0) {
188 return; 267 return;
189 } 268 }
190 269
191 let tabs = browserTabs.map(browserTab => { 270 let tabs = await Promise.all(browserTabs.map(async browserTab => {
271 // convert favicon data URI to blob
272 let favIcon = null;
273 if (!browserTab.discarded) {
274 try {
275 let response = await fetch(browserTab.favIconUrl);
276 favIcon = await response.blob();
277 } catch (e) {
278 if (!(e instanceof AbortError)) {
279 throw e;
280 }
281 }
282 }
283
192 let tab = new Tab({ 284 let tab = new Tab({
193 url: browserTab.url, 285 url: browserTab.url,
194 title: browserTab.title, 286 title: browserTab.title,
195 favIconUrl: browserTab.favIconUrl 287 favIcon
196 }); 288 });
197 return [tab.uuid, tab]; 289 return [tab.uuid, tab];
198 }); 290 }));
199 291
200 // create empty tab which becomes the new active tab 292 // create empty tab which becomes the new active tab
201 await browser.tabs.create({active: true}); 293 await browser.tabs.create({active: true});
202 294
203 // capture tabs, return null for discarded tabs since they can only be 295 // capture tabs, return null for discarded tabs since they can only be
205 // interaction, and thus might hang the capture process indefinetly 297 // interaction, and thus might hang the capture process indefinetly
206 let thumbnails = await Promise.all(browserTabs.map(browserTab => 298 let thumbnails = await Promise.all(browserTabs.map(browserTab =>
207 !browserTab.discarded ? 299 !browserTab.discarded ?
208 this.createTabThumbnail(browserTab.id) : null)); 300 this.createTabThumbnail(browserTab.id) : null));
209 for (let [, tab] of tabs) { 301 for (let [, tab] of tabs) {
210 tab.thumbnailUrl = thumbnails.shift(); 302 tab.thumbnail = thumbnails.shift();
211 } 303 }
212 304
213 let tabCollection = new TabCollection({tabs}); 305 let tabCollection = new TabCollection({tabs});
214 console.log('created tab collection:', tabCollection); 306 console.log('created tab collection:', tabCollection);
307
308 // store tab favicons and thumbnails
309 let tabCollectionData = {
310 uuid: tabCollection.uuid,
311 tabs: new Map()
312 };
313 for (let [uuid, tab] of tabs) {
314 tabCollectionData.tabs.set(uuid, {
315 favIcon: tab.favIcon,
316 thumbnail: tab.thumbnail
317 });
318 }
319 await this.objectStoreDB.set(tabCollectionData.uuid, tabCollectionData);
215 320
216 // store tab collection 321 // store tab collection
217 console.log('storing tab collection:', tabCollection); 322 console.log('storing tab collection:', tabCollection);
218 await browser.storage.sync.set({ 323 await browser.storage.sync.set({
219 [`collection:${tabCollection.uuid}`]: tabCollection.serialize() 324 [`collection:${tabCollection.uuid}`]: tabCollection.serialize()
256 } 361 }
257 362
258 async removeTabCollection(tabCollectionUuid) { 363 async removeTabCollection(tabCollectionUuid) {
259 console.log('removing tab collection %s', tabCollectionUuid); 364 console.log('removing tab collection %s', tabCollectionUuid);
260 await browser.storage.sync.remove(`collection:${tabCollectionUuid}`); 365 await browser.storage.sync.remove(`collection:${tabCollectionUuid}`);
366 this.objectStoreDB.delete(tabCollectionUuid);
261 } 367 }
262 368
263 async restoreTabCollection(tabCollectionUuid, windowId) { 369 async restoreTabCollection(tabCollectionUuid, windowId) {
264 console.log('restoring tab collection %s in window %s', 370 console.log('restoring tab collection %s in window %s',
265 tabCollectionUuid, windowId); 371 tabCollectionUuid, windowId);
277 }, tabProperties))); 383 }, tabProperties)));
278 await Promise.all(creatingTabs); 384 await Promise.all(creatingTabs);
279 await this.removeTabCollection(tabCollectionUuid); 385 await this.removeTabCollection(tabCollectionUuid);
280 } 386 }
281 387
282 onStorageChanged(changes, areaName) { 388 async updateTabData(tabCollectionUuid) {
389 let tabCollectionDataObject;
390 try {
391 tabCollectionDataObject =
392 await this.objectStoreDB.get(tabCollectionUuid);
393 } catch (e) {
394 console.error(`Failed to get data from database: e.message`);
395 return;
396 }
397 if (typeof tabCollectionDataObject === 'undefined') {
398 // does not exist in database
399 console.log('no data stored for tab collection', tabCollectionUuid);
400 return;
401 }
402
403 console.log(`updating tab collection ${tabCollectionUuid} with data`,
404 tabCollectionDataObject);
405 let tabCollection = this.tabCollections.get(tabCollectionUuid);
406 for (let [tabUuid, tab] of tabCollection.tabs) {
407 let tabDataObject = tabCollectionDataObject.tabs.get(tabUuid);
408 if (typeof tabDataObject === 'undefined') {
409 continue;
410 }
411 tab.favIcon = tabDataObject.favIcon;
412 tab.thumbnail = tabDataObject.thumbnail;
413 }
414 }
415
416 async onStorageChanged(changes, areaName) {
283 if (areaName !== 'sync') { 417 if (areaName !== 'sync') {
284 return; 418 return;
285 } 419 }
286 420
287 console.group('sync storage area changed:', changes); 421 console.group('sync storage area changed:', changes);
296 let tabCollectionUuid = key.replace('collection:', ''); 430 let tabCollectionUuid = key.replace('collection:', '');
297 if (typeof oldValue === 'undefined') { 431 if (typeof oldValue === 'undefined') {
298 // a new collection was created 432 // a new collection was created
299 let newTabCollection = TabCollection.deserialize(newValue); 433 let newTabCollection = TabCollection.deserialize(newValue);
300 this.tabCollections.set(tabCollectionUuid, newTabCollection); 434 this.tabCollections.set(tabCollectionUuid, newTabCollection);
435 // try to get tab favicons and thumbnails
436 await this.updateTabData(tabCollectionUuid);
301 437
302 this.broadcastMessage({ 438 this.broadcastMessage({
303 type: 'tabCollectionCreated', 439 type: 'tabCollectionCreated',
304 tabCollection: newTabCollection 440 tabCollection: newTabCollection
305 }); 441 });
306 } else if (typeof newValue === 'undefined') { 442 } else if (typeof newValue === 'undefined') {
307 // a collection has been removed 443 // a collection has been removed
308 this.tabCollections.delete(tabCollectionUuid); 444 this.tabCollections.delete(tabCollectionUuid);
445 this.objectStoreDB.delete(tabCollectionUuid);
309 446
310 this.broadcastMessage({ 447 this.broadcastMessage({
311 type: 'tabCollectionRemoved', 448 type: 'tabCollectionRemoved',
312 tabCollectionUuid 449 tabCollectionUuid
313 }); 450 });
314 } else { 451 } else {
315 // a collection has changed 452 // a collection has changed
316 let newTabCollection = TabCollection.deserialize(newValue); 453 let newTabCollection = TabCollection.deserialize(newValue);
317 this.tabCollections.set(tabCollectionUuid, newTabCollection); 454 this.tabCollections.set(tabCollectionUuid, newTabCollection);
455 // try to get tab favicons and thumbnails
456 await this.updateTabData(tabCollectionUuid);
318 457
319 this.broadcastMessage({ 458 this.broadcastMessage({
320 type: 'tabCollectionChanged', 459 type: 'tabCollectionChanged',
321 tabCollection: newTabCollection 460 tabCollection: newTabCollection
322 }); 461 });