blob: 05149b56c2219d5d6e9fecacab6a6442c403ea2c [file] [log] [blame]
import {openDB} from 'idb';
const dbName = 'TWPTAvatarsDB';
const threadListLoadEvent = 'TWPT_ViewForumResponse';
const createMessageLoadEvent = 'TWPT_CreateMessageRequest';
// Time after the last use when a cache entry should be deleted (in s):
const cacheExpirationTime = 4 * 24 * 60 * 60; // 4 days
// Probability of running the piece of code to remove unused cache entries after
// loading the thread list.
const probRemoveUnusedCacheEntries = 0.10; // 10%
// Time after which an unauthorized forum entry expires (in s).
const unauthorizedForumExpirationTime = 1 * 24 * 60 * 60; // 1 day
export default class AvatarsDB {
constructor() {
this.dbPromise = undefined;
this.openDB();
this.setUpInvalidationsHandlers();
}
openDB() {
if (this.dbPromise === undefined)
this.dbPromise = openDB(dbName, 1, {
upgrade: (udb, oldVersion, newVersion, transaction) => {
switch (oldVersion) {
case 0:
var cache = udb.createObjectStore('avatarsCache', {
keyPath: 'threadId',
});
cache.createIndex(
'lastUsedTimestamp', 'lastUsedTimestamp', {unique: false});
var unauthedForums = udb.createObjectStore('unauthorizedForums', {
keyPath: 'forumId',
});
unauthedForums.createIndex(
'expirationTimestamp', 'expirationTimestamp',
{unique: false});
}
},
});
}
// avatarsCache methods:
getCacheEntry(threadId) {
return this.dbPromise.then(db => db.get('avatarsCache', threadId));
}
putCacheEntry(entry) {
return this.dbPromise.then(db => db.put('avatarsCache', entry));
}
invalidateCacheEntryIfExists(threadId) {
return this.dbPromise.then(db => db.delete('avatarsCache', threadId));
}
removeUnusedCacheEntries() {
console.debug('[threadListAvatars] Removing unused cache entries...');
return this.dbPromise
.then(db => {
var upperBoundTimestamp =
Math.floor(Date.now() / 1000) - cacheExpirationTime;
var range = IDBKeyRange.upperBound(upperBoundTimestamp);
var tx = db.transaction('avatarsCache', 'readwrite');
var index = tx.store.index('lastUsedTimestamp');
return index.openCursor(range);
})
.then(function iterateCursor(cursor) {
if (!cursor) return;
cursor.delete();
return cursor.continue().then(iterateCursor);
});
}
setUpInvalidationsHandlers() {
window.addEventListener(
threadListLoadEvent, e => this.handleInvalidationsByListLoad(e));
window.addEventListener(
createMessageLoadEvent, e => this.handleInvalidationByNewMessage(e));
}
handleInvalidationsByListLoad(e) {
var response = e?.detail?.body;
var threads = response?.['1']?.['2'];
if (threads === undefined) {
console.warn(
'[threadListAvatars] The thread list doesn\'t contain any threads.');
return;
}
var promises = [];
threads.forEach(t => {
var id = t?.['2']?.['1']?.['1'];
var currentLastMessageId = t?.['2']?.['10'];
if (id === undefined) return;
promises.push(this.getCacheEntry(id).then(entry => {
if (entry === undefined) return;
// If the cache entry is still valid.
if (currentLastMessageId !== undefined &&
currentLastMessageId == entry.lastMessageId) {
entry.lastUsedTimestamp = Math.floor(Date.now() / 1000);
return this.putCacheEntry(entry).catch(err => {
console.error(
'[threadListAvatars] Error while updating lastUsedTimestamp from thread in cache:',
err);
});
}
console.debug(
'[threadListAvatars] Invalidating thread', entry.threadId);
return this.invalidateCacheEntryIfExists(entry.threadId).catch(err => {
console.error(
'[threadListAvatars] Error while invalidating thread from cache:',
err);
});
}));
});
Promise.allSettled(promises).then(() => {
if (Math.random() < probRemoveUnusedCacheEntries)
this.removeUnusedCacheEntries().catch(err => {
console.error(
'[threadListAvatars] Error while removing unused cache entries:',
err);
});
});
}
handleInvalidationByNewMessage(e) {
var request = e?.detail?.body;
var threadId = request?.['2'];
if (threadId === undefined) {
console.warn(
'[threadListAvatars] Thread ID couldn\'t be parsed from the CreateMessage request.');
return;
}
console.debug(
'[threadListAvatars] Invalidating thread', threadId,
'due to intercepting a CreateMessage request for that thread.');
return this.invalidateCacheEntryIfExists(threadId);
}
// unauthorizedForums methods:
isForumUnauthorized(forumId) {
return this.dbPromise.then(db => db.get('unauthorizedForums', forumId))
.then(entry => {
if (entry === undefined) return false;
var now = Math.floor(Date.now() / 1000);
if (entry.expirationTimestamp > now) return true;
this.invalidateUnauthorizedForum(forumId);
return false;
});
}
putUnauthorizedForum(forumId) {
return this.dbPromise.then(db => db.put('unauthorizedForums', {
forumId,
expirationTimestamp:
Math.floor(Date.now() / 1000) + unauthorizedForumExpirationTime,
}));
}
invalidateUnauthorizedForum(forumId) {
return this.dbPromise.then(db => db.delete('unauthorizedForums', forumId));
}
};