From fb81a55035c4b6045279b7fbde7c7ca01ed00d1a Mon Sep 17 00:00:00 2001 From: Jaron Date: Sat, 15 Feb 2025 11:05:59 -0600 Subject: [PATCH 1/2] Added channel api shape and class --- src/services/bus.ts | 10 ++- src/services/caches/cache.ts | 97 ++++++++++++++++++++++++++++-- src/services/caches/channels.ts | 10 +++ src/services/caches/messages.ts | 5 +- src/services/client.ts | 5 +- src/services/connectors/gateway.ts | 2 +- src/services/hydrator.ts | 3 + src/types/channel.ts | 40 ++++++++++++ src/types/message.ts | 7 ++- testing/demo.test.ts | 12 ++-- 10 files changed, 170 insertions(+), 21 deletions(-) create mode 100644 src/services/caches/channels.ts diff --git a/src/services/bus.ts b/src/services/bus.ts index e5bd283..d44f1b0 100644 --- a/src/services/bus.ts +++ b/src/services/bus.ts @@ -3,9 +3,13 @@ import type { Context } from '../types/context'; const EventBus = new EventEmitter() as Emitter; -export type Emitter = EventEmitter & { - emit: (event_name: string, context: Context | null, ...args: any[]) => boolean - on: (event_name: string, listener: Function) => Emitter +export class Emitter extends EventEmitter { + emit(event_name: string, context: Context | null, ...args: any[]): boolean { + 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 }; \ No newline at end of file diff --git a/src/services/caches/cache.ts b/src/services/caches/cache.ts index d055e39..20de3ce 100644 --- a/src/services/caches/cache.ts +++ b/src/services/caches/cache.ts @@ -5,11 +5,12 @@ import type { Processor } from "../processor"; export class ObjectCache { - cache = new Map(); + cache: LRUCache; requests = new Map>(); 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(10_000) this.type = type; processor.events.register(events, async (_context: Context, e: Record) => { this.cache.set(e.id, factory[fromDiscord](e)); @@ -64,8 +65,96 @@ export class ObjectCache { return this.fetch(object_id); } } +} - list(): Map | T> { - return this.cache; +class ListNode { + key: K; + value: V; + prev: ListNode | null = null; + next: ListNode | null = null; + + constructor(key: K, value: V) { + this.key = key; + this.value = value; } } + +class LRUCache { + private capacity: number; + private map: Map>; + private head: ListNode; + private tail: ListNode; + + constructor(capacity: number) { + this.capacity = capacity; + this.map = new Map(); + + // Dummy head and tail to simplify boundary conditions + this.head = new ListNode(null as any, null as any); + this.tail = new ListNode(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): void { + this.removeNode(node); + this.addToFront(node); + } + + private addToFront(node: ListNode): void { + node.next = this.head.next; + node.prev = this.head; + this.head.next!.prev = node; + this.head.next = node; + } + + private removeNode(node: ListNode): 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; + } + } +} \ No newline at end of file diff --git a/src/services/caches/channels.ts b/src/services/caches/channels.ts new file mode 100644 index 0000000..e48118d --- /dev/null +++ b/src/services/caches/channels.ts @@ -0,0 +1,10 @@ +import { Channel } from "../../types/channel"; +import type { API } from "../api"; +import type { Processor } from "../processor"; +import { ObjectCache } from "./cache"; + +export class Channels extends ObjectCache { + constructor (api: API, processor: Processor) { + super(api, processor, 'channels', Channel, ['CHANNEL_CREATE', 'CHANNEL_UPDATE']); + } +} \ No newline at end of file diff --git a/src/services/caches/messages.ts b/src/services/caches/messages.ts index 404f046..ad2a177 100644 --- a/src/services/caches/messages.ts +++ b/src/services/caches/messages.ts @@ -1,14 +1,13 @@ import { fromDiscord } from "../../types/common"; import { Message, type MessagePayload } from "../../types/message"; import type { API } from "../api"; -import type { Client } from "../client"; import type { Processor } from "../processor"; import { ObjectCache } from "./cache"; export class Messages extends ObjectCache { - constructor (protected client: Client, api: API, processor: Processor) { - super(api, processor, 'message', Message, ['MESSAGE_CREATE', 'MESSAGE_UPDATE']); + constructor (api: API, processor: Processor) { + super(api, processor, 'messages', Message, ['MESSAGE_CREATE', 'MESSAGE_UPDATE']); } async fetch(channel_id: string, message_id: string): Promise { diff --git a/src/services/client.ts b/src/services/client.ts index 9fe5886..3031b97 100644 --- a/src/services/client.ts +++ b/src/services/client.ts @@ -7,6 +7,7 @@ import { type DiscordMessage, Message } from '../types/message'; import { Guilds } from './caches/guilds'; import { Messages } from './caches/messages'; import { Users } from './caches/users'; +import { Channels } from './caches/channels'; import { hydrate, hydrator, type Hydrateable, type Hydrated } from './hydrator'; import { EventBus, type Emitter } from './bus'; @@ -23,6 +24,7 @@ export class Client { guilds: Guilds; users: Users; messages: Messages; + channels: Channels; chatCommands: Processor[HandlerType.ChatCommands]; applicationCommands: Processor[HandlerType.ApplicationCommands]; interactions: Processor[HandlerType.Interactions]; @@ -42,7 +44,8 @@ export class Client { this.gateway = new Gateway(this, this.processor, this.api, this.bus); this.guilds = new Guilds(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); // Save references to command and event handlers this.chatCommands = this.processor[HandlerType.ChatCommands]; this.applicationCommands = this.processor[HandlerType.ApplicationCommands]; diff --git a/src/services/connectors/gateway.ts b/src/services/connectors/gateway.ts index 241b9c5..5112253 100644 --- a/src/services/connectors/gateway.ts +++ b/src/services/connectors/gateway.ts @@ -121,7 +121,7 @@ export class Gateway { throw new Error('Event name is required'); } - // log('Event:', payload.t); + log('Event:', payload.t); // Extract the event name and data const event_name = payload.t; diff --git a/src/services/hydrator.ts b/src/services/hydrator.ts index 479ef81..03278d1 100644 --- a/src/services/hydrator.ts +++ b/src/services/hydrator.ts @@ -56,6 +56,9 @@ export async function hydrate, K exte } else if (expectation === Expectation.Guild && exists(data.guild_id)) { let message = data as unknown as HydratedMessage<[Expectation.Guild, ...Expectation[]]>; message.guild = await client.guilds.get(data.guild_id); + } else if (expectation === Expectation.Channel && exists(data.channel_id)) { + let message = data as unknown as HydratedMessage<[Expectation.Channel, ...Expectation[]]>; + message.channel = await client.channels.get(data.channel_id); } else { throw new Error(`Invalid expectation requested for Message: ${expectation}`); } diff --git a/src/types/channel.ts b/src/types/channel.ts index 5736741..aadaa12 100644 --- a/src/types/channel.ts +++ b/src/types/channel.ts @@ -1,3 +1,5 @@ +import { Trait } from "./common"; + export class Channel { version: number; type: number; @@ -33,7 +35,45 @@ export class Channel { this.last_pin_timestamp = data.last_pin_timestamp; } + static [Trait.fromDiscord](data: DiscordChannel): Channel { + return new Channel({ + version: data.version, + type: data.type, + position: data.position, + permission_overwrites: data.permission_overwrites, + name: data.name, + id: data.id, + flags: data.flags, + topic: data.topic, + rate_limit_per_user: data.rate_limit_per_user, + parent_id: data.parent_id, + last_message_id: data.last_message_id, + user_limit: data.user_limit, + rtc_region: data.rtc_region, + bitrate: data.bitrate, + last_pin_timestamp: data.last_pin_timestamp + }); + } + static fromAPIResponse(data: any): Channel { return new Channel(data); } +} + +export type DiscordChannel = { + version: number; + type: number; + position: number; + permission_overwrites: any[]; + name: string; + id: string; + flags: number; + topic?: string | null; + rate_limit_per_user?: number; + parent_id?: string; + last_message_id?: string | null; + user_limit?: number; + rtc_region?: string | null; + bitrate?: number; + last_pin_timestamp?: string; } \ No newline at end of file diff --git a/src/types/message.ts b/src/types/message.ts index 878bb97..43cbe68 100644 --- a/src/types/message.ts +++ b/src/types/message.ts @@ -1,9 +1,10 @@ import { exists, isNil } from '../utils'; 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 { Guild } from './guild'; -import { type DiscordUser, User } from './user'; +import { User, type DiscordUser } from './user'; export type Component = Partial