blob: 05149b56c2219d5d6e9fecacab6a6442c403ea2c [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ínez27c69962021-07-17 23:32:51 +020078 window.addEventListener(
avm99963afda2372021-08-08 20:54:05 +020079 threadListLoadEvent, e => this.handleInvalidationsByListLoad(e));
80 window.addEventListener(
81 createMessageLoadEvent, e => this.handleInvalidationByNewMessage(e));
Adrià Vilanova Martínez27c69962021-07-17 23:32:51 +020082 }
83
avm99963afda2372021-08-08 20:54:05 +020084 handleInvalidationsByListLoad(e) {
Adrià Vilanova Martínez27c69962021-07-17 23:32:51 +020085 var response = e?.detail?.body;
86 var threads = response?.['1']?.['2'];
87 if (threads === undefined) {
88 console.warn(
89 '[threadListAvatars] The thread list doesn\'t contain any threads.');
90 return;
91 }
92
93 var promises = [];
94 threads.forEach(t => {
95 var id = t?.['2']?.['1']?.['1'];
Adrià Vilanova Martínez27c69962021-07-17 23:32:51 +020096 var currentLastMessageId = t?.['2']?.['10'];
97
Adrià Vilanova Martínezd5615032021-07-22 22:19:37 +020098 if (id === undefined) return;
Adrià Vilanova Martínez27c69962021-07-17 23:32:51 +020099
100 promises.push(this.getCacheEntry(id).then(entry => {
101 if (entry === undefined) return;
102
103 // If the cache entry is still valid.
Adrià Vilanova Martínezd5615032021-07-22 22:19:37 +0200104 if (currentLastMessageId !== undefined &&
105 currentLastMessageId == entry.lastMessageId) {
Adrià Vilanova Martínez27c69962021-07-17 23:32:51 +0200106 entry.lastUsedTimestamp = Math.floor(Date.now() / 1000);
107 return this.putCacheEntry(entry).catch(err => {
108 console.error(
109 '[threadListAvatars] Error while updating lastUsedTimestamp from thread in cache:',
110 err);
111 });
112 }
113
114 console.debug(
115 '[threadListAvatars] Invalidating thread', entry.threadId);
116 return this.invalidateCacheEntryIfExists(entry.threadId).catch(err => {
117 console.error(
118 '[threadListAvatars] Error while invalidating thread from cache:',
119 err);
120 });
121 }));
122 });
123
124 Promise.allSettled(promises).then(() => {
125 if (Math.random() < probRemoveUnusedCacheEntries)
126 this.removeUnusedCacheEntries().catch(err => {
127 console.error(
128 '[threadListAvatars] Error while removing unused cache entries:',
129 err);
130 });
131 });
132 }
Adrià Vilanova Martínezc41edf42021-07-18 02:06:55 +0200133
avm99963afda2372021-08-08 20:54:05 +0200134 handleInvalidationByNewMessage(e) {
135 var request = e?.detail?.body;
136 var threadId = request?.['2'];
137 if (threadId === undefined) {
138 console.warn(
139 '[threadListAvatars] Thread ID couldn\'t be parsed from the CreateMessage request.');
140 return;
141 }
142
143 console.debug(
144 '[threadListAvatars] Invalidating thread', threadId,
145 'due to intercepting a CreateMessage request for that thread.');
146 return this.invalidateCacheEntryIfExists(threadId);
147 }
148
Adrià Vilanova Martínezc41edf42021-07-18 02:06:55 +0200149 // unauthorizedForums methods:
150 isForumUnauthorized(forumId) {
151 return this.dbPromise.then(db => db.get('unauthorizedForums', forumId))
152 .then(entry => {
153 if (entry === undefined) return false;
154
155 var now = Math.floor(Date.now() / 1000);
156 if (entry.expirationTimestamp > now) return true;
157
158 this.invalidateUnauthorizedForum(forumId);
159 return false;
160 });
161 }
162
163 putUnauthorizedForum(forumId) {
164 return this.dbPromise.then(db => db.put('unauthorizedForums', {
165 forumId,
166 expirationTimestamp:
167 Math.floor(Date.now() / 1000) + unauthorizedForumExpirationTime,
168 }));
169 }
170
171 invalidateUnauthorizedForum(forumId) {
172 return this.dbPromise.then(db => db.delete('unauthorizedForums', forumId));
173 }
Adrià Vilanova Martínez27c69962021-07-17 23:32:51 +0200174};