Cache thread avatars
This change adds the AvatarsDB class, which lets the threadListAvatars
feature interact with a cache of thread avatars.
This is an implementation of points 1 and 2 in the "Idea" section of the
following doc: go/eu7T9m (public link available in the linked bug). The
doc includes a rationale for this change and what it does.
Bug: 2
Change-Id: Ida9fcd909e3bd4a552361317b9013cb8734272a6
diff --git a/src/contentScripts/communityConsole/avatars.js b/src/contentScripts/communityConsole/avatars.js
index 253fe8f..3b27333 100644
--- a/src/contentScripts/communityConsole/avatars.js
+++ b/src/contentScripts/communityConsole/avatars.js
@@ -1,9 +1,16 @@
+import {waitFor} from 'poll-until-promise';
+
import {CCApi} from '../../common/api.js';
import {parseUrl} from '../../common/commonUtils.js';
-export var avatars = {
- isFilterSetUp: false,
- privateForums: [],
+import AvatarsDB from './utils/AvatarsDB.js'
+
+export default class AvatarsHandler {
+ constructor() {
+ this.isFilterSetUp = false;
+ this.privateForums = [];
+ this.db = new AvatarsDB();
+ }
// Gets a list of private forums. If it is already cached, the cached list is
// returned; otherwise it is also computed and cached.
@@ -41,7 +48,7 @@
this.isFilterSetUp = true;
return resolve(this.privateForums);
});
- },
+ }
// Some threads belong to private forums, and this feature will not be able to
// get its avatars since it makes an anonymomus call to get the contents of
@@ -53,10 +60,11 @@
return this.getPrivateForums().then(privateForums => {
return !privateForums.includes(thread.forum);
});
- },
+ }
- // Get an object with the author of the thread and an array of the first |num|
- // replies from the thread |thread|.
+ // Get an object with the author of the thread, an array of the first |num|
+ // replies from the thread |thread|, and additional information about the
+ // thread.
getFirstMessages(thread, num = 15) {
return CCApi(
'ViewThread', {
@@ -103,9 +111,82 @@
return {
messages,
author,
+
+ // The following fields are useful for the cache and can be
+ // undefined, but this is checked before adding an entry to the
+ // cache.
+ updatedTimestamp: data?.['1']?.['2']?.['1']?.['4'],
+ lastMessageId: data?.['1']?.['2']?.['10'],
};
});
- },
+ }
+
+ // Get a list of at most |num| avatars for thread |thread| by calling the API
+ getVisibleAvatarsFromServer(thread, num) {
+ return this.getFirstMessages(thread).then(result => {
+ var messages = result.messages;
+ var author = result.author;
+ var updatedTimestamp =
+ Math.floor(Number.parseInt(result.updatedTimestamp) / 1000000);
+ var lastMessageId = result.lastMessageId;
+
+ var avatarUrls = [];
+
+ var authorUrl = author?.['1']?.['2'];
+ if (authorUrl !== undefined) avatarUrls.push(authorUrl);
+
+ for (var m of messages) {
+ var url = m?.['3']?.['1']?.['2'];
+
+ if (url === undefined) continue;
+ if (!avatarUrls.includes(url)) avatarUrls.push(url);
+ if (avatarUrls.length == 3) break;
+ }
+
+ // Add entry to cache if all the extra metadata could be retrieved.
+ if (updatedTimestamp !== undefined && lastMessageId !== undefined)
+ this.db.putCacheEntry({
+ threadId: thread.thread,
+ updatedTimestamp,
+ lastMessageId,
+ avatarUrls,
+ num,
+ lastUsedTimestamp: Math.floor(Date.now() / 1000),
+ });
+
+ return avatarUrls;
+ });
+ }
+
+ // Returns an object with a cache entry that matches the request if found (via
+ // the |entry| property). The property |found| indicates whether the cache
+ // entry was found.
+ getVisibleAvatarsFromCache(thread, num) {
+ return waitFor(
+ () => this.db.getCacheEntry(thread.thread).then(entry => {
+ if (entry === undefined || entry.num < num)
+ return {
+ found: false,
+ };
+
+ // Only use cache entry if lastUsedTimestamp is within the last 30
+ // seconds (which means it has been checked for invalidations):
+ var now = Math.floor(Date.now() / 1000);
+ var diff = now - entry.lastUsedTimestamp;
+ if (diff > 30)
+ throw new Error(
+ 'lastUsedTimestamp isn\'t within the last 30 seconds (the difference is:',
+ diff, ').');
+ return {
+ found: true,
+ entry,
+ };
+ }),
+ {
+ interval: 400,
+ timeout: 10 * 1000,
+ });
+ }
// Get a list of at most |num| avatars for thread |thread|
getVisibleAvatars(thread, num = 3) {
@@ -115,27 +196,19 @@
return [];
}
- return this.getFirstMessages(thread).then(result => {
- var messages = result.messages;
- var author = result.author;
+ return this.getVisibleAvatarsFromCache(thread, num)
+ .then(res => {
+ if (res.found) return res.entry.avatarUrls;
- var avatarUrls = [];
-
- var authorUrl = author?.['1']?.['2'];
- if (authorUrl !== undefined) avatarUrls.push(authorUrl);
-
- for (var m of messages) {
- var url = m?.['3']?.['1']?.['2'];
-
- if (url === undefined) continue;
- if (!avatarUrls.includes(url)) avatarUrls.push(url);
- if (avatarUrls.length == 3) break;
- }
-
- return avatarUrls;
- });
+ return this.getVisibleAvatarsFromServer(thread, num);
+ })
+ .catch(err => {
+ console.error(
+ '[threadListAvatars] Error while retrieving avatars:', err);
+ return this.getVisibleAvatarsFromServer(thread, num);
+ });
});
- },
+ }
// Inject avatars for thread summary (thread item) |node| in a thread list.
inject(node) {
@@ -174,5 +247,5 @@
'[threadListAvatars] Could not retrieve avatars for thread',
thread, err);
});
- },
+ }
};