blob: 311fffffaf959642e8850e1742c6f4964286b756 [file] [log] [blame]
Adrià Vilanova Martínez27c69962021-07-17 23:32:51 +02001import {openDB} from 'idb';
2
3const dbName = 'TWPTAvatarsDB';
Adrià Vilanova Martínez351860d2021-08-16 11:33:24 +02004const threadListRequestEvent = 'TWPT_ViewForumRequest';
Adrià Vilanova Martínez27c69962021-07-17 23:32:51 +02005const threadListLoadEvent = 'TWPT_ViewForumResponse';
avm99963afda2372021-08-08 20:54:05 +02006const createMessageLoadEvent = 'TWPT_CreateMessageRequest';
Adrià Vilanova Martínez27c69962021-07-17 23:32:51 +02007// Time after the last use when a cache entry should be deleted (in s):
Adrià Vilanova Martínezc41edf42021-07-18 02:06:55 +02008const cacheExpirationTime = 4 * 24 * 60 * 60; // 4 days
Adrià Vilanova Martínez27c69962021-07-17 23:32:51 +02009// Probability of running the piece of code to remove unused cache entries after
10// loading the thread list.
11const probRemoveUnusedCacheEntries = 0.10; // 10%
12
Adrià Vilanova Martínezc41edf42021-07-18 02:06:55 +020013// Time after which an unauthorized forum entry expires (in s).
14const unauthorizedForumExpirationTime = 1 * 24 * 60 * 60; // 1 day
15
Adrià Vilanova Martínez27c69962021-07-17 23:32:51 +020016export default class AvatarsDB {
17 constructor() {
18 this.dbPromise = undefined;
19 this.openDB();
avm99963afda2372021-08-08 20:54:05 +020020 this.setUpInvalidationsHandlers();
Adrià Vilanova Martínez27c69962021-07-17 23:32:51 +020021 }
22
23 openDB() {
24 if (this.dbPromise === undefined)
25 this.dbPromise = openDB(dbName, 1, {
26 upgrade: (udb, oldVersion, newVersion, transaction) => {
27 switch (oldVersion) {
28 case 0:
29 var cache = udb.createObjectStore('avatarsCache', {
30 keyPath: 'threadId',
31 });
32 cache.createIndex(
33 'lastUsedTimestamp', 'lastUsedTimestamp', {unique: false});
34
35 var unauthedForums = udb.createObjectStore('unauthorizedForums', {
36 keyPath: 'forumId',
37 });
38 unauthedForums.createIndex(
39 'expirationTimestamp', 'expirationTimestamp',
40 {unique: false});
41 }
42 },
43 });
44 }
45
Adrià Vilanova Martínezc41edf42021-07-18 02:06:55 +020046 // avatarsCache methods:
Adrià Vilanova Martínez27c69962021-07-17 23:32:51 +020047 getCacheEntry(threadId) {
48 return this.dbPromise.then(db => db.get('avatarsCache', threadId));
49 }
50
51 putCacheEntry(entry) {
52 return this.dbPromise.then(db => db.put('avatarsCache', entry));
53 }
54
55 invalidateCacheEntryIfExists(threadId) {
56 return this.dbPromise.then(db => db.delete('avatarsCache', threadId));
57 }
58
59 removeUnusedCacheEntries() {
60 console.debug('[threadListAvatars] Removing unused cache entries...');
61 return this.dbPromise
62 .then(db => {
63 var upperBoundTimestamp =
Adrià Vilanova Martínezc41edf42021-07-18 02:06:55 +020064 Math.floor(Date.now() / 1000) - cacheExpirationTime;
Adrià Vilanova Martínez27c69962021-07-17 23:32:51 +020065 var range = IDBKeyRange.upperBound(upperBoundTimestamp);
66
67 var tx = db.transaction('avatarsCache', 'readwrite');
68 var index = tx.store.index('lastUsedTimestamp');
69 return index.openCursor(range);
70 })
71 .then(function iterateCursor(cursor) {
72 if (!cursor) return;
73 cursor.delete();
74 return cursor.continue().then(iterateCursor);
75 });
76 }
77
avm99963afda2372021-08-08 20:54:05 +020078 setUpInvalidationsHandlers() {
Adrià Vilanova Martínez351860d2021-08-16 11:33:24 +020079 let ignoredRequests = [];
80
81 window.addEventListener(threadListRequestEvent, e => {
Adrià Vilanova Martínez31a66162021-08-16 10:31:23 +020082 // Ignore ViewForum requests made by the chat feature and the "Mark as
83 // duplicate" dialog.
84 //
85 // All those requests have |maxNum| set to 10 and 20 respectively, while
86 // the requests that we want to handle are the ones to initially load the
87 // thread list (which currently requests 100 threads) and the ones to load
88 // more threads (which request 50 threads).
89 var maxNum = e.detail.body?.['2']?.['1']?.['2'];
Adrià Vilanova Martínez351860d2021-08-16 11:33:24 +020090 if (maxNum == 10 || maxNum == 20) ignoredRequests.push(e.$TWPTID);
91 });
92 window.addEventListener(threadListLoadEvent, e => {
93 if (ignoredRequests.includes(e.$TWPTID)) {
94 ignoredRequests = ignoredRequests.filter(item => item != e.$TWPTID);
95 return;
96 }
Adrià Vilanova Martínez31a66162021-08-16 10:31:23 +020097
98 this.handleInvalidationsByListLoad(e);
99 });
avm99963afda2372021-08-08 20:54:05 +0200100 window.addEventListener(
101 createMessageLoadEvent, e => this.handleInvalidationByNewMessage(e));
Adrià Vilanova Martínez27c69962021-07-17 23:32:51 +0200102 }
103
avm99963afda2372021-08-08 20:54:05 +0200104 handleInvalidationsByListLoad(e) {
Adrià Vilanova Martínez27c69962021-07-17 23:32:51 +0200105 var response = e?.detail?.body;
106 var threads = response?.['1']?.['2'];
107 if (threads === undefined) {
108 console.warn(
109 '[threadListAvatars] The thread list doesn\'t contain any threads.');
110 return;
111 }
112
113 var promises = [];
114 threads.forEach(t => {
115 var id = t?.['2']?.['1']?.['1'];
Adrià Vilanova Martínez27c69962021-07-17 23:32:51 +0200116 var currentLastMessageId = t?.['2']?.['10'];
117
Adrià Vilanova Martínezd5615032021-07-22 22:19:37 +0200118 if (id === undefined) return;
Adrià Vilanova Martínez27c69962021-07-17 23:32:51 +0200119
120 promises.push(this.getCacheEntry(id).then(entry => {
121 if (entry === undefined) return;
122
123 // If the cache entry is still valid.
Adrià Vilanova Martínezd5615032021-07-22 22:19:37 +0200124 if (currentLastMessageId !== undefined &&
125 currentLastMessageId == entry.lastMessageId) {
Adrià Vilanova Martínez27c69962021-07-17 23:32:51 +0200126 entry.lastUsedTimestamp = Math.floor(Date.now() / 1000);
127 return this.putCacheEntry(entry).catch(err => {
128 console.error(
129 '[threadListAvatars] Error while updating lastUsedTimestamp from thread in cache:',
130 err);
131 });
132 }
133
134 console.debug(
135 '[threadListAvatars] Invalidating thread', entry.threadId);
136 return this.invalidateCacheEntryIfExists(entry.threadId).catch(err => {
137 console.error(
138 '[threadListAvatars] Error while invalidating thread from cache:',
139 err);
140 });
141 }));
142 });
143
144 Promise.allSettled(promises).then(() => {
145 if (Math.random() < probRemoveUnusedCacheEntries)
146 this.removeUnusedCacheEntries().catch(err => {
147 console.error(
148 '[threadListAvatars] Error while removing unused cache entries:',
149 err);
150 });
151 });
152 }
Adrià Vilanova Martínezc41edf42021-07-18 02:06:55 +0200153
avm99963afda2372021-08-08 20:54:05 +0200154 handleInvalidationByNewMessage(e) {
155 var request = e?.detail?.body;
156 var threadId = request?.['2'];
157 if (threadId === undefined) {
158 console.warn(
159 '[threadListAvatars] Thread ID couldn\'t be parsed from the CreateMessage request.');
160 return;
161 }
162
163 console.debug(
164 '[threadListAvatars] Invalidating thread', threadId,
165 'due to intercepting a CreateMessage request for that thread.');
166 return this.invalidateCacheEntryIfExists(threadId);
167 }
168
Adrià Vilanova Martínezc41edf42021-07-18 02:06:55 +0200169 // unauthorizedForums methods:
170 isForumUnauthorized(forumId) {
171 return this.dbPromise.then(db => db.get('unauthorizedForums', forumId))
172 .then(entry => {
173 if (entry === undefined) return false;
174
175 var now = Math.floor(Date.now() / 1000);
176 if (entry.expirationTimestamp > now) return true;
177
178 this.invalidateUnauthorizedForum(forumId);
179 return false;
180 });
181 }
182
183 putUnauthorizedForum(forumId) {
184 return this.dbPromise.then(db => db.put('unauthorizedForums', {
185 forumId,
186 expirationTimestamp:
187 Math.floor(Date.now() / 1000) + unauthorizedForumExpirationTime,
188 }));
189 }
190
191 invalidateUnauthorizedForum(forumId) {
192 return this.dbPromise.then(db => db.delete('unauthorizedForums', forumId));
193 }
Adrià Vilanova Martínez27c69962021-07-17 23:32:51 +0200194};