blob: 191940fdb9331a89a0ce40c7b8f5d4258bcf4680 [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):
Adrià Vilanova Martínezc41edf42021-07-18 02:06:55 +02006const cacheExpirationTime = 4 * 24 * 60 * 60; // 4 days
Adrià Vilanova Martínez27c69962021-07-17 23:32:51 +02007// Probability of running the piece of code to remove unused cache entries after
8// loading the thread list.
9const probRemoveUnusedCacheEntries = 0.10; // 10%
10
Adrià Vilanova Martínezc41edf42021-07-18 02:06:55 +020011// Time after which an unauthorized forum entry expires (in s).
12const unauthorizedForumExpirationTime = 1 * 24 * 60 * 60; // 1 day
13
Adrià Vilanova Martínez27c69962021-07-17 23:32:51 +020014export default class AvatarsDB {
15 constructor() {
16 this.dbPromise = undefined;
17 this.openDB();
18 this.setUpInvalidationsHandler();
19 }
20
21 openDB() {
22 if (this.dbPromise === undefined)
23 this.dbPromise = openDB(dbName, 1, {
24 upgrade: (udb, oldVersion, newVersion, transaction) => {
25 switch (oldVersion) {
26 case 0:
27 var cache = udb.createObjectStore('avatarsCache', {
28 keyPath: 'threadId',
29 });
30 cache.createIndex(
31 'lastUsedTimestamp', 'lastUsedTimestamp', {unique: false});
32
33 var unauthedForums = udb.createObjectStore('unauthorizedForums', {
34 keyPath: 'forumId',
35 });
36 unauthedForums.createIndex(
37 'expirationTimestamp', 'expirationTimestamp',
38 {unique: false});
39 }
40 },
41 });
42 }
43
Adrià Vilanova Martínezc41edf42021-07-18 02:06:55 +020044 // avatarsCache methods:
Adrià Vilanova Martínez27c69962021-07-17 23:32:51 +020045 getCacheEntry(threadId) {
46 return this.dbPromise.then(db => db.get('avatarsCache', threadId));
47 }
48
49 putCacheEntry(entry) {
50 return this.dbPromise.then(db => db.put('avatarsCache', entry));
51 }
52
53 invalidateCacheEntryIfExists(threadId) {
54 return this.dbPromise.then(db => db.delete('avatarsCache', threadId));
55 }
56
57 removeUnusedCacheEntries() {
58 console.debug('[threadListAvatars] Removing unused cache entries...');
59 return this.dbPromise
60 .then(db => {
61 var upperBoundTimestamp =
Adrià Vilanova Martínezc41edf42021-07-18 02:06:55 +020062 Math.floor(Date.now() / 1000) - cacheExpirationTime;
Adrià Vilanova Martínez27c69962021-07-17 23:32:51 +020063 var range = IDBKeyRange.upperBound(upperBoundTimestamp);
64
65 var tx = db.transaction('avatarsCache', 'readwrite');
66 var index = tx.store.index('lastUsedTimestamp');
67 return index.openCursor(range);
68 })
69 .then(function iterateCursor(cursor) {
70 if (!cursor) return;
71 cursor.delete();
72 return cursor.continue().then(iterateCursor);
73 });
74 }
75
76 setUpInvalidationsHandler() {
77 window.addEventListener(
78 threadListLoadEvent, e => this.handleInvalidations(e));
79 }
80
81 handleInvalidations(e) {
82 var response = e?.detail?.body;
83 var threads = response?.['1']?.['2'];
84 if (threads === undefined) {
85 console.warn(
86 '[threadListAvatars] The thread list doesn\'t contain any threads.');
87 return;
88 }
89
90 var promises = [];
91 threads.forEach(t => {
92 var id = t?.['2']?.['1']?.['1'];
Adrià Vilanova Martínez27c69962021-07-17 23:32:51 +020093 var currentLastMessageId = t?.['2']?.['10'];
94
Adrià Vilanova Martínezd5615032021-07-22 22:19:37 +020095 if (id === undefined) return;
Adrià Vilanova Martínez27c69962021-07-17 23:32:51 +020096
97 promises.push(this.getCacheEntry(id).then(entry => {
98 if (entry === undefined) return;
99
100 // If the cache entry is still valid.
Adrià Vilanova Martínezd5615032021-07-22 22:19:37 +0200101 if (currentLastMessageId !== undefined &&
102 currentLastMessageId == entry.lastMessageId) {
Adrià Vilanova Martínez27c69962021-07-17 23:32:51 +0200103 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 }
Adrià Vilanova Martínezc41edf42021-07-18 02:06:55 +0200130
131 // unauthorizedForums methods:
132 isForumUnauthorized(forumId) {
133 return this.dbPromise.then(db => db.get('unauthorizedForums', forumId))
134 .then(entry => {
135 if (entry === undefined) return false;
136
137 var now = Math.floor(Date.now() / 1000);
138 if (entry.expirationTimestamp > now) return true;
139
140 this.invalidateUnauthorizedForum(forumId);
141 return false;
142 });
143 }
144
145 putUnauthorizedForum(forumId) {
146 return this.dbPromise.then(db => db.put('unauthorizedForums', {
147 forumId,
148 expirationTimestamp:
149 Math.floor(Date.now() / 1000) + unauthorizedForumExpirationTime,
150 }));
151 }
152
153 invalidateUnauthorizedForum(forumId) {
154 return this.dbPromise.then(db => db.delete('unauthorizedForums', forumId));
155 }
Adrià Vilanova Martínez27c69962021-07-17 23:32:51 +0200156};