zulip/web/src/message_list_data_cache.ts

100 lines
3.3 KiB
TypeScript

import * as all_messages_data from "./all_messages_data";
import type {Filter} from "./filter";
import type {MessageListData} from "./message_list_data";
// LRU cache for message list data.
//
// While it's unlikely that user will narrow to empty filter,
// but we will still need to update all_messages_data since it used
// as super set for populating other views.
let cache = new Map<number, MessageListData>([[0, all_messages_data.all_messages_data]]);
let latest_key = 0;
// Maximum number of data items to cache.
const CACHE_STORAGE_LIMIT = 100;
function move_to_end(key: number, cached_data: MessageListData): void {
// Move the item to the end of the cache.
// Map remembers the original insertion order of the keys.
cache.delete(key);
latest_key += 1;
cache.set(latest_key, cached_data);
// In theory, a cache like this might need to consider integer
// overflow on latest_key, but that's not a realistic possibility
// with how these data structures are used given the lifetime on a
// Zulip web app window.
}
export function get(filter: Filter): MessageListData | undefined {
for (const [key, cached_data] of cache.entries()) {
if (cached_data.filter.equals(filter)) {
move_to_end(key, cached_data);
return cached_data;
}
}
return undefined;
}
export function add(message_list_data: MessageListData): void {
for (const [key, cached_data] of cache.entries()) {
if (cached_data.filter.equals(message_list_data.filter)) {
// We could chose to maintain in the cache the
// message_list_data passed in, or the one already in the
// cache. These can be different primarily when they
// represent non-overlapping ID ranges.
//
// Saving the more current of the two message lists seems
// like a better tiebreak, but we may be able to eliminate
// this class of case if we implement the plan in #16697.
move_to_end(key, message_list_data);
return;
}
}
if (cache.size >= CACHE_STORAGE_LIMIT) {
// Remove the oldest item from the cache.
for (const [key, cached_data] of cache.entries()) {
// We never want to remove the all_messages_data from the cache.
if (cached_data.filter.equals(all_messages_data.all_messages_data.filter)) {
continue;
}
cache.delete(key);
break;
}
}
latest_key += 1;
cache.set(latest_key, message_list_data);
}
export function all(): MessageListData[] {
return [...cache.values()];
}
export function clear(): void {
cache = new Map([[0, all_messages_data.all_messages_data]]);
latest_key = 0;
}
export function get_superset_datasets(filter: Filter): MessageListData[] {
const superset_datasets = [];
// Try to get exact match first.
const superset_data = get(filter);
if (superset_data !== undefined) {
// TODO: Search for additional superset datasets.
superset_datasets.push(superset_data);
}
return [...superset_datasets, all_messages_data.all_messages_data];
}
export function remove(filter: Filter): void {
for (const [key, cached_data] of cache.entries()) {
if (cached_data.filter.equals(filter)) {
cache.delete(key);
return;
}
}
}