blob: b59460dd11eeaa7457edd05c69cc95e1bc5e26b4 [file] [log] [blame]
Adrià Vilanova Martínez27c69962021-07-17 23:32:51 +02001import {openDB} from 'idb';
2
3const dbName = 'TWPTAvatarsDB';
4const threadListLoadEvent = 'TWPT_ViewForumResponse';
5// Time after the last use when a cache entry should be deleted (in s):
6const expirationTime = 4 * 24 * 60 * 60; // 4 days
7// Probability of running the piece of code to remove unused cache entries after
8// loading the thread list.
9const probRemoveUnusedCacheEntries = 0.10; // 10%
10
11export default class AvatarsDB {
12 constructor() {
13 this.dbPromise = undefined;
14 this.openDB();
15 this.setUpInvalidationsHandler();
16 }
17
18 openDB() {
19 if (this.dbPromise === undefined)
20 this.dbPromise = openDB(dbName, 1, {
21 upgrade: (udb, oldVersion, newVersion, transaction) => {
22 switch (oldVersion) {
23 case 0:
24 var cache = udb.createObjectStore('avatarsCache', {
25 keyPath: 'threadId',
26 });
27 cache.createIndex(
28 'lastUsedTimestamp', 'lastUsedTimestamp', {unique: false});
29
30 var unauthedForums = udb.createObjectStore('unauthorizedForums', {
31 keyPath: 'forumId',
32 });
33 unauthedForums.createIndex(
34 'expirationTimestamp', 'expirationTimestamp',
35 {unique: false});
36 }
37 },
38 });
39 }
40
41 getCacheEntry(threadId) {
42 return this.dbPromise.then(db => db.get('avatarsCache', threadId));
43 }
44
45 putCacheEntry(entry) {
46 return this.dbPromise.then(db => db.put('avatarsCache', entry));
47 }
48
49 invalidateCacheEntryIfExists(threadId) {
50 return this.dbPromise.then(db => db.delete('avatarsCache', threadId));
51 }
52
53 removeUnusedCacheEntries() {
54 console.debug('[threadListAvatars] Removing unused cache entries...');
55 return this.dbPromise
56 .then(db => {
57 var upperBoundTimestamp =
58 Math.floor(Date.now() / 1000) - expirationTime;
59 var range = IDBKeyRange.upperBound(upperBoundTimestamp);
60
61 var tx = db.transaction('avatarsCache', 'readwrite');
62 var index = tx.store.index('lastUsedTimestamp');
63 return index.openCursor(range);
64 })
65 .then(function iterateCursor(cursor) {
66 if (!cursor) return;
67 cursor.delete();
68 return cursor.continue().then(iterateCursor);
69 });
70 }
71
72 setUpInvalidationsHandler() {
73 window.addEventListener(
74 threadListLoadEvent, e => this.handleInvalidations(e));
75 }
76
77 handleInvalidations(e) {
78 var response = e?.detail?.body;
79 var threads = response?.['1']?.['2'];
80 if (threads === undefined) {
81 console.warn(
82 '[threadListAvatars] The thread list doesn\'t contain any threads.');
83 return;
84 }
85
86 var promises = [];
87 threads.forEach(t => {
88 var id = t?.['2']?.['1']?.['1'];
89 var currentUpdatedTimestamp =
90 Math.floor(Number.parseInt(t?.['2']?.['1']?.['4']) / 1000000);
91 var currentLastMessageId = t?.['2']?.['10'];
92
93 if (id === undefined || currentUpdatedTimestamp === undefined ||
94 currentLastMessageId === undefined)
95 return;
96
97 promises.push(this.getCacheEntry(id).then(entry => {
98 if (entry === undefined) return;
99
100 // If the cache entry is still valid.
101 if (currentLastMessageId == entry.lastMessageId ||
102 currentUpdatedTimestamp <= entry.updatedTimestamp) {
103 entry.lastUsedTimestamp = Math.floor(Date.now() / 1000);
104 return this.putCacheEntry(entry).catch(err => {
105 console.error(
106 '[threadListAvatars] Error while updating lastUsedTimestamp from thread in cache:',
107 err);
108 });
109 }
110
111 console.debug(
112 '[threadListAvatars] Invalidating thread', entry.threadId);
113 return this.invalidateCacheEntryIfExists(entry.threadId).catch(err => {
114 console.error(
115 '[threadListAvatars] Error while invalidating thread from cache:',
116 err);
117 });
118 }));
119 });
120
121 Promise.allSettled(promises).then(() => {
122 if (Math.random() < probRemoveUnusedCacheEntries)
123 this.removeUnusedCacheEntries().catch(err => {
124 console.error(
125 '[threadListAvatars] Error while removing unused cache entries:',
126 err);
127 });
128 });
129 }
130};