import Globals from './globals'; import { Message, ShouldNeverHappenError } from './data-types'; // TODO: make this non-static export default class RecentMessageRAMCache { private data = new Map(); // (serverId, channelId) -> { messages, size, hasFirstMessage, lastUsed } private size = 0; clear(): void { this.data.clear(); this.size = 0; } _cullIfNeeded(): void { // TODO: Test this if (this.size > Globals.MAX_RAM_CACHED_MESSAGES_TOTAL_SIZE) { let entries = Array.from(this.data.entries()) .map(([ key, value ]) => { return { id: key, value: value }}) .sort((a, b) => b.value.lastUsed.getTime() - a.value.lastUsed.getTime()); while (this.size > Globals.MAX_RAM_CACHED_MESSAGES_TOTAL_SIZE) { let entry = entries.pop(); if (entry === undefined) throw new ShouldNeverHappenError('No entry in the array but the message cache still has a size...'); this.data.delete(entry.id); this.size -= entry.value.size; } } } _cullChannelIfNeeded(serverId: string, channelId: string): void { // TODO: test this let id = `s#${serverId}/c#${channelId}`; let value = this.data.get(id); if (!value) return; while (value.size > Globals.MAX_RAM_CACHED_MESSAGES_CHANNEL_SIZE) { value.hasFirstMessage = false; let message = value.messages.shift(); if (!message) return; value.size -= message.text?.length ?? 0; this.size -= message.text?.length ?? 0; } } // @param messages may be modified in addNewMessage due to pass-by-reference fun putRecentMessages(serverId: string, channelId: string, messages: Message[]): void { let size = 0; for (let message of messages) { size += message.text?.length ?? 0; } if (size > Globals.MAX_RAM_CACHED_MESSAGES_CHANNEL_SIZE) return; let id = `s#${serverId}/c#${channelId}`; this.data.set(id, { messages: messages, size: size, hasFirstMessage: true, // will be false if this was ever channel-culled lastUsed: new Date() }); this.size += size; this._cullIfNeeded(); } addNewMessage(serverId: string, message: Message): void { let channelId = message.channel.id; let id = `s#${serverId}/c#${channelId}`; let value = this.data.get(id); if (!value) return; value.messages.push(message); value.size += message.text?.length ?? 0; this.size += message.text?.length ?? 0; this._cullChannelIfNeeded(serverId, channelId); this._cullIfNeeded(); } updateMessage(serverId: string, oldMessage: Message, newMessage: Message): void { let channelId = oldMessage.channel.id; let id = `s#${serverId}/c#${channelId}`; let value = this.data.get(id); if (!value) return; let taggedIndex = value.messages.findIndex(cachedMessage => cachedMessage.id == oldMessage.id); let [ oldCachedMessage ] = value.messages.splice(taggedIndex, 1, newMessage); value.size -= oldCachedMessage.text?.length ?? 0; value.size += newMessage.text?.length ?? 0; this.size -= oldCachedMessage.text?.length ?? 0; this.size += newMessage.text?.length ?? 0; this._cullChannelIfNeeded(serverId, channelId); this._cullIfNeeded(); } deleteMessage(serverId: string, message: Message): void { let channelId = message.channel.id; let id = `s#${serverId}/c#${channelId}`; let value = this.data.get(id); if (!value) return; let taggedIndex = value.messages.findIndex(cachedMessage => cachedMessage.id == message.id); let [ oldCachedMessage ] = value.messages.splice(taggedIndex, 1); value.size -= oldCachedMessage.text?.length ?? 0; this.size -= oldCachedMessage.text?.length ?? 0; // No need to cull since we are always shrinking } dropChannel(serverId: string, channelId: string): void { let id = `s#${serverId}/c#${channelId}`; let value = this.data.get(id); if (!value) return; this.size -= value.size; this.data.delete(id); } getRecentMessages(serverId: string, channelId: string, number: number): Message[] | null { let id = `s#${serverId}/c#${channelId}`; if (!this.data.has(id)) { //LOG.silly(`recent message cache miss on ${id} requesting ${number}`); return null; // not in the cache } let value = this.data.get(id); if (!value) throw new ShouldNeverHappenError('javascript is not on a single thread >:|'); if (!value.hasFirstMessage && value.messages.length < number) { //LOG.silly(`recent message cache incomplete on ${id} requesting ${number}`, { value }); return null; // requesting older messages than we have } value.lastUsed = new Date(); return value.messages.slice(-number); } }