Skip to content

Commit 551e626

Browse files
committed
Use IndexedDB to persist tree cache across sessions #3434
1 parent 4dc1b12 commit 551e626

File tree

3 files changed

+365
-53
lines changed

3 files changed

+365
-53
lines changed

webextensions/background/background-cache.js

+44-25
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
configs
1414
} from '/common/common.js';
1515
import * as ApiTabs from '/common/api-tabs.js';
16+
import * as CacheStorage from '/common/cache-storage.js';
1617
import * as Constants from '/common/constants.js';
1718
import * as MetricsData from '/common/metrics-data.js';
1819
import * as TabsInternalOperation from '/common/tabs-internal-operation.js';
@@ -38,6 +39,7 @@ export function activate() {
3839
configs.$addObserver(onConfigChange);
3940

4041
if (!configs.persistCachedTree) {
42+
// clear obsolete cache
4143
browser.windows.getAll().then(windows => {
4244
for (const win of windows) {
4345
browser.sessions.removeWindowValue(win.id, Constants.kWINDOW_STATE_CACHED_TABS).catch(ApiTabs.createErrorSuppressor());
@@ -299,31 +301,32 @@ async function updateWindowCache(owner, key, value) {
299301
if (!owner)
300302
return;
301303

302-
if (!configs.persistCachedTree) {
303-
const storageKey = `backgroundCache-${await UniqueId.ensureWindowId(owner.windowId)}-${key}`;
304-
if (value)
305-
mCaches[storageKey] = value;
306-
else
307-
delete mCaches[storageKey];
308-
return;
309-
}
310-
311-
if (value === undefined) {
304+
if (configs.persistCachedTree) {
312305
try {
313-
return browser.sessions.removeWindowValue(owner.windowId, key).catch(ApiTabs.createErrorSuppressor());
314-
}
315-
catch(e) {
316-
console.log(new Error('fatal error: failed to delete window cache'), e, owner, key, value);
317-
}
318-
}
319-
else {
320-
try {
321-
return browser.sessions.setWindowValue(owner.windowId, key, value).catch(ApiTabs.createErrorSuppressor());
306+
if (value)
307+
await CacheStorage.setValue({
308+
windowId: owner.windowId,
309+
key,
310+
value,
311+
store: CacheStorage.BACKGROUND,
312+
});
313+
else
314+
await CacheStorage.deleteValue({
315+
windowId: owner.windowId,
316+
key,
317+
store: CacheStorage.BACKGROUND,
318+
});
319+
return;
322320
}
323-
catch(e) {
324-
console.log(new Error('fatal error: failed to update window cache'), e, owner, key, value);
321+
catch(_error) {
325322
}
326323
}
324+
325+
const storageKey = `backgroundCache-${await UniqueId.ensureWindowId(owner.windowId)}-${key}`;
326+
if (value)
327+
mCaches[storageKey] = value;
328+
else
329+
delete mCaches[storageKey];
327330
}
328331

329332
export function markWindowCacheDirtyFromTab(tab, akey) {
@@ -339,11 +342,21 @@ export function markWindowCacheDirtyFromTab(tab, akey) {
339342
}
340343

341344
async function getWindowCache(owner, key) {
342-
if (!configs.persistCachedTree) {
343-
const storageKey = `backgroundCache-${await UniqueId.ensureWindowId(owner.windowId)}-${key}`;
344-
return mCaches[storageKey];
345+
if (configs.persistCachedTree) {
346+
try {
347+
const value = await CacheStorage.getValue({
348+
windowId: owner.windowId,
349+
key,
350+
store: CacheStorage.BACKGROUND,
351+
});
352+
return value;
353+
}
354+
catch(_error) {
355+
}
345356
}
346-
return browser.sessions.getWindowValue(owner.windowId, key).catch(ApiTabs.createErrorHandler());
357+
358+
const storageKey = `backgroundCache-${await UniqueId.ensureWindowId(owner.windowId)}-${key}`;
359+
return mCaches[storageKey];
347360
}
348361

349362
function getWindowCacheOwner(windowId) {
@@ -535,6 +548,12 @@ browser.runtime.onMessage.addListener((message, _sender) => {
535548
});
536549

537550
browser.windows.onRemoved.addListener(async windowId => {
551+
try {
552+
CacheStorage.clearForWindow(windowId);
553+
}
554+
catch(_error) {
555+
}
556+
538557
const storageKeyPart = `Cache-${await UniqueId.ensureWindowId(windowId)}-`;
539558
for (const key in mCaches) {
540559
if (key.includes(storageKeyPart))

webextensions/common/cache-storage.js

+279
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,279 @@
1+
/*
2+
# This Source Code Form is subject to the terms of the Mozilla Public
3+
# License, v. 2.0. If a copy of the MPL was not distributed with this
4+
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
5+
*/
6+
'use strict';
7+
8+
import * as UniqueId from '/common/unique-id.js';
9+
10+
const DB_NAME = 'PermanentStorage';
11+
const DB_VERSION = 2;
12+
const EXPIRATION_TIME_IN_MSEC = 7 * 24 * 60 * 60 * 1000; // 7 days
13+
14+
export const BACKGROUND = 'backgroundCaches';
15+
export const SIDEBAR = 'sidebarCaches';
16+
17+
let mOpenedDB;
18+
19+
async function openDB() {
20+
if (mOpenedDB)
21+
return mOpenedDB;
22+
return new Promise((resolve, _reject) => {
23+
const request = indexedDB.open(DB_NAME, DB_VERSION);
24+
25+
request.onerror = () => {
26+
// This can fail if this is in a private window.
27+
// See: https://github.com/piroor/treestyletab/issues/3387
28+
//reject(new Error('Failed to open database'));
29+
resolve(null);
30+
};
31+
32+
request.onsuccess = () => {
33+
const db = request.result;
34+
mOpenedDB = db;
35+
resolve(db);
36+
};
37+
38+
request.onupgradeneeded = (event) => {
39+
const db = event.target.result;
40+
if (event.oldVersion < DB_VERSION) {
41+
try {
42+
db.deleteObjectStore(BACKGROUND);
43+
db.deleteObjectStore(SIDEBAR);
44+
}
45+
catch(_error) {
46+
}
47+
48+
const backgroundCachesStore = db.createObjectStore(BACKGROUND, { keyPath: 'key', unique: true });
49+
const sidebarCachesStore = db.createObjectStore(SIDEBAR, { keyPath: 'key', unique: true });
50+
51+
backgroundCachesStore.createIndex('windowId', 'windowId', { unique: false });
52+
sidebarCachesStore.createIndex('windowId', 'windowId', { unique: false });
53+
54+
backgroundCachesStore.createIndex('timestamp', 'timestamp');
55+
sidebarCachesStore.createIndex('timestamp', 'timestamp');
56+
}
57+
};
58+
});
59+
}
60+
61+
export async function setValue({ windowId, key, value, store } = {}) {
62+
const [db, windowUniqueId] = await Promise.all([
63+
openDB(),
64+
UniqueId.ensureWindowId(windowId),
65+
]);
66+
if (!db)
67+
return;
68+
69+
reserveToExpireOldEntries();
70+
71+
const cacheKey = `${windowUniqueId}-${key}`;
72+
const timestamp = Date.now();
73+
try {
74+
const transaction = db.transaction([store], 'readwrite');
75+
const cacheStore = transaction.objectStore(store);
76+
77+
cacheStore.put({
78+
key: cacheKey,
79+
windowId: windowUniqueId,
80+
value,
81+
timestamp,
82+
});
83+
84+
transaction.oncomplete = () => {
85+
//db.close();
86+
windowId = undefined;
87+
key = undefined;
88+
value = undefined;
89+
store = undefined;
90+
};
91+
}
92+
catch(error) {
93+
console.error(`Failed to store cache ${cacheKey} in the store ${store}`, error);
94+
}
95+
}
96+
97+
export async function deleteValue({ windowId, key, store } = {}) {
98+
const [db, windowUniqueId] = await Promise.all([
99+
openDB(),
100+
UniqueId.ensureWindowId(windowId),
101+
]);
102+
if (!db)
103+
return;
104+
105+
reserveToExpireOldEntries();
106+
107+
const cacheKey = `${windowUniqueId}-${key}`;
108+
try {
109+
const transaction = db.transaction([store], 'readwrite');
110+
const cacheStore = transaction.objectStore(store);
111+
cacheStore.delete(cacheKey);
112+
transaction.oncomplete = () => {
113+
//db.close();
114+
windowId = undefined;
115+
key = undefined;
116+
store = undefined;
117+
};
118+
}
119+
catch(error) {
120+
console.error(`Failed to delete cache ${cacheKey} in the store ${store}`, error);
121+
}
122+
}
123+
124+
export async function getValue({ windowId, key, store } = {}) {
125+
return new Promise(async (resolve, _reject) => {
126+
const [db, windowUniqueId] = await Promise.all([
127+
openDB(),
128+
UniqueId.ensureWindowId(windowId),
129+
]);
130+
if (!db) {
131+
resolve(null);
132+
return;
133+
}
134+
135+
const cacheKey = `${windowUniqueId}-${key}`;
136+
const timestamp = Date.now();
137+
try {
138+
const transaction = db.transaction([store], 'readwrite');
139+
const cacheStore = transaction.objectStore(store);
140+
141+
const cacheRequest = cacheStore.get(cacheKey);
142+
143+
cacheRequest.onsuccess = () => {
144+
const cache = cacheRequest.result;
145+
if (!cache) {
146+
resolve(null);
147+
return;
148+
}
149+
cacheStore.put({ key: cacheKey, timestamp });
150+
resolve(cache.value);
151+
cache.key = undefined;
152+
cache.windowId = undefined;
153+
cache.value = undefined;
154+
};
155+
156+
transaction.oncomplete = () => {
157+
//db.close();
158+
windowId = undefined;
159+
key = undefined;
160+
store = undefined;
161+
};
162+
}
163+
catch(error) {
164+
console.error('Failed to get from cache:', error);
165+
resolve(null);
166+
}
167+
});
168+
}
169+
170+
export async function clearForWindow(windowId) {
171+
reserveToExpireOldEntries();
172+
return new Promise(async (resolve, reject) => {
173+
const [db, windowUniqueId] = await Promise.all([
174+
openDB(),
175+
UniqueId.ensureWindowId(windowId),
176+
]);
177+
if (!db) {
178+
resolve(null);
179+
return;
180+
}
181+
182+
try {
183+
const transaction = db.transaction([BACKGROUND, SIDEBAR], 'readwrite');
184+
const backgroundCacheStore = transaction.objectStore(BACKGROUND);
185+
const sidebarCacheStore = transaction.objectStore(SIDEBAR);
186+
187+
const backgroundCacheIndex = backgroundCacheStore.index('windowId');
188+
const sidebarCacheIndex = sidebarCacheStore.index('windowId');
189+
190+
const backgroundCacheRequest = backgroundCacheIndex.openCursor(IDBKeyRange.only(windowUniqueId));
191+
backgroundCacheRequest.onsuccess = (event) => {
192+
const cursor = event.target.result;
193+
if (!cursor)
194+
return;
195+
const key = cursor.primaryKey;
196+
cursor.continue();
197+
backgroundCacheStore.delete(key);
198+
};
199+
200+
const sidebarCacheRequest = sidebarCacheIndex.openCursor(IDBKeyRange.only(windowUniqueId));
201+
sidebarCacheRequest.onsuccess = (event) => {
202+
const cursor = event.target.result;
203+
if (!cursor)
204+
return;
205+
const key = cursor.primaryKey;
206+
cursor.continue();
207+
sidebarCacheStore.delete(key);
208+
};
209+
210+
transaction.oncomplete = () => {
211+
//db.close();
212+
resolve();
213+
};
214+
}
215+
catch(error) {
216+
console.error('Failed to clear caches:', error);
217+
reject(error);
218+
}
219+
});
220+
}
221+
222+
async function reserveToExpireOldEntries() {
223+
if (reserveToExpireOldEntries.reservedExpiration)
224+
clearTimeout(reserveToExpireOldEntries.reservedExpiration);
225+
reserveToExpireOldEntries.reservedExpiration = setTimeout(() => {
226+
reserveToExpireOldEntries.reservedExpiration = null;
227+
expireOldEntries();
228+
}, 500);
229+
}
230+
231+
async function expireOldEntries() {
232+
return new Promise(async (resolve, reject) => {
233+
const db = await openDB();
234+
if (!db) {
235+
resolve();
236+
return;
237+
}
238+
239+
try {
240+
const transaction = db.transaction([BACKGROUND, SIDEBAR], 'readwrite');
241+
const backgroundCacheStore = transaction.objectStore(BACKGROUND);
242+
const sidebarCacheStore = transaction.objectStore(SIDEBAR);
243+
244+
const backgroundCacheIndex = backgroundCacheStore.index('timestamp');
245+
const sidebarCacheIndex = sidebarCacheStore.index('timestamp');
246+
247+
const expirationTimestamp = Date.now() - EXPIRATION_TIME_IN_MSEC;
248+
249+
const backgroundCacheRequest = backgroundCacheIndex.openCursor(IDBKeyRange.upperBound(expirationTimestamp));
250+
backgroundCacheRequest.onsuccess = (event) => {
251+
const cursor = event.target.result;
252+
if (!cursor)
253+
return;
254+
const key = cursor.primaryKey;
255+
cursor.continue();
256+
backgroundCacheStore.delete(key);
257+
};
258+
259+
const sidebarCacheRequest = sidebarCacheIndex.openCursor(IDBKeyRange.upperBound(expirationTimestamp));
260+
sidebarCacheRequest.onsuccess = (event) => {
261+
const cursor = event.target.result;
262+
if (!cursor)
263+
return;
264+
const key = cursor.primaryKey;
265+
cursor.continue();
266+
sidebarCacheStore.delete(key);
267+
};
268+
269+
transaction.oncomplete = () => {
270+
//db.close();
271+
resolve();
272+
};
273+
}
274+
catch(error) {
275+
console.error('Failed to expire old entries:', error);
276+
reject(error);
277+
}
278+
});
279+
}

0 commit comments

Comments
 (0)