blob: f1b09e9aa5f850d555f88e426fb03ed47e9b39ee [file] [log] [blame]
Adrià Vilanova Martínez27c69962021-07-17 23:32:51 +02001import {openDB} from 'idb';
2
3const dbName = 'TWPTAvatarsDB';
4const threadListLoadEvent = 'TWPT_ViewForumResponse';
avm99963afda2372021-08-08 20:54:05 +02005const createMessageLoadEvent = 'TWPT_CreateMessageRequest';
Adrià Vilanova Martínez27c69962021-07-17 23:32:51 +02006// Time after the last use when a cache entry should be deleted (in s):
Adrià Vilanova Martínezc41edf42021-07-18 02:06:55 +02007const cacheExpirationTime = 4 * 24 * 60 * 60; // 4 days
Adrià Vilanova Martínez27c69962021-07-17 23:32:51 +02008// Probability of running the piece of code to remove unused cache entries after
9// loading the thread list.
10const probRemoveUnusedCacheEntries = 0.10; // 10%
11
Adrià Vilanova Martínezc41edf42021-07-18 02:06:55 +020012// Time after which an unauthorized forum entry expires (in s).
13const unauthorizedForumExpirationTime = 1 * 24 * 60 * 60; // 1 day
14
Adrià Vilanova Martínez27c69962021-07-17 23:32:51 +020015export default class AvatarsDB {
16 constructor() {
17 this.dbPromise = undefined;
18 this.openDB();
avm99963afda2372021-08-08 20:54:05 +020019 this.setUpInvalidationsHandlers();
Adrià Vilanova Martínez27c69962021-07-17 23:32:51 +020020 }
21
22 openDB() {
23 if (this.dbPromise === undefined)
24 this.dbPromise = openDB(dbName, 1, {
25 upgrade: (udb, oldVersion, newVersion, transaction) => {
26 switch (oldVersion) {
27 case 0:
28 var cache = udb.createObjectStore('avatarsCache', {
29 keyPath: 'threadId',
30 });
31 cache.createIndex(
32 'lastUsedTimestamp', 'lastUsedTimestamp', {unique: false});
33
34 var unauthedForums = udb.createObjectStore('unauthorizedForums', {
35 keyPath: 'forumId',
36 });
37 unauthedForums.createIndex(
38 'expirationTimestamp', 'expirationTimestamp',
39 {unique: false});
40 }
41 },
42 });
43 }
44
Adrià Vilanova Martínezc41edf42021-07-18 02:06:55 +020045 // avatarsCache methods:
Adrià Vilanova Martínez27c69962021-07-17 23:32:51 +020046 getCacheEntry(threadId) {
47 return this.dbPromise.then(db => db.get('avatarsCache', threadId));
48 }
49
50 putCacheEntry(entry) {
51 return this.dbPromise.then(db => db.put('avatarsCache', entry));
52 }
53
54 invalidateCacheEntryIfExists(threadId) {
55 return this.dbPromise.then(db => db.delete('avatarsCache', threadId));
56 }
57
58 removeUnusedCacheEntries() {
59 console.debug('[threadListAvatars] Removing unused cache entries...');
60 return this.dbPromise
61 .then(db => {
62 var upperBoundTimestamp =
Adrià Vilanova Martínezc41edf42021-07-18 02:06:55 +020063 Math.floor(Date.now() / 1000) - cacheExpirationTime;
Adrià Vilanova Martínez27c69962021-07-17 23:32:51 +020064 var range = IDBKeyRange.upperBound(upperBoundTimestamp);
65
66 var tx = db.transaction('avatarsCache', 'readwrite');
67 var index = tx.store.index('lastUsedTimestamp');
68 return index.openCursor(range);
69 })
70 .then(function iterateCursor(cursor) {
71 if (!cursor) return;
72 cursor.delete();
73 return cursor.continue().then(iterateCursor);
74 });
75 }
76
avm99963afda2372021-08-08 20:54:05 +020077 setUpInvalidationsHandlers() {
Adrià Vilanova Martínez31a66162021-08-16 10:31:23 +020078 window.addEventListener(threadListLoadEvent, e => {
79 // Ignore ViewForum requests made by the chat feature and the "Mark as
80 // duplicate" dialog.
81 //
82 // All those requests have |maxNum| set to 10 and 20 respectively, while
83 // the requests that we want to handle are the ones to initially load the
84 // thread list (which currently requests 100 threads) and the ones to load
85 // more threads (which request 50 threads).
86 var maxNum = e.detail.body?.['2']?.['1']?.['2'];
87 if (maxNum == 10 || maxNum == 20) return;
88
89 this.handleInvalidationsByListLoad(e);
90 });
avm99963afda2372021-08-08 20:54:05 +020091 window.addEventListener(
92 createMessageLoadEvent, e => this.handleInvalidationByNewMessage(e));
Adrià Vilanova Martínez27c69962021-07-17 23:32:51 +020093 }
94
avm99963afda2372021-08-08 20:54:05 +020095 handleInvalidationsByListLoad(e) {
Adrià Vilanova Martínez27c69962021-07-17 23:32:51 +020096 var response = e?.detail?.body;
97 var threads = response?.['1']?.['2'];
98 if (threads === undefined) {
99 console.warn(
100 '[threadListAvatars] The thread list doesn\'t contain any threads.');
101 return;
102 }
103
104 var promises = [];
105 threads.forEach(t => {
106 var id = t?.['2']?.['1']?.['1'];
Adrià Vilanova Martínez27c69962021-07-17 23:32:51 +0200107 var currentLastMessageId = t?.['2']?.['10'];
108
Adrià Vilanova Martínezd5615032021-07-22 22:19:37 +0200109 if (id === undefined) return;
Adrià Vilanova Martínez27c69962021-07-17 23:32:51 +0200110
111 promises.push(this.getCacheEntry(id).then(entry => {
112 if (entry === undefined) return;
113
114 // If the cache entry is still valid.
Adrià Vilanova Martínezd5615032021-07-22 22:19:37 +0200115 if (currentLastMessageId !== undefined &&
116 currentLastMessageId == entry.lastMessageId) {
Adrià Vilanova Martínez27c69962021-07-17 23:32:51 +0200117 entry.lastUsedTimestamp = Math.floor(Date.now() / 1000);
118 return this.putCacheEntry(entry).catch(err => {
119 console.error(
120 '[threadListAvatars] Error while updating lastUsedTimestamp from thread in cache:',
121 err);
122 });
123 }
124
125 console.debug(
126 '[threadListAvatars] Invalidating thread', entry.threadId);
127 return this.invalidateCacheEntryIfExists(entry.threadId).catch(err => {
128 console.error(
129 '[threadListAvatars] Error while invalidating thread from cache:',
130 err);
131 });
132 }));
133 });
134
135 Promise.allSettled(promises).then(() => {
136 if (Math.random() < probRemoveUnusedCacheEntries)
137 this.removeUnusedCacheEntries().catch(err => {
138 console.error(
139 '[threadListAvatars] Error while removing unused cache entries:',
140 err);
141 });
142 });
143 }
Adrià Vilanova Martínezc41edf42021-07-18 02:06:55 +0200144
avm99963afda2372021-08-08 20:54:05 +0200145 handleInvalidationByNewMessage(e) {
146 var request = e?.detail?.body;
147 var threadId = request?.['2'];
148 if (threadId === undefined) {
149 console.warn(
150 '[threadListAvatars] Thread ID couldn\'t be parsed from the CreateMessage request.');
151 return;
152 }
153
154 console.debug(
155 '[threadListAvatars] Invalidating thread', threadId,
156 'due to intercepting a CreateMessage request for that thread.');
157 return this.invalidateCacheEntryIfExists(threadId);
158 }
159
Adrià Vilanova Martínezc41edf42021-07-18 02:06:55 +0200160 // unauthorizedForums methods:
161 isForumUnauthorized(forumId) {
162 return this.dbPromise.then(db => db.get('unauthorizedForums', forumId))
163 .then(entry => {
164 if (entry === undefined) return false;
165
166 var now = Math.floor(Date.now() / 1000);
167 if (entry.expirationTimestamp > now) return true;
168
169 this.invalidateUnauthorizedForum(forumId);
170 return false;
171 });
172 }
173
174 putUnauthorizedForum(forumId) {
175 return this.dbPromise.then(db => db.put('unauthorizedForums', {
176 forumId,
177 expirationTimestamp:
178 Math.floor(Date.now() / 1000) + unauthorizedForumExpirationTime,
179 }));
180 }
181
182 invalidateUnauthorizedForum(forumId) {
183 return this.dbPromise.then(db => db.delete('unauthorizedForums', forumId));
184 }
Adrià Vilanova Martínez27c69962021-07-17 23:32:51 +0200185};