blob: a8c155c9c536e76d38cc2ce20a4cc75e8099bca3 [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'];
93 var currentUpdatedTimestamp =
94 Math.floor(Number.parseInt(t?.['2']?.['1']?.['4']) / 1000000);
95 var currentLastMessageId = t?.['2']?.['10'];
96
97 if (id === undefined || currentUpdatedTimestamp === undefined ||
98 currentLastMessageId === undefined)
99 return;
100
101 promises.push(this.getCacheEntry(id).then(entry => {
102 if (entry === undefined) return;
103
104 // If the cache entry is still valid.
105 if (currentLastMessageId == entry.lastMessageId ||
106 currentUpdatedTimestamp <= entry.updatedTimestamp) {
107 entry.lastUsedTimestamp = Math.floor(Date.now() / 1000);
108 return this.putCacheEntry(entry).catch(err => {
109 console.error(
110 '[threadListAvatars] Error while updating lastUsedTimestamp from thread in cache:',
111 err);
112 });
113 }
114
115 console.debug(
116 '[threadListAvatars] Invalidating thread', entry.threadId);
117 return this.invalidateCacheEntryIfExists(entry.threadId).catch(err => {
118 console.error(
119 '[threadListAvatars] Error while invalidating thread from cache:',
120 err);
121 });
122 }));
123 });
124
125 Promise.allSettled(promises).then(() => {
126 if (Math.random() < probRemoveUnusedCacheEntries)
127 this.removeUnusedCacheEntries().catch(err => {
128 console.error(
129 '[threadListAvatars] Error while removing unused cache entries:',
130 err);
131 });
132 });
133 }
Adrià Vilanova Martínezc41edf42021-07-18 02:06:55 +0200134
135 // unauthorizedForums methods:
136 isForumUnauthorized(forumId) {
137 return this.dbPromise.then(db => db.get('unauthorizedForums', forumId))
138 .then(entry => {
139 if (entry === undefined) return false;
140
141 var now = Math.floor(Date.now() / 1000);
142 if (entry.expirationTimestamp > now) return true;
143
144 this.invalidateUnauthorizedForum(forumId);
145 return false;
146 });
147 }
148
149 putUnauthorizedForum(forumId) {
150 return this.dbPromise.then(db => db.put('unauthorizedForums', {
151 forumId,
152 expirationTimestamp:
153 Math.floor(Date.now() / 1000) + unauthorizedForumExpirationTime,
154 }));
155 }
156
157 invalidateUnauthorizedForum(forumId) {
158 return this.dbPromise.then(db => db.delete('unauthorizedForums', forumId));
159 }
Adrià Vilanova Martínez27c69962021-07-17 23:32:51 +0200160};