Merge pull request #5 from jaronpate/ft/lru-cache

Add LRU cache implementation
This commit is contained in:
2025-02-15 17:52:28 -06:00
committed by GitHub
5 changed files with 112 additions and 20 deletions
+7 -3
View File
@@ -3,9 +3,13 @@ import type { Context } from '../types/context';
const EventBus = new EventEmitter() as Emitter; const EventBus = new EventEmitter() as Emitter;
export type Emitter = EventEmitter & { export class Emitter extends EventEmitter {
emit: (event_name: string, context: Context | null, ...args: any[]) => boolean emit(event_name: string, context: Context | null, ...args: any[]): boolean {
on: (event_name: string, listener: Function) => Emitter return super.emit(event_name, context, ...args);
}
on(event_name: string, listener: (context: Context | null, ...args: any[]) => void): this {
return super.on(event_name, listener);
}
} }
export { EventBus }; export { EventBus };
+93 -4
View File
@@ -5,11 +5,12 @@ import type { Processor } from "../processor";
export class ObjectCache<T, K extends T> { export class ObjectCache<T, K extends T> {
cache = new Map<string, K>(); cache: LRUCache<string, K>;
requests = new Map<string, Promise<K>>(); requests = new Map<string, Promise<K>>();
readonly type: string; readonly type: string;
constructor(protected api: API, processor: Processor, type: string, private readonly factory: { [fromDiscord]: (data: any) => K } & (new (data: T) => T), events: string[]) { constructor(protected api: API, processor: Processor, type: string, private readonly factory: { [fromDiscord]: (data: any) => K } & (new (data: T) => T), events: string[], capacity: number = 10_000) {
this.cache = new LRUCache<string, K>(10_000)
this.type = type; this.type = type;
processor.events.register(events, async (_context: Context, e: Record<string, any>) => { processor.events.register(events, async (_context: Context, e: Record<string, any>) => {
this.cache.set(e.id, factory[fromDiscord](e)); this.cache.set(e.id, factory[fromDiscord](e));
@@ -64,8 +65,96 @@ export class ObjectCache<T, K extends T> {
return this.fetch(object_id); return this.fetch(object_id);
} }
} }
}
list(): Map<string, Promise<T> | T> { class ListNode<K, V> {
return this.cache; key: K;
value: V;
prev: ListNode<K, V> | null = null;
next: ListNode<K, V> | null = null;
constructor(key: K, value: V) {
this.key = key;
this.value = value;
} }
} }
class LRUCache<K, V> {
private capacity: number;
private map: Map<K, ListNode<K, V>>;
private head: ListNode<K, V>;
private tail: ListNode<K, V>;
constructor(capacity: number) {
this.capacity = capacity;
this.map = new Map();
// Dummy head and tail to simplify boundary conditions
this.head = new ListNode<K, V>(null as any, null as any);
this.tail = new ListNode<K, V>(null as any, null as any);
this.head.next = this.tail;
this.tail.prev = this.head;
}
get(key: K): V | undefined {
if (!this.map.has(key)) return undefined;
const node = this.map.get(key)!;
this.moveToFront(node);
return node.value;
}
has(key: K): boolean {
return this.map.has(key);
}
set(key: K, value: V): void {
if (this.map.has(key)) {
// Update existing node value and move it to front
const node = this.map.get(key)!;
node.value = value;
this.moveToFront(node);
} else {
// Create a new node
const newNode = new ListNode(key, value);
this.map.set(key, newNode);
this.addToFront(newNode);
// If over capacity, remove the least recently used (LRU) node
if (this.map.size > this.capacity) {
this.removeLRU();
}
}
}
private moveToFront(node: ListNode<K, V>): void {
this.removeNode(node);
this.addToFront(node);
}
private addToFront(node: ListNode<K, V>): void {
node.next = this.head.next;
node.prev = this.head;
this.head.next!.prev = node;
this.head.next = node;
}
private removeNode(node: ListNode<K, V>): void {
node.prev!.next = node.next;
node.next!.prev = node.prev;
}
private removeLRU(): void {
const lruNode = this.tail.prev!;
this.removeNode(lruNode);
this.map.delete(lruNode.key);
}
*[Symbol.iterator](): IterableIterator<[K, V]> {
let current = this.head.next;
while (current && current !== this.tail) {
yield [current.key, current.value];
current = current.next;
}
}
}
+2 -3
View File
@@ -1,14 +1,13 @@
import { fromDiscord } from "../../types/common"; import { fromDiscord } from "../../types/common";
import { Message, type MessagePayload } from "../../types/message"; import { Message, type MessagePayload } from "../../types/message";
import type { API } from "../api"; import type { API } from "../api";
import type { Client } from "../client";
import type { Processor } from "../processor"; import type { Processor } from "../processor";
import { ObjectCache } from "./cache"; import { ObjectCache } from "./cache";
export class Messages extends ObjectCache<Message, MessagePayload> { export class Messages extends ObjectCache<Message, MessagePayload> {
constructor (protected client: Client, api: API, processor: Processor) { constructor (api: API, processor: Processor) {
super(api, processor, 'message', Message, ['MESSAGE_CREATE', 'MESSAGE_UPDATE']); super(api, processor, 'messages', Message, ['MESSAGE_CREATE', 'MESSAGE_UPDATE']);
} }
async fetch(channel_id: string, message_id: string): Promise<MessagePayload> { async fetch(channel_id: string, message_id: string): Promise<MessagePayload> {
+7 -7
View File
@@ -1,15 +1,15 @@
import { API } from './api';
import { Gateway } from './connectors/gateway';
import { Processor } from './processor';
import { Expectation, HandlerType, Intent, type ClientConfig } from '../types/common'; import { Expectation, HandlerType, Intent, type ClientConfig } from '../types/common';
import { Message, type DiscordMessage } from '../types/message';
import { exists, isNil } from '../utils/index'; import { exists, isNil } from '../utils/index';
import { type DiscordMessage, Message } from '../types/message'; import { API } from './api';
import { EventBus, type Emitter } from './bus';
import { Channels } from './caches/channels';
import { Guilds } from './caches/guilds'; import { Guilds } from './caches/guilds';
import { Messages } from './caches/messages'; import { Messages } from './caches/messages';
import { Channels } from './caches/channels';
import { Users } from './caches/users'; import { Users } from './caches/users';
import { Gateway } from './connectors/gateway';
import { hydrate, hydrator, type Hydrateable, type Hydrated } from './hydrator'; import { hydrate, hydrator, type Hydrateable, type Hydrated } from './hydrator';
import { EventBus, type Emitter } from './bus'; import { Processor } from './processor';
export class Client { export class Client {
readonly token: string; readonly token: string;
@@ -44,7 +44,7 @@ export class Client {
this.gateway = new Gateway(this, this.processor, this.api, this.bus); this.gateway = new Gateway(this, this.processor, this.api, this.bus);
this.guilds = new Guilds(this.api, this.processor); this.guilds = new Guilds(this.api, this.processor);
this.users = new Users(this.api, this.processor); this.users = new Users(this.api, this.processor);
this.messages = new Messages(this, this.api, this.processor); this.messages = new Messages(this.api, this.processor);
this.channels = new Channels(this.api, this.processor); this.channels = new Channels(this.api, this.processor);
// Save references to command and event handlers // Save references to command and event handlers
this.chatCommands = this.processor[HandlerType.ChatCommands]; this.chatCommands = this.processor[HandlerType.ChatCommands];
+3 -3
View File
@@ -1,10 +1,10 @@
import { exists, isNil } from '../utils'; import { exists, isNil } from '../utils';
import { GatewayObject } from './base'; import { GatewayObject } from './base';
import { fromDiscord, Trait, type Expectation, type Expects } from './common'; import type { Channel } from './channel';
import { fromDiscord, Trait, type Expectation } from './common';
import type { Embed } from './embed'; import type { Embed } from './embed';
import type { Guild } from './guild'; import type { Guild } from './guild';
import { Channel } from './channel'; import { User, type DiscordUser } from './user';
import { type DiscordUser, User } from './user';
export type Component = Partial<Button | SelectMenu> & { export type Component = Partial<Button | SelectMenu> & {
type: ComponentType; type: ComponentType;