blob: ba32fea44ad2800addfb59a5b48e1a467a50e829 [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ínezac9fc9e2021-07-22 12:45:32 +020095 if (id === undefined || currentLastMessageId === 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ínezac9fc9e2021-07-22 12:45:32 +0200101 if (currentLastMessageId == entry.lastMessageId) {
Adrià Vilanova Martínez27c69962021-07-17 23:32:51 +0200102 entry.lastUsedTimestamp = Math.floor(Date.now() / 1000);
103 return this.putCacheEntry(entry).catch(err => {
104 console.error(
105 '[threadListAvatars] Error while updating lastUsedTimestamp from thread in cache:',
106 err);
107 });
108 }
109
110 console.debug(
111 '[threadListAvatars] Invalidating thread', entry.threadId);
112 return this.invalidateCacheEntryIfExists(entry.threadId).catch(err => {
113 console.error(
114 '[threadListAvatars] Error while invalidating thread from cache:',
115 err);
116 });
117 }));
118 });
119
120 Promise.allSettled(promises).then(() => {
121 if (Math.random() < probRemoveUnusedCacheEntries)
122 this.removeUnusedCacheEntries().catch(err => {
123 console.error(
124 '[threadListAvatars] Error while removing unused cache entries:',
125 err);
126 });
127 });
128 }
Adrià Vilanova Martínezc41edf42021-07-18 02:06:55 +0200129
130 // unauthorizedForums methods:
131 isForumUnauthorized(forumId) {
132 return this.dbPromise.then(db => db.get('unauthorizedForums', forumId))
133 .then(entry => {
134 if (entry === undefined) return false;
135
136 var now = Math.floor(Date.now() / 1000);
137 if (entry.expirationTimestamp > now) return true;
138
139 this.invalidateUnauthorizedForum(forumId);
140 return false;
141 });
142 }
143
144 putUnauthorizedForum(forumId) {
145 return this.dbPromise.then(db => db.put('unauthorizedForums', {
146 forumId,
147 expirationTimestamp:
148 Math.floor(Date.now() / 1000) + unauthorizedForumExpirationTime,
149 }));
150 }
151
152 invalidateUnauthorizedForum(forumId) {
153 return this.dbPromise.then(db => db.delete('unauthorizedForums', forumId));
154 }
Adrià Vilanova Martínez27c69962021-07-17 23:32:51 +0200155};