From 0da67319f3e6a753b95f132e19cd14cae9c6bf81 Mon Sep 17 00:00:00 2001 From: MARCROCK22 Date: Sat, 1 Jun 2024 20:14:40 +0000 Subject: [PATCH] fix: cache flow --- src/cache/index.ts | 1150 ++++++++++++++++++------------------ src/client/client.ts | 454 +++++++------- src/client/workerclient.ts | 805 ++++++++++++------------- src/events/handler.ts | 265 +++++---- 4 files changed, 1315 insertions(+), 1359 deletions(-) diff --git a/src/cache/index.ts b/src/cache/index.ts index df53c45..a67fda5 100644 --- a/src/cache/index.ts +++ b/src/cache/index.ts @@ -1,575 +1,575 @@ -import type { If } from '../common'; - -import type { Adapter } from './adapters'; - -import { Guilds } from './resources/guilds'; -import { Users } from './resources/users'; - -import { Channels } from './resources/channels'; -import { Emojis } from './resources/emojis'; -import { Members } from './resources/members'; -import { Presences } from './resources/presence'; -import { Roles } from './resources/roles'; -import { StageInstances } from './resources/stage-instances'; -import { Stickers } from './resources/stickers'; -import { Threads } from './resources/threads'; -import { VoiceStates } from './resources/voice-states'; - -import { ChannelType, GatewayIntentBits, type GatewayDispatchPayload } from 'discord-api-types/v10'; -import type { InternalOptions, UsingClient } from '../commands'; -import { Overwrites } from './resources/overwrites'; -import { Messages } from './resources/messages'; - -export type InferAsyncCache = InternalOptions extends { asyncCache: infer P } ? P : false; -export type ReturnCache = If, T>; - -// GuildBased -export type GuildBased = 'members' | 'voiceStates'; - -// ClientGuildBased -export type GuildRelated = - | 'emojis' - | 'roles' - | 'threads' - | 'channels' - | 'stickers' - | 'presences' - | 'stageInstances' - | 'overwrites' - | 'messages'; - -// ClientBased -export type NonGuildBased = 'users' | 'guilds'; - -type ReturnManagers = { - [K in NonGuildBased | GuildBased | GuildRelated]: NonNullable['get']>>>; -}; - -export * from './adapters/index'; - -export type CachedEvents = - | 'READY' - | 'GUILD_CREATE' - | 'GUILD_UPDATE' - | 'GUILD_DELETE' - | 'CHANNEL_CREATE' - | 'CHANNEL_UPDATE' - | 'CHANNEL_DELETE' - | 'GUILD_ROLE_CREATE' - | 'GUILD_ROLE_UPDATE' - | 'GUILD_ROLE_DELETE' - | 'GUILD_EMOJIS_UPDATE' - | 'GUILD_STICKERS_UPDATE' - | 'GUILD_MEMBER_ADD' - | 'GUILD_MEMBER_UPDATE' - | 'GUILD_MEMBER_REMOVE' - | 'MESSAGE_CREATE' - | 'PRESENCE_UPDATE' - | 'THREAD_DELETE' - | 'THREAD_CREATE' - | 'THREAD_UPDATE' - | 'USER_UPDATE' - | 'VOICE_STATE_UPDATE' - | 'STAGE_INSTANCE_CREATE' - | 'STAGE_INSTANCE_UPDATE' - | 'STAGE_INSTANCE_DELETE'; - -export class Cache { - // non-guild based - users?: Users; - guilds?: Guilds; - - // guild based - members?: Members; - voiceStates?: VoiceStates; - - // guild related - overwrites?: Overwrites; - roles?: Roles; - emojis?: Emojis; - threads?: Threads; - channels?: Channels; - stickers?: Stickers; - presences?: Presences; - stageInstances?: StageInstances; - messages?: Messages; - - constructor( - public intents: number, - public adapter: Adapter, - readonly disabledCache: (NonGuildBased | GuildBased | GuildRelated)[] = [], - client?: UsingClient, - ) { - // non-guild based - if (!this.disabledCache.includes('users')) { - this.users = new Users(this, client); - } - if (!this.disabledCache.includes('guilds')) { - this.guilds = new Guilds(this, client); - } - - // guild related - if (!this.disabledCache.includes('members')) { - this.members = new Members(this, client); - } - if (!this.disabledCache.includes('voiceStates')) { - this.voiceStates = new VoiceStates(this, client); - } - - // guild based - if (!this.disabledCache.includes('roles')) { - this.roles = new Roles(this, client); - } - if (!this.disabledCache.includes('overwrites')) { - this.overwrites = new Overwrites(this, client); - } - if (!this.disabledCache.includes('channels')) { - this.channels = new Channels(this, client); - } - if (!this.disabledCache.includes('emojis')) { - this.emojis = new Emojis(this, client); - } - if (!this.disabledCache.includes('stickers')) { - this.stickers = new Stickers(this, client); - } - if (!this.disabledCache.includes('presences')) { - this.presences = new Presences(this, client); - } - if (!this.disabledCache.includes('threads')) { - this.threads = new Threads(this, client); - } - if (!this.disabledCache.includes('stageInstances')) { - this.stageInstances = new StageInstances(this, client); - } - if (!this.disabledCache.includes('messages')) { - this.messages = new Messages(this, client); - } - } - - /** @internal */ - __setClient(client: UsingClient) { - this.users?.__setClient(client); - this.guilds?.__setClient(client); - - this.members?.__setClient(client); - this.voiceStates?.__setClient(client); - - this.roles?.__setClient(client); - this.overwrites?.__setClient(client); - this.channels?.__setClient(client); - this.emojis?.__setClient(client); - this.stickers?.__setClient(client); - this.presences?.__setClient(client); - this.threads?.__setClient(client); - this.stageInstances?.__setClient(client); - this.messages?.__setClient(client); - } - - flush(): ReturnCache { - return this.adapter.flush() as void; - } - - // internal use ./structures - hasIntent(intent: keyof typeof GatewayIntentBits) { - return (this.intents & GatewayIntentBits[intent]) === GatewayIntentBits[intent]; - } - - get hasGuildsIntent() { - return this.hasIntent('Guilds'); - } - - get hasRolesIntent() { - return this.hasGuildsIntent; - } - - get hasChannelsIntent() { - return this.hasGuildsIntent; - } - - get hasGuildMembersIntent() { - return this.hasIntent('GuildMembers'); - } - - get hasEmojisAndStickersIntent() { - return this.hasIntent('GuildEmojisAndStickers'); - } - - get hasVoiceStatesIntent() { - return this.hasIntent('GuildVoiceStates'); - } - - get hasPrenseceUpdates() { - return this.hasIntent('GuildPresences'); - } - - get hasDirectMessages() { - return this.hasIntent('DirectMessages'); - } - - async bulkGet( - keys: ( - | readonly [ - /* type */ - NonGuildBased | GuildRelated, - /* source id */ - string, - ] - | readonly [ - /* type */ - GuildBased, - /* source id */ - string, - /* guild id */ - string, - ] - )[], - ) { - const allData: Partial> = {}; - for (const [type, id, guildId] of keys) { - switch (type) { - case 'voiceStates': - case 'members': - { - if (!allData[type]) { - allData[type] = []; - } - allData[type]!.push([id, guildId]); - } - break; - case 'roles': - case 'threads': - case 'stickers': - case 'channels': - case 'presences': - case 'stageInstances': - case 'emojis': - case 'users': - case 'guilds': - case 'overwrites': - case 'messages': - { - if (!allData[type]) { - allData[type] = []; - } - allData[type]!.push([id]); - } - break; - default: - throw new Error(`Invalid type ${type}`); - } - } - - const obj: Partial<{ - [K in keyof ReturnManagers]: ReturnManagers[K][]; - }> = {}; - - for (const i in allData) { - const key = i as NonGuildBased | GuildBased | GuildRelated; - const values = allData[key]!; - obj[key] = []; - for (const value of values) { - const g = await this[key]?.get(value[0], value[1]); - if (!g) { - continue; - } - obj[key]!.push(g as never); - } - } - - return obj; - } - - async bulkPatch( - keys: ( - | readonly [ - /* type */ - NonGuildBased, - /* data */ - any, - /* source id */ - string, - ] - | readonly [ - /* type */ - GuildBased | GuildRelated, - /* data */ - any, - /* source id */ - string, - /* guild id */ - string, - ] - )[], - ) { - const allData: [string, any][] = []; - const relationshipsData: Record = {}; - for (const [type, data, id, guildId] of keys) { - switch (type) { - case 'roles': - case 'threads': - case 'stickers': - case 'channels': - case 'presences': - case 'stageInstances': - case 'emojis': - case 'overwrites': - case 'messages': - { - if (!this[type]?.filter(data, id, guildId)) continue; - const hashId = this[type]?.hashId(guildId!); - if (!hashId) { - continue; - } - if (!(hashId in relationshipsData)) { - relationshipsData[hashId] = []; - } - relationshipsData[hashId].push(id); - if (type !== 'overwrites') { - data.guild_id = guildId; - } - allData.push([this[type]!.hashId(id), this[type]!.parse(data, id, guildId!)]); - } - break; - case 'voiceStates': - case 'members': - { - if (!this[type]?.filter(data, id, guildId)) continue; - const hashId = this[type]?.hashId(guildId!); - if (!hashId) { - continue; - } - if (!(hashId in relationshipsData)) { - relationshipsData[hashId] = []; - } - relationshipsData[hashId].push(id); - data.guild_id = guildId; - allData.push([this[type]!.hashGuildId(guildId, id), this[type]!.parse(data, id, guildId!)]); - } - break; - case 'users': - case 'guilds': - { - if (!this[type]?.filter(data, id)) continue; - const hashId = this[type]?.namespace; - if (!hashId) { - continue; - } - if (!(hashId in relationshipsData)) { - relationshipsData[hashId] = []; - } - relationshipsData[hashId].push(id); - allData.push([this[type]!.hashId(id), data]); - } - break; - default: - throw new Error(`Invalid type ${type}`); - } - } - - await this.adapter.bulkAddToRelationShip(relationshipsData); - await this.adapter.patch(false, allData); - } - - async bulkSet( - keys: ( - | readonly [ - /* type */ - NonGuildBased, - /* data */ - any, - /* source id */ - string, - ] - | readonly [ - /* type */ - GuildBased | GuildRelated, - /* data */ - any, - /* source id */ - string, - /* guild id */ - string, - ] - )[], - ) { - const allData: [string, any][] = []; - const relationshipsData: Record = {}; - for (const [type, data, id, guildId] of keys) { - switch (type) { - case 'roles': - case 'threads': - case 'stickers': - case 'channels': - case 'presences': - case 'stageInstances': - case 'emojis': - case 'overwrites': - case 'messages': - { - if (!this[type]?.filter(data, id, guildId)) continue; - const hashId = this[type]?.hashId(guildId!); - if (!hashId) { - continue; - } - if (!(hashId in relationshipsData)) { - relationshipsData[hashId] = []; - } - relationshipsData[hashId].push(id); - if (type !== 'overwrites') { - data.guild_id = guildId; - } - allData.push([this[type]!.hashId(id), this[type]!.parse(data, id, guildId!)]); - } - break; - case 'voiceStates': - case 'members': - { - if (!this[type]?.filter(data, id, guildId)) continue; - const hashId = this[type]?.hashId(guildId!); - if (!hashId) { - continue; - } - if (!(hashId in relationshipsData)) { - relationshipsData[hashId] = []; - } - relationshipsData[hashId].push(id); - data.guild_id = guildId; - allData.push([this[type]!.hashGuildId(guildId, id), this[type]!.parse(data, id, guildId!)]); - } - break; - case 'users': - case 'guilds': - { - if (!this[type]?.filter(data, id)) continue; - const hashId = this[type]?.namespace; - if (!hashId) { - continue; - } - if (!(hashId in relationshipsData)) { - relationshipsData[hashId] = []; - } - relationshipsData[hashId].push(id); - allData.push([this[type]!.hashId(id), data]); - } - break; - default: - throw new Error(`Invalid type ${type}`); - } - } - - await this.adapter.bulkAddToRelationShip(relationshipsData); - await this.adapter.set(allData); - } - - async onPacket(event: GatewayDispatchPayload) { - switch (event.t) { - case 'READY': - await this.users?.set(event.d.user.id, event.d.user); - break; - case 'GUILD_CREATE': - case 'GUILD_UPDATE': - await this.guilds?.patch(event.d.id, { unavailable: false, ...event.d }); - break; - case 'GUILD_DELETE': - if (event.d.unavailable) { - await this.guilds?.patch(event.d.id, event.d); - } else { - await this.guilds?.remove(event.d.id); - } - break; - case 'CHANNEL_CREATE': - case 'CHANNEL_UPDATE': - if ('guild_id' in event.d) { - await this.channels?.set(event.d.id, event.d.guild_id!, event.d); - if (event.d.permission_overwrites?.length) - await this.overwrites?.set(event.d.id, event.d.guild_id!, event.d.permission_overwrites); - break; - } - if (event.d.type === ChannelType.DM) { - await this.channels?.set(event.d.recipients![0]?.id, '@me', event.d); - break; - } - break; - case 'CHANNEL_DELETE': - await this.channels?.remove(event.d.id, 'guild_id' in event.d ? event.d.guild_id! : '@me'); - break; - case 'GUILD_ROLE_CREATE': - case 'GUILD_ROLE_UPDATE': - await this.roles?.set(event.d.role.id, event.d.guild_id, event.d.role); - break; - case 'GUILD_ROLE_DELETE': - await this.roles?.remove(event.d.role_id, event.d.guild_id); - break; - case 'GUILD_EMOJIS_UPDATE': - await this.emojis?.remove(await this.emojis?.keys(event.d.guild_id), event.d.guild_id); - await this.emojis?.set( - event.d.emojis.map(x => [x.id!, x]), - event.d.guild_id, - ); - break; - case 'GUILD_STICKERS_UPDATE': - await this.stickers?.remove(await this.stickers?.keys(event.d.guild_id), event.d.guild_id); - await this.stickers?.set( - event.d.stickers.map(x => [x.id, x] as const), - event.d.guild_id, - ); - break; - case 'GUILD_MEMBER_ADD': - case 'GUILD_MEMBER_UPDATE': - if (event.d.user) await this.members?.set(event.d.user.id, event.d.guild_id, event.d); - break; - case 'GUILD_MEMBER_REMOVE': - await this.members?.remove(event.d.user.id, event.d.guild_id); - break; - - case 'PRESENCE_UPDATE': - // Should update member data? - await this.presences?.set(event.d.user.id, event.d.guild_id, event.d); - break; - - case 'THREAD_CREATE': - case 'THREAD_UPDATE': - if (event.d.guild_id) await this.threads?.set(event.d.id, event.d.guild_id, event.d); - break; - - case 'THREAD_DELETE': - await this.threads?.remove(event.d.id, event.d.guild_id); - break; - - case 'USER_UPDATE': - await this.users?.set(event.d.id, event.d); - break; - - case 'VOICE_STATE_UPDATE': - if (!event.d.guild_id) { - return; - } - - if (event.d.channel_id != null) { - await this.voiceStates?.set(event.d.user_id, event.d.guild_id, event.d); - } else { - await this.voiceStates?.remove(event.d.user_id, event.d.guild_id); - } - break; - case 'STAGE_INSTANCE_CREATE': - case 'STAGE_INSTANCE_UPDATE': - await this.stageInstances?.set(event.d.id, event.d.guild_id, event.d); - break; - case 'STAGE_INSTANCE_DELETE': - await this.stageInstances?.remove(event.d.id, event.d.guild_id); - break; - case 'MESSAGE_CREATE': - await this.messages?.set(event.d.id, event.d.channel_id, event.d); - break; - case 'MESSAGE_UPDATE': - await this.messages?.patch(event.d.id, event.d.channel_id, event.d); - break; - case 'MESSAGE_DELETE': - await this.messages?.remove(event.d.id, event.d.channel_id); - break; - case 'MESSAGE_DELETE_BULK': - await this.messages?.remove(event.d.ids, event.d.channel_id); - break; - } - } -} +import type { If } from '../common'; + +import type { Adapter } from './adapters'; + +import { Guilds } from './resources/guilds'; +import { Users } from './resources/users'; + +import { Channels } from './resources/channels'; +import { Emojis } from './resources/emojis'; +import { Members } from './resources/members'; +import { Presences } from './resources/presence'; +import { Roles } from './resources/roles'; +import { StageInstances } from './resources/stage-instances'; +import { Stickers } from './resources/stickers'; +import { Threads } from './resources/threads'; +import { VoiceStates } from './resources/voice-states'; + +import { ChannelType, GatewayIntentBits, type GatewayDispatchPayload } from 'discord-api-types/v10'; +import type { InternalOptions, UsingClient } from '../commands'; +import { Overwrites } from './resources/overwrites'; +import { Messages } from './resources/messages'; + +export type InferAsyncCache = InternalOptions extends { asyncCache: infer P } ? P : false; +export type ReturnCache = If, T>; + +// GuildBased +export type GuildBased = 'members' | 'voiceStates'; + +// ClientGuildBased +export type GuildRelated = + | 'emojis' + | 'roles' + | 'threads' + | 'channels' + | 'stickers' + | 'presences' + | 'stageInstances' + | 'overwrites' + | 'messages'; + +// ClientBased +export type NonGuildBased = 'users' | 'guilds'; + +type ReturnManagers = { + [K in NonGuildBased | GuildBased | GuildRelated]: NonNullable['get']>>>; +}; + +export * from './adapters/index'; + +export type CachedEvents = + | 'READY' + | 'GUILD_CREATE' + | 'GUILD_UPDATE' + | 'GUILD_DELETE' + | 'CHANNEL_CREATE' + | 'CHANNEL_UPDATE' + | 'CHANNEL_DELETE' + | 'GUILD_ROLE_CREATE' + | 'GUILD_ROLE_UPDATE' + | 'GUILD_ROLE_DELETE' + | 'GUILD_EMOJIS_UPDATE' + | 'GUILD_STICKERS_UPDATE' + | 'GUILD_MEMBER_ADD' + | 'GUILD_MEMBER_UPDATE' + | 'GUILD_MEMBER_REMOVE' + | 'MESSAGE_CREATE' + | 'PRESENCE_UPDATE' + | 'THREAD_DELETE' + | 'THREAD_CREATE' + | 'THREAD_UPDATE' + | 'USER_UPDATE' + | 'VOICE_STATE_UPDATE' + | 'STAGE_INSTANCE_CREATE' + | 'STAGE_INSTANCE_UPDATE' + | 'STAGE_INSTANCE_DELETE'; + +export class Cache { + // non-guild based + users?: Users; + guilds?: Guilds; + + // guild based + members?: Members; + voiceStates?: VoiceStates; + + // guild related + overwrites?: Overwrites; + roles?: Roles; + emojis?: Emojis; + threads?: Threads; + channels?: Channels; + stickers?: Stickers; + presences?: Presences; + stageInstances?: StageInstances; + messages?: Messages; + + constructor( + public intents: number, + public adapter: Adapter, + readonly disabledCache: (NonGuildBased | GuildBased | GuildRelated)[] = [], + client?: UsingClient, + ) { + // non-guild based + if (!this.disabledCache.includes('users')) { + this.users = new Users(this, client); + } + if (!this.disabledCache.includes('guilds')) { + this.guilds = new Guilds(this, client); + } + + // guild related + if (!this.disabledCache.includes('members')) { + this.members = new Members(this, client); + } + if (!this.disabledCache.includes('voiceStates')) { + this.voiceStates = new VoiceStates(this, client); + } + + // guild based + if (!this.disabledCache.includes('roles')) { + this.roles = new Roles(this, client); + } + if (!this.disabledCache.includes('overwrites')) { + this.overwrites = new Overwrites(this, client); + } + if (!this.disabledCache.includes('channels')) { + this.channels = new Channels(this, client); + } + if (!this.disabledCache.includes('emojis')) { + this.emojis = new Emojis(this, client); + } + if (!this.disabledCache.includes('stickers')) { + this.stickers = new Stickers(this, client); + } + if (!this.disabledCache.includes('presences')) { + this.presences = new Presences(this, client); + } + if (!this.disabledCache.includes('threads')) { + this.threads = new Threads(this, client); + } + if (!this.disabledCache.includes('stageInstances')) { + this.stageInstances = new StageInstances(this, client); + } + if (!this.disabledCache.includes('messages')) { + this.messages = new Messages(this, client); + } + } + + /** @internal */ + __setClient(client: UsingClient) { + this.users?.__setClient(client); + this.guilds?.__setClient(client); + + this.members?.__setClient(client); + this.voiceStates?.__setClient(client); + + this.roles?.__setClient(client); + this.overwrites?.__setClient(client); + this.channels?.__setClient(client); + this.emojis?.__setClient(client); + this.stickers?.__setClient(client); + this.presences?.__setClient(client); + this.threads?.__setClient(client); + this.stageInstances?.__setClient(client); + this.messages?.__setClient(client); + } + + flush(): ReturnCache { + return this.adapter.flush() as void; + } + + // internal use ./structures + hasIntent(intent: keyof typeof GatewayIntentBits) { + return (this.intents & GatewayIntentBits[intent]) === GatewayIntentBits[intent]; + } + + get hasGuildsIntent() { + return this.hasIntent('Guilds'); + } + + get hasRolesIntent() { + return this.hasGuildsIntent; + } + + get hasChannelsIntent() { + return this.hasGuildsIntent; + } + + get hasGuildMembersIntent() { + return this.hasIntent('GuildMembers'); + } + + get hasEmojisAndStickersIntent() { + return this.hasIntent('GuildEmojisAndStickers'); + } + + get hasVoiceStatesIntent() { + return this.hasIntent('GuildVoiceStates'); + } + + get hasPrenseceUpdates() { + return this.hasIntent('GuildPresences'); + } + + get hasDirectMessages() { + return this.hasIntent('DirectMessages'); + } + + async bulkGet( + keys: ( + | readonly [ + /* type */ + NonGuildBased | GuildRelated, + /* source id */ + string, + ] + | readonly [ + /* type */ + GuildBased, + /* source id */ + string, + /* guild id */ + string, + ] + )[], + ) { + const allData: Partial> = {}; + for (const [type, id, guildId] of keys) { + switch (type) { + case 'voiceStates': + case 'members': + { + if (!allData[type]) { + allData[type] = []; + } + allData[type]!.push([id, guildId]); + } + break; + case 'roles': + case 'threads': + case 'stickers': + case 'channels': + case 'presences': + case 'stageInstances': + case 'emojis': + case 'users': + case 'guilds': + case 'overwrites': + case 'messages': + { + if (!allData[type]) { + allData[type] = []; + } + allData[type]!.push([id]); + } + break; + default: + throw new Error(`Invalid type ${type}`); + } + } + + const obj: Partial<{ + [K in keyof ReturnManagers]: ReturnManagers[K][]; + }> = {}; + + for (const i in allData) { + const key = i as NonGuildBased | GuildBased | GuildRelated; + const values = allData[key]!; + obj[key] = []; + for (const value of values) { + const g = await this[key]?.get(value[0], value[1]); + if (!g) { + continue; + } + obj[key]!.push(g as never); + } + } + + return obj; + } + + async bulkPatch( + keys: ( + | readonly [ + /* type */ + NonGuildBased, + /* data */ + any, + /* source id */ + string, + ] + | readonly [ + /* type */ + GuildBased | GuildRelated, + /* data */ + any, + /* source id */ + string, + /* guild id */ + string, + ] + )[], + ) { + const allData: [string, any][] = []; + const relationshipsData: Record = {}; + for (const [type, data, id, guildId] of keys) { + switch (type) { + case 'roles': + case 'threads': + case 'stickers': + case 'channels': + case 'presences': + case 'stageInstances': + case 'emojis': + case 'overwrites': + case 'messages': + { + if (!this[type]?.filter(data, id, guildId)) continue; + const hashId = this[type]?.hashId(guildId!); + if (!hashId) { + continue; + } + if (!(hashId in relationshipsData)) { + relationshipsData[hashId] = []; + } + relationshipsData[hashId].push(id); + if (type !== 'overwrites') { + data.guild_id = guildId; + } + allData.push([this[type]!.hashId(id), this[type]!.parse(data, id, guildId!)]); + } + break; + case 'voiceStates': + case 'members': + { + if (!this[type]?.filter(data, id, guildId)) continue; + const hashId = this[type]?.hashId(guildId!); + if (!hashId) { + continue; + } + if (!(hashId in relationshipsData)) { + relationshipsData[hashId] = []; + } + relationshipsData[hashId].push(id); + data.guild_id = guildId; + allData.push([this[type]!.hashGuildId(guildId, id), this[type]!.parse(data, id, guildId!)]); + } + break; + case 'users': + case 'guilds': + { + if (!this[type]?.filter(data, id)) continue; + const hashId = this[type]?.namespace; + if (!hashId) { + continue; + } + if (!(hashId in relationshipsData)) { + relationshipsData[hashId] = []; + } + relationshipsData[hashId].push(id); + allData.push([this[type]!.hashId(id), data]); + } + break; + default: + throw new Error(`Invalid type ${type}`); + } + } + + await this.adapter.bulkAddToRelationShip(relationshipsData); + await this.adapter.patch(false, allData); + } + + async bulkSet( + keys: ( + | readonly [ + /* type */ + NonGuildBased, + /* data */ + any, + /* source id */ + string, + ] + | readonly [ + /* type */ + GuildBased | GuildRelated, + /* data */ + any, + /* source id */ + string, + /* guild id */ + string, + ] + )[], + ) { + const allData: [string, any][] = []; + const relationshipsData: Record = {}; + for (const [type, data, id, guildId] of keys) { + switch (type) { + case 'roles': + case 'threads': + case 'stickers': + case 'channels': + case 'presences': + case 'stageInstances': + case 'emojis': + case 'overwrites': + case 'messages': + { + if (!this[type]?.filter(data, id, guildId)) continue; + const hashId = this[type]?.hashId(guildId!); + if (!hashId) { + continue; + } + if (!(hashId in relationshipsData)) { + relationshipsData[hashId] = []; + } + relationshipsData[hashId].push(id); + if (type !== 'overwrites') { + data.guild_id = guildId; + } + allData.push([this[type]!.hashId(id), this[type]!.parse(data, id, guildId!)]); + } + break; + case 'voiceStates': + case 'members': + { + if (!this[type]?.filter(data, id, guildId)) continue; + const hashId = this[type]?.hashId(guildId!); + if (!hashId) { + continue; + } + if (!(hashId in relationshipsData)) { + relationshipsData[hashId] = []; + } + relationshipsData[hashId].push(id); + data.guild_id = guildId; + allData.push([this[type]!.hashGuildId(guildId, id), this[type]!.parse(data, id, guildId!)]); + } + break; + case 'users': + case 'guilds': + { + if (!this[type]?.filter(data, id)) continue; + const hashId = this[type]?.namespace; + if (!hashId) { + continue; + } + if (!(hashId in relationshipsData)) { + relationshipsData[hashId] = []; + } + relationshipsData[hashId].push(id); + allData.push([this[type]!.hashId(id), data]); + } + break; + default: + throw new Error(`Invalid type ${type}`); + } + } + + await this.adapter.bulkAddToRelationShip(relationshipsData); + await this.adapter.set(allData); + } + + async onPacket(event: GatewayDispatchPayload) { + switch (event.t) { + case 'READY': + await this.users?.set(event.d.user.id, event.d.user); + break; + case 'GUILD_CREATE': + case 'GUILD_UPDATE': + await this.guilds?.patch(event.d.id, { unavailable: false, ...event.d }); + break; + case 'GUILD_DELETE': + if (event.d.unavailable) { + await this.guilds?.patch(event.d.id, event.d); + } else { + await this.guilds?.remove(event.d.id); + } + break; + case 'CHANNEL_CREATE': + case 'CHANNEL_UPDATE': + if ('guild_id' in event.d) { + await this.channels?.set(event.d.id, event.d.guild_id!, event.d); + if (event.d.permission_overwrites?.length) + await this.overwrites?.set(event.d.id, event.d.guild_id!, event.d.permission_overwrites); + break; + } + if (event.d.type === ChannelType.DM) { + await this.channels?.set(event.d.recipients![0]?.id, '@me', event.d); + break; + } + break; + case 'CHANNEL_DELETE': + await this.channels?.remove(event.d.id, 'guild_id' in event.d ? event.d.guild_id! : '@me'); + break; + case 'GUILD_ROLE_CREATE': + case 'GUILD_ROLE_UPDATE': + await this.roles?.set(event.d.role.id, event.d.guild_id, event.d.role); + break; + case 'GUILD_ROLE_DELETE': + await this.roles?.remove(event.d.role_id, event.d.guild_id); + break; + case 'GUILD_EMOJIS_UPDATE': + await this.emojis?.remove(await this.emojis?.keys(event.d.guild_id), event.d.guild_id); + await this.emojis?.set( + event.d.emojis.map(x => [x.id!, x]), + event.d.guild_id, + ); + break; + case 'GUILD_STICKERS_UPDATE': + await this.stickers?.remove(await this.stickers?.keys(event.d.guild_id), event.d.guild_id); + await this.stickers?.set( + event.d.stickers.map(x => [x.id, x] as const), + event.d.guild_id, + ); + break; + case 'GUILD_MEMBER_ADD': + case 'GUILD_MEMBER_UPDATE': + if (event.d.user) await this.members?.set(event.d.user.id, event.d.guild_id, event.d); + break; + case 'GUILD_MEMBER_REMOVE': + await this.members?.remove(event.d.user.id, event.d.guild_id); + break; + + case 'PRESENCE_UPDATE': + // Should update member data? + await this.presences?.set(event.d.user.id, event.d.guild_id, event.d); + break; + + case 'THREAD_CREATE': + case 'THREAD_UPDATE': + if (event.d.guild_id) await this.threads?.set(event.d.id, event.d.guild_id, event.d); + break; + + case 'THREAD_DELETE': + await this.threads?.remove(event.d.id, event.d.guild_id); + break; + + case 'USER_UPDATE': + await this.users?.set(event.d.id, event.d); + break; + + case 'VOICE_STATE_UPDATE': + if (!event.d.guild_id) { + return; + } + + if (event.d.channel_id != null) { + await this.voiceStates?.set(event.d.user_id, event.d.guild_id, event.d); + } else { + await this.voiceStates?.remove(event.d.user_id, event.d.guild_id); + } + break; + case 'STAGE_INSTANCE_CREATE': + case 'STAGE_INSTANCE_UPDATE': + await this.stageInstances?.set(event.d.id, event.d.guild_id, event.d); + break; + case 'STAGE_INSTANCE_DELETE': + await this.stageInstances?.remove(event.d.id, event.d.guild_id); + break; + case 'MESSAGE_CREATE': + await this.messages?.set(event.d.id, event.d.channel_id, event.d); + break; + case 'MESSAGE_UPDATE': + await this.messages?.patch(event.d.id, event.d.channel_id, event.d); + break; + case 'MESSAGE_DELETE': + await this.messages?.remove(event.d.id, event.d.channel_id); + break; + case 'MESSAGE_DELETE_BULK': + await this.messages?.remove(event.d.ids, event.d.channel_id); + break; + } + } +} diff --git a/src/client/client.ts b/src/client/client.ts index bb63c2b..f382971 100644 --- a/src/client/client.ts +++ b/src/client/client.ts @@ -1,237 +1,217 @@ -import { GatewayIntentBits, type GatewayDispatchPayload, type GatewayPresenceUpdateData } from 'discord-api-types/v10'; -import type { Command, CommandContext, Message, SubCommand } from '..'; -import { lazyLoadPackage, type DeepPartial, type If, type WatcherPayload, type WatcherSendToShard } from '../common'; -import { EventHandler } from '../events'; -import { ClientUser } from '../structures'; -import { ShardManager, properties, type ShardManagerOptions } from '../websocket'; -import { MemberUpdateHandler } from '../websocket/discord/events/memberUpdate'; -import { PresenceUpdateHandler } from '../websocket/discord/events/presenceUpdate'; -import type { BaseClientOptions, InternalRuntimeConfig, ServicesOptions, StartOptions } from './base'; -import { BaseClient } from './base'; -import { onInteractionCreate } from './oninteractioncreate'; -import { onMessageCreate } from './onmessagecreate'; -import { Collectors } from './collectors'; - -let parentPort: import('node:worker_threads').MessagePort; - -export class Client extends BaseClient { - private __handleGuilds?: Set = new Set(); - gateway!: ShardManager; - me!: If; - declare options: ClientOptions; - memberUpdateHandler = new MemberUpdateHandler(); - presenceUpdateHandler = new PresenceUpdateHandler(); - collectors = new Collectors(); - events? = new EventHandler(this.logger, this.collectors); - - constructor(options?: ClientOptions) { - super(options); - } - - setServices({ - gateway, - ...rest - }: ServicesOptions & { - gateway?: ShardManager; - handlers?: ServicesOptions['handlers'] & { - events?: EventHandler['callback']; - }; - }) { - super.setServices(rest); - if (gateway) { - const onPacket = this.onPacket.bind(this); - const oldFn = gateway.options.handlePayload; - gateway.options.handlePayload = async (shardId, packet) => { - await onPacket(shardId, packet); - return oldFn(shardId, packet); - }; - this.gateway = gateway; - } - if (rest.handlers && 'events' in rest.handlers) { - if (!rest.handlers.events) { - this.events = undefined; - } else if (typeof rest.handlers.events === 'function') { - this.events = new EventHandler(this.logger, this.collectors); - this.events.setHandlers({ - callback: rest.handlers.events, - }); - } else { - this.events = rest.handlers.events; - } - } - } - - async loadEvents(dir?: string) { - dir ??= await this.getRC().then(x => x.events); - if (dir && this.events) { - await this.events.load(dir); - this.logger.info('EventHandler loaded'); - } - } - - protected async execute(options: { token?: string; intents?: number } = {}) { - await super.execute(options); - - const worker_threads = lazyLoadPackage('node:worker_threads'); - - if (worker_threads?.parentPort) { - parentPort = worker_threads.parentPort; - } - - if (worker_threads?.workerData?.__USING_WATCHER__) { - parentPort?.on('message', (data: WatcherPayload | WatcherSendToShard) => { - switch (data.type) { - case 'PAYLOAD': - this.gateway.options.handlePayload(data.shardId, data.payload); - break; - case 'SEND_TO_SHARD': - this.gateway.send(data.shardId, data.payload); - break; - } - }); - } else { - await this.gateway.spawnShards(); - } - } - - async start(options: Omit, 'httpConnection'> = {}, execute = true) { - await super.start(options); - await this.loadEvents(options.eventsDir); - - const { token: tokenRC, intents: intentsRC, debug: debugRC } = await this.getRC(); - const token = options?.token ?? tokenRC; - const intents = options?.connection?.intents ?? intentsRC; - - if (!this.gateway) { - BaseClient.assertString(token, 'token is not a string'); - this.gateway = new ShardManager({ - token, - info: await this.proxy.gateway.bot.get(), - intents, - handlePayload: async (shardId, packet) => { - await this.options?.handlePayload?.(shardId, packet); - return this.onPacket(shardId, packet); - }, - presence: this.options?.presence, - debug: debugRC, - shardStart: this.options?.shards?.start, - shardEnd: this.options?.shards?.end ?? this.options?.shards?.total, - totalShards: this.options?.shards?.total ?? this.options?.shards?.end, - properties: { ...this.options?.gateway?.properties, ...properties }, - compress: this.options?.gateway?.compress, - }); - } - - this.cache.intents = this.gateway.options.intents; - - if (execute) { - await this.execute(options.connection); - } else { - await super.execute(options); - } - } - - protected async onPacket(shardId: number, packet: GatewayDispatchPayload) { - await this.events?.runEvent('RAW', this, packet, shardId); - switch (packet.t) { - //// Cases where we must obtain the old data before updating - case 'GUILD_MEMBER_UPDATE': - if (!this.memberUpdateHandler.check(packet.d)) { - return; - } - await this.events?.execute(packet.t, packet, this as Client, shardId); - await this.cache.onPacket(packet); - break; - case 'PRESENCE_UPDATE': - if (!this.presenceUpdateHandler.check(packet.d as any)) { - return; - } - await this.events?.execute(packet.t, packet, this as Client, shardId); - await this.cache.onPacket(packet); - break; - - case 'MESSAGE_UPDATE': - case 'MESSAGE_DELETE_BULK': - case 'MESSAGE_DELETE': - case 'GUILD_DELETE': - case 'CHANNEL_UPDATE': - case 'GUILD_EMOJIS_UPDATE': - case 'GUILD_UPDATE': - case 'GUILD_ROLE_UPDATE': - case 'GUILD_ROLE_DELETE': - case 'THREAD_UPDATE': - case 'USER_UPDATE': - case 'VOICE_STATE_UPDATE': - case 'STAGE_INSTANCE_UPDATE': - case 'GUILD_STICKERS_UPDATE': - await this.events?.execute(packet.t, packet, this as Client, shardId); - await this.cache.onPacket(packet); - break; - //rest of the events - default: { - await this.cache.onPacket(packet); - await this.events?.execute(packet.t, packet, this as Client, shardId); - switch (packet.t) { - case 'INTERACTION_CREATE': - await onInteractionCreate(this, packet.d, shardId); - break; - case 'MESSAGE_CREATE': - await onMessageCreate(this, packet.d, shardId); - break; - case 'READY': - for (const g of packet.d.guilds) { - this.__handleGuilds?.add(g.id); - } - this.botId = packet.d.user.id; - this.applicationId = packet.d.application.id; - this.me = new ClientUser(this, packet.d.user, packet.d.application) as never; - if ( - !( - this.__handleGuilds?.size && - (this.gateway.options.intents & GatewayIntentBits.Guilds) === GatewayIntentBits.Guilds - ) - ) { - if ([...this.gateway.values()].every(shard => shard.data.session_id)) { - await this.events?.runEvent('BOT_READY', this, this.me, -1); - } - delete this.__handleGuilds; - } - this.debugger?.debug(`#${shardId}[${packet.d.user.username}](${this.botId}) is online...`); - break; - case 'GUILD_CREATE': { - if (this.__handleGuilds?.has(packet.d.id)) { - this.__handleGuilds.delete(packet.d.id); - if (!this.__handleGuilds.size && [...this.gateway.values()].every(shard => shard.data.session_id)) { - await this.events?.runEvent('BOT_READY', this, this.me, -1); - } - if (!this.__handleGuilds.size) delete this.__handleGuilds; - return; - } - break; - } - } - break; - } - } - } -} - -export interface ClientOptions extends BaseClientOptions { - presence?: (shardId: number) => GatewayPresenceUpdateData; - shards?: { - start: number; - end: number; - total?: number; - }; - gateway?: { - properties?: Partial; - compress?: ShardManagerOptions['compress']; - }; - commands?: BaseClientOptions['commands'] & { - prefix?: (message: Message) => Promise | string[]; - deferReplyResponse?: (ctx: CommandContext) => Parameters[0]; - reply?: (ctx: CommandContext) => boolean; - argsParser?: (content: string, command: SubCommand | Command, message: Message) => Record; - }; - handlePayload?: ShardManagerOptions['handlePayload']; -} +import { GatewayIntentBits, type GatewayDispatchPayload, type GatewayPresenceUpdateData } from 'discord-api-types/v10'; +import type { Command, CommandContext, Message, SubCommand } from '..'; +import { lazyLoadPackage, type DeepPartial, type If, type WatcherPayload, type WatcherSendToShard } from '../common'; +import { EventHandler } from '../events'; +import { ClientUser } from '../structures'; +import { ShardManager, properties, type ShardManagerOptions } from '../websocket'; +import { MemberUpdateHandler } from '../websocket/discord/events/memberUpdate'; +import { PresenceUpdateHandler } from '../websocket/discord/events/presenceUpdate'; +import type { BaseClientOptions, InternalRuntimeConfig, ServicesOptions, StartOptions } from './base'; +import { BaseClient } from './base'; +import { onInteractionCreate } from './oninteractioncreate'; +import { onMessageCreate } from './onmessagecreate'; +import { Collectors } from './collectors'; + +let parentPort: import('node:worker_threads').MessagePort; + +export class Client extends BaseClient { + private __handleGuilds?: Set = new Set(); + gateway!: ShardManager; + me!: If; + declare options: ClientOptions; + memberUpdateHandler = new MemberUpdateHandler(); + presenceUpdateHandler = new PresenceUpdateHandler(); + collectors = new Collectors(); + events? = new EventHandler(this); + + constructor(options?: ClientOptions) { + super(options); + } + + setServices({ + gateway, + ...rest + }: ServicesOptions & { + gateway?: ShardManager; + handlers?: ServicesOptions['handlers'] & { + events?: EventHandler['callback']; + }; + }) { + super.setServices(rest); + if (gateway) { + const onPacket = this.onPacket.bind(this); + const oldFn = gateway.options.handlePayload; + gateway.options.handlePayload = async (shardId, packet) => { + await onPacket(shardId, packet); + return oldFn(shardId, packet); + }; + this.gateway = gateway; + } + if (rest.handlers && 'events' in rest.handlers) { + if (!rest.handlers.events) { + this.events = undefined; + } else if (typeof rest.handlers.events === 'function') { + this.events = new EventHandler(this); + this.events.setHandlers({ + callback: rest.handlers.events, + }); + } else { + this.events = rest.handlers.events; + } + } + } + + async loadEvents(dir?: string) { + dir ??= await this.getRC().then(x => x.events); + if (dir && this.events) { + await this.events.load(dir); + this.logger.info('EventHandler loaded'); + } + } + + protected async execute(options: { token?: string; intents?: number } = {}) { + await super.execute(options); + + const worker_threads = lazyLoadPackage('node:worker_threads'); + + if (worker_threads?.parentPort) { + parentPort = worker_threads.parentPort; + } + + if (worker_threads?.workerData?.__USING_WATCHER__) { + parentPort?.on('message', (data: WatcherPayload | WatcherSendToShard) => { + switch (data.type) { + case 'PAYLOAD': + this.gateway.options.handlePayload(data.shardId, data.payload); + break; + case 'SEND_TO_SHARD': + this.gateway.send(data.shardId, data.payload); + break; + } + }); + } else { + await this.gateway.spawnShards(); + } + } + + async start(options: Omit, 'httpConnection'> = {}, execute = true) { + await super.start(options); + await this.loadEvents(options.eventsDir); + + const { token: tokenRC, intents: intentsRC, debug: debugRC } = await this.getRC(); + const token = options?.token ?? tokenRC; + const intents = options?.connection?.intents ?? intentsRC; + + if (!this.gateway) { + BaseClient.assertString(token, 'token is not a string'); + this.gateway = new ShardManager({ + token, + info: await this.proxy.gateway.bot.get(), + intents, + handlePayload: async (shardId, packet) => { + await this.options?.handlePayload?.(shardId, packet); + return this.onPacket(shardId, packet); + }, + presence: this.options?.presence, + debug: debugRC, + shardStart: this.options?.shards?.start, + shardEnd: this.options?.shards?.end ?? this.options?.shards?.total, + totalShards: this.options?.shards?.total ?? this.options?.shards?.end, + properties: { ...this.options?.gateway?.properties, ...properties }, + compress: this.options?.gateway?.compress, + }); + } + + this.cache.intents = this.gateway.options.intents; + + if (execute) { + await this.execute(options.connection); + } else { + await super.execute(options); + } + } + + protected async onPacket(shardId: number, packet: GatewayDispatchPayload) { + // await this.events?.runEvent('RAW', this, packet, shardId); + switch (packet.t) { + //// Cases where we must obtain the old data before updating + case 'GUILD_MEMBER_UPDATE': + if (!this.memberUpdateHandler.check(packet.d)) { + return; + } + await this.events?.execute(packet.t, packet, this as Client, shardId); + break; + case 'PRESENCE_UPDATE': + if (!this.presenceUpdateHandler.check(packet.d as any)) { + return; + } + await this.events?.execute(packet.t, packet, this as Client, shardId); + break; + + //rest of the events + default: { + await this.events?.execute(packet.t, packet, this as Client, shardId); + switch (packet.t) { + case 'INTERACTION_CREATE': + await onInteractionCreate(this, packet.d, shardId); + break; + case 'MESSAGE_CREATE': + await onMessageCreate(this, packet.d, shardId); + break; + case 'READY': + for (const g of packet.d.guilds) { + this.__handleGuilds?.add(g.id); + } + this.botId = packet.d.user.id; + this.applicationId = packet.d.application.id; + this.me = new ClientUser(this, packet.d.user, packet.d.application) as never; + if ( + !( + this.__handleGuilds?.size && + (this.gateway.options.intents & GatewayIntentBits.Guilds) === GatewayIntentBits.Guilds + ) + ) { + if ([...this.gateway.values()].every(shard => shard.data.session_id)) { + await this.events?.runEvent('BOT_READY', this, this.me, -1); + } + delete this.__handleGuilds; + } + this.debugger?.debug(`#${shardId}[${packet.d.user.username}](${this.botId}) is online...`); + break; + case 'GUILD_CREATE': { + if (this.__handleGuilds?.has(packet.d.id)) { + this.__handleGuilds.delete(packet.d.id); + if (!this.__handleGuilds.size && [...this.gateway.values()].every(shard => shard.data.session_id)) { + await this.events?.runEvent('BOT_READY', this, this.me, -1); + } + if (!this.__handleGuilds.size) delete this.__handleGuilds; + return; + } + break; + } + } + break; + } + } + } +} + +export interface ClientOptions extends BaseClientOptions { + presence?: (shardId: number) => GatewayPresenceUpdateData; + shards?: { + start: number; + end: number; + total?: number; + }; + gateway?: { + properties?: Partial; + compress?: ShardManagerOptions['compress']; + }; + commands?: BaseClientOptions['commands'] & { + prefix?: (message: Message) => Promise | string[]; + deferReplyResponse?: (ctx: CommandContext) => Parameters[0]; + reply?: (ctx: CommandContext) => boolean; + argsParser?: (content: string, command: SubCommand | Command, message: Message) => Record; + }; + handlePayload?: ShardManagerOptions['handlePayload']; +} diff --git a/src/client/workerclient.ts b/src/client/workerclient.ts index 7fbccf8..4a759fb 100644 --- a/src/client/workerclient.ts +++ b/src/client/workerclient.ts @@ -1,418 +1,387 @@ -import { GatewayIntentBits, type GatewayDispatchPayload, type GatewaySendPayload } from 'discord-api-types/v10'; -import { randomUUID } from 'node:crypto'; -import { ApiHandler, Logger } from '..'; -import type { Cache } from '../cache'; -import { WorkerAdapter } from '../cache'; -import { LogLevels, lazyLoadPackage, type DeepPartial, type When } from '../common'; -import { EventHandler } from '../events'; -import { ClientUser } from '../structures'; -import { Shard, type ShardManagerOptions, type WorkerData } from '../websocket'; -import type { - WorkerReady, - WorkerReceivePayload, - WorkerRequestConnect, - WorkerSendEval, - WorkerSendEvalResponse, - WorkerSendInfo, - WorkerSendResultPayload, - WorkerSendShardInfo, - WorkerShardInfo, - WorkerStart, -} from '../websocket/discord/worker'; -import type { ManagerMessages } from '../websocket/discord/workermanager'; -import type { BaseClientOptions, ServicesOptions, StartOptions } from './base'; -import { BaseClient } from './base'; -import type { Client } from './client'; -import { onInteractionCreate } from './oninteractioncreate'; -import { onMessageCreate } from './onmessagecreate'; -import { Collectors } from './collectors'; - -let workerData: WorkerData; -let manager: import('node:worker_threads').MessagePort; -try { - workerData = { - debug: process.env.SEYFERT_WORKER_DEBUG === 'true', - intents: Number.parseInt(process.env.SEYFERT_WORKER_INTENTS!), - path: process.env.SEYFERT_WORKER_PATH!, - shards: process.env.SEYFERT_WORKER_SHARDS!.split(',').map(id => Number.parseInt(id)), - token: process.env.SEYFERT_WORKER_TOKEN!, - workerId: Number.parseInt(process.env.SEYFERT_WORKER_WORKERID!), - workerProxy: process.env.SEYFERT_WORKER_WORKERPROXY === 'true', - } as WorkerData; -} catch {} - -export class WorkerClient extends BaseClient { - private __handleGuilds?: Set = new Set(); - logger = new Logger({ - name: `[Worker #${workerData.workerId}]`, - }); - - collectors = new Collectors(); - events? = new EventHandler(this.logger, this.collectors); - me!: When; - promises = new Map void; timeout: NodeJS.Timeout }>(); - - shards = new Map(); - - declare options: WorkerClientOptions; - - constructor(options?: WorkerClientOptions) { - super(options); - if (!process.env.SEYFERT_SPAWNING) { - throw new Error('WorkerClient cannot spawn without manager'); - } - this.postMessage({ - type: 'WORKER_START', - workerId: workerData.workerId, - } satisfies WorkerStart); - - const worker_threads = lazyLoadPackage('node:worker_threads'); - if (worker_threads?.parentPort) { - manager = worker_threads?.parentPort; - } - (manager ?? process).on('message', (data: ManagerMessages) => this.handleManagerMessages(data)); - - this.setServices({ - cache: { - adapter: new WorkerAdapter(workerData), - disabledCache: options?.disabledCache, - }, - }); - if (workerData.debug) { - this.debugger = new Logger({ - name: `[Worker #${workerData.workerId}]`, - logLevel: LogLevels.Debug, - }); - } - if (workerData.workerProxy) { - this.setServices({ - rest: new ApiHandler({ - token: workerData.token, - workerProxy: true, - debug: workerData.debug, - }), - }); - } - } - - get workerId() { - return workerData.workerId; - } - - get latency() { - let acc = 0; - - this.shards.forEach(s => (acc += s.latency)); - - return acc / this.shards.size; - } - - setServices({ - ...rest - }: ServicesOptions & { - handlers?: ServicesOptions['handlers'] & { - events?: EventHandler['callback']; - }; - }) { - super.setServices(rest); - if (rest.handlers && 'events' in rest.handlers) { - if (!rest.handlers.events) { - this.events = undefined; - } else if (typeof rest.handlers.events === 'function') { - this.events = new EventHandler(this.logger, this.collectors); - this.events.setHandlers({ - callback: rest.handlers.events, - }); - } else { - this.events = rest.handlers.events; - } - } - } - - async start(options: Omit, 'httpConnection' | 'token' | 'connection'> = {}) { - await super.start(options); - await this.loadEvents(options.eventsDir); - this.cache.intents = workerData.intents; - } - - async loadEvents(dir?: string) { - dir ??= await this.getRC().then(x => x.events); - if (dir && this.events) { - await this.events.load(dir); - this.logger.info('EventHandler loaded'); - } - } - - postMessage(body: any) { - if (manager) return manager.postMessage(body); - return process.send!(body); - } - - protected async handleManagerMessages(data: ManagerMessages) { - switch (data.type) { - case 'CACHE_RESULT': - if (this.cache.adapter instanceof WorkerAdapter && this.cache.adapter.promises.has(data.nonce)) { - const cacheData = this.cache.adapter.promises.get(data.nonce)!; - clearTimeout(cacheData.timeout); - cacheData.resolve(data.result); - this.cache.adapter.promises.delete(data.nonce); - } - break; - case 'SEND_PAYLOAD': - { - const shard = this.shards.get(data.shardId); - if (!shard) { - this.logger.fatal('Worker trying send payload by non-existent shard'); - return; - } - - await shard.send(true, { - ...data, - } satisfies GatewaySendPayload); - - this.postMessage({ - type: 'RESULT_PAYLOAD', - nonce: data.nonce, - workerId: this.workerId, - } satisfies WorkerSendResultPayload); - } - break; - case 'ALLOW_CONNECT': - { - const shard = this.shards.get(data.shardId); - if (!shard) { - this.logger.fatal('Worker trying connect non-existent shard'); - return; - } - shard.options.presence = data.presence; - await shard.connect(); - } - break; - case 'SPAWN_SHARDS': - { - const onPacket = this.onPacket.bind(this); - const handlePayload = this.options?.handlePayload?.bind(this); - const self = this; - for (const id of workerData.shards) { - let shard = this.shards.get(id); - if (!shard) { - shard = new Shard(id, { - token: workerData.token, - intents: workerData.intents, - info: data.info, - compress: data.compress, - debugger: this.debugger, - async handlePayload(shardId, payload) { - await handlePayload?.(shardId, payload); - await self.cache.onPacket(payload); - await onPacket?.(payload, shardId); - self.postMessage({ - workerId: workerData.workerId, - shardId, - type: 'RECEIVE_PAYLOAD', - payload, - } satisfies WorkerReceivePayload); - }, - }); - this.shards.set(id, shard); - } - - this.postMessage({ - type: 'CONNECT_QUEUE', - shardId: id, - workerId: workerData.workerId, - } satisfies WorkerRequestConnect); - } - } - break; - case 'SHARD_INFO': - { - const shard = this.shards.get(data.shardId); - if (!shard) { - this.logger.fatal('Worker trying get non-existent shard'); - return; - } - - this.postMessage({ - ...generateShardInfo(shard), - nonce: data.nonce, - type: 'SHARD_INFO', - workerId: this.workerId, - } satisfies WorkerSendShardInfo); - } - break; - case 'WORKER_INFO': - { - this.postMessage({ - shards: [...this.shards.values()].map(generateShardInfo), - workerId: workerData.workerId, - type: 'WORKER_INFO', - nonce: data.nonce, - } satisfies WorkerSendInfo); - } - break; - case 'BOT_READY': - await this.events?.runEvent('BOT_READY', this, this.me, -1); - break; - case 'API_RESPONSE': - { - const promise = this.rest.workerPromises!.get(data.nonce); - if (!promise) return; - this.rest.workerPromises!.delete(data.nonce); - if (data.error) return promise.reject(data.error); - promise.resolve(data.response); - } - break; - case 'EXECUTE_EVAL': - { - let result; - try { - // biome-ignore lint/security/noGlobalEval: yes - result = await eval(` - (${data.func})(this) - `); - } catch (e) { - result = e; - } - this.postMessage({ - type: 'EVAL_RESPONSE', - response: result, - workerId: workerData.workerId, - nonce: data.nonce, - } satisfies WorkerSendEvalResponse); - } - break; - case 'EVAL_RESPONSE': - { - const evalResponse = this.promises.get(data.nonce); - if (!evalResponse) return; - this.promises.delete(data.nonce); - clearTimeout(evalResponse.timeout); - evalResponse.resolve(data.response); - } - break; - } - } - - private generateNonce(large = true): string { - const uuid = randomUUID(); - const nonce = large ? uuid : uuid.split('-')[0]; - if (this.promises.has(nonce)) return this.generateNonce(large); - return nonce; - } - - private generateSendPromise(nonce: string, message = 'Timeout'): Promise { - return new Promise((res, rej) => { - const timeout = setTimeout(() => { - this.promises.delete(nonce); - rej(new Error(message)); - }, 60e3); - this.promises.set(nonce, { resolve: res, timeout }); - }); - } - - tellWorker(workerId: number, func: (_: this) => {}) { - const nonce = this.generateNonce(); - this.postMessage({ - type: 'EVAL', - func: func.toString(), - toWorkerId: workerId, - workerId: workerData.workerId, - nonce, - } satisfies WorkerSendEval); - return this.generateSendPromise(nonce); - } - - protected async onPacket(packet: GatewayDispatchPayload, shardId: number) { - await this.events?.execute('RAW', packet, this as WorkerClient, shardId); - switch (packet.t) { - case 'GUILD_MEMBER_UPDATE': - case 'PRESENCE_UPDATE': - - case 'MESSAGE_UPDATE': - case 'MESSAGE_DELETE_BULK': - case 'MESSAGE_DELETE': - case 'GUILD_DELETE': - case 'CHANNEL_UPDATE': - case 'GUILD_EMOJIS_UPDATE': - case 'GUILD_UPDATE': - case 'GUILD_ROLE_UPDATE': - case 'GUILD_ROLE_DELETE': - case 'THREAD_UPDATE': - case 'USER_UPDATE': - case 'VOICE_STATE_UPDATE': - case 'STAGE_INSTANCE_UPDATE': - case 'GUILD_STICKERS_UPDATE': - await this.events?.execute(packet.t, packet, this as WorkerClient, shardId); - await this.cache.onPacket(packet); - break; - //rest of the events - default: - { - await this.events?.execute(packet.t, packet, this, shardId); - switch (packet.t) { - case 'READY': - for (const g of packet.d.guilds) { - this.__handleGuilds?.add(g.id); - } - this.botId = packet.d.user.id; - this.applicationId = packet.d.application.id; - this.me = new ClientUser(this, packet.d.user, packet.d.application) as never; - if ( - !( - this.__handleGuilds?.size && - (workerData.intents & GatewayIntentBits.Guilds) === GatewayIntentBits.Guilds - ) - ) { - if ([...this.shards.values()].every(shard => shard.data.session_id)) { - this.postMessage({ - type: 'WORKER_READY', - workerId: this.workerId, - } as WorkerReady); - await this.events?.runEvent('WORKER_READY', this, this.me, -1); - } - delete this.__handleGuilds; - } - this.debugger?.debug(`#${shardId} [${packet.d.user.username}](${this.botId}) is online...`); - break; - case 'INTERACTION_CREATE': - await onInteractionCreate(this, packet.d, shardId); - break; - case 'MESSAGE_CREATE': - await onMessageCreate(this, packet.d, shardId); - break; - case 'GUILD_CREATE': { - if (this.__handleGuilds?.has(packet.d.id)) { - this.__handleGuilds.delete(packet.d.id); - if (!this.__handleGuilds.size && [...this.shards.values()].every(shard => shard.data.session_id)) { - this.postMessage({ - type: 'WORKER_READY', - workerId: this.workerId, - } as WorkerReady); - await this.events?.runEvent('WORKER_READY', this, this.me, -1); - } - if (!this.__handleGuilds.size) delete this.__handleGuilds; - return; - } - } - } - } - break; - } - } -} - -export function generateShardInfo(shard: Shard): WorkerShardInfo { - return { - open: shard.isOpen, - shardId: shard.id, - latency: shard.latency, - resumable: shard.resumable, - }; -} - -interface WorkerClientOptions extends BaseClientOptions { - disabledCache: Cache['disabledCache']; - commands?: NonNullable['commands']; - handlePayload?: ShardManagerOptions['handlePayload']; -} +import { GatewayIntentBits, type GatewayDispatchPayload, type GatewaySendPayload } from 'discord-api-types/v10'; +import { randomUUID } from 'node:crypto'; +import { ApiHandler, Logger } from '..'; +import type { Cache } from '../cache'; +import { WorkerAdapter } from '../cache'; +import { LogLevels, lazyLoadPackage, type DeepPartial, type When } from '../common'; +import { EventHandler } from '../events'; +import { ClientUser } from '../structures'; +import { Shard, type ShardManagerOptions, type WorkerData } from '../websocket'; +import type { + WorkerReady, + WorkerReceivePayload, + WorkerRequestConnect, + WorkerSendEval, + WorkerSendEvalResponse, + WorkerSendInfo, + WorkerSendResultPayload, + WorkerSendShardInfo, + WorkerShardInfo, + WorkerStart, +} from '../websocket/discord/worker'; +import type { ManagerMessages } from '../websocket/discord/workermanager'; +import type { BaseClientOptions, ServicesOptions, StartOptions } from './base'; +import { BaseClient } from './base'; +import type { Client } from './client'; +import { onInteractionCreate } from './oninteractioncreate'; +import { onMessageCreate } from './onmessagecreate'; +import { Collectors } from './collectors'; + +let workerData: WorkerData; +let manager: import('node:worker_threads').MessagePort; +try { + workerData = { + debug: process.env.SEYFERT_WORKER_DEBUG === 'true', + intents: Number.parseInt(process.env.SEYFERT_WORKER_INTENTS!), + path: process.env.SEYFERT_WORKER_PATH!, + shards: process.env.SEYFERT_WORKER_SHARDS!.split(',').map(id => Number.parseInt(id)), + token: process.env.SEYFERT_WORKER_TOKEN!, + workerId: Number.parseInt(process.env.SEYFERT_WORKER_WORKERID!), + workerProxy: process.env.SEYFERT_WORKER_WORKERPROXY === 'true', + } as WorkerData; +} catch {} + +export class WorkerClient extends BaseClient { + private __handleGuilds?: Set = new Set(); + logger = new Logger({ + name: `[Worker #${workerData.workerId}]`, + }); + + collectors = new Collectors(); + events? = new EventHandler(this); + me!: When; + promises = new Map void; timeout: NodeJS.Timeout }>(); + + shards = new Map(); + + declare options: WorkerClientOptions; + + constructor(options?: WorkerClientOptions) { + super(options); + if (!process.env.SEYFERT_SPAWNING) { + throw new Error('WorkerClient cannot spawn without manager'); + } + this.postMessage({ + type: 'WORKER_START', + workerId: workerData.workerId, + } satisfies WorkerStart); + + const worker_threads = lazyLoadPackage('node:worker_threads'); + if (worker_threads?.parentPort) { + manager = worker_threads?.parentPort; + } + (manager ?? process).on('message', (data: ManagerMessages) => this.handleManagerMessages(data)); + + this.setServices({ + cache: { + adapter: new WorkerAdapter(workerData), + disabledCache: options?.disabledCache, + }, + }); + if (workerData.debug) { + this.debugger = new Logger({ + name: `[Worker #${workerData.workerId}]`, + logLevel: LogLevels.Debug, + }); + } + if (workerData.workerProxy) { + this.setServices({ + rest: new ApiHandler({ + token: workerData.token, + workerProxy: true, + debug: workerData.debug, + }), + }); + } + } + + get workerId() { + return workerData.workerId; + } + + get latency() { + let acc = 0; + + this.shards.forEach(s => (acc += s.latency)); + + return acc / this.shards.size; + } + + setServices({ + ...rest + }: ServicesOptions & { + handlers?: ServicesOptions['handlers'] & { + events?: EventHandler['callback']; + }; + }) { + super.setServices(rest); + if (rest.handlers && 'events' in rest.handlers) { + if (!rest.handlers.events) { + this.events = undefined; + } else if (typeof rest.handlers.events === 'function') { + this.events = new EventHandler(this); + this.events.setHandlers({ + callback: rest.handlers.events, + }); + } else { + this.events = rest.handlers.events; + } + } + } + + async start(options: Omit, 'httpConnection' | 'token' | 'connection'> = {}) { + await super.start(options); + await this.loadEvents(options.eventsDir); + this.cache.intents = workerData.intents; + } + + async loadEvents(dir?: string) { + dir ??= await this.getRC().then(x => x.events); + if (dir && this.events) { + await this.events.load(dir); + this.logger.info('EventHandler loaded'); + } + } + + postMessage(body: any) { + if (manager) return manager.postMessage(body); + return process.send!(body); + } + + protected async handleManagerMessages(data: ManagerMessages) { + switch (data.type) { + case 'CACHE_RESULT': + if (this.cache.adapter instanceof WorkerAdapter && this.cache.adapter.promises.has(data.nonce)) { + const cacheData = this.cache.adapter.promises.get(data.nonce)!; + clearTimeout(cacheData.timeout); + cacheData.resolve(data.result); + this.cache.adapter.promises.delete(data.nonce); + } + break; + case 'SEND_PAYLOAD': + { + const shard = this.shards.get(data.shardId); + if (!shard) { + this.logger.fatal('Worker trying send payload by non-existent shard'); + return; + } + + await shard.send(true, { + ...data, + } satisfies GatewaySendPayload); + + this.postMessage({ + type: 'RESULT_PAYLOAD', + nonce: data.nonce, + workerId: this.workerId, + } satisfies WorkerSendResultPayload); + } + break; + case 'ALLOW_CONNECT': + { + const shard = this.shards.get(data.shardId); + if (!shard) { + this.logger.fatal('Worker trying connect non-existent shard'); + return; + } + shard.options.presence = data.presence; + await shard.connect(); + } + break; + case 'SPAWN_SHARDS': + { + const onPacket = this.onPacket.bind(this); + const handlePayload = this.options?.handlePayload?.bind(this); + const self = this; + for (const id of workerData.shards) { + let shard = this.shards.get(id); + if (!shard) { + shard = new Shard(id, { + token: workerData.token, + intents: workerData.intents, + info: data.info, + compress: data.compress, + debugger: this.debugger, + async handlePayload(shardId, payload) { + await handlePayload?.(shardId, payload); + await onPacket?.(payload, shardId); + self.postMessage({ + workerId: workerData.workerId, + shardId, + type: 'RECEIVE_PAYLOAD', + payload, + } satisfies WorkerReceivePayload); + }, + }); + this.shards.set(id, shard); + } + + this.postMessage({ + type: 'CONNECT_QUEUE', + shardId: id, + workerId: workerData.workerId, + } satisfies WorkerRequestConnect); + } + } + break; + case 'SHARD_INFO': + { + const shard = this.shards.get(data.shardId); + if (!shard) { + this.logger.fatal('Worker trying get non-existent shard'); + return; + } + + this.postMessage({ + ...generateShardInfo(shard), + nonce: data.nonce, + type: 'SHARD_INFO', + workerId: this.workerId, + } satisfies WorkerSendShardInfo); + } + break; + case 'WORKER_INFO': + { + this.postMessage({ + shards: [...this.shards.values()].map(generateShardInfo), + workerId: workerData.workerId, + type: 'WORKER_INFO', + nonce: data.nonce, + } satisfies WorkerSendInfo); + } + break; + case 'BOT_READY': + await this.events?.runEvent('BOT_READY', this, this.me, -1); + break; + case 'API_RESPONSE': + { + const promise = this.rest.workerPromises!.get(data.nonce); + if (!promise) return; + this.rest.workerPromises!.delete(data.nonce); + if (data.error) return promise.reject(data.error); + promise.resolve(data.response); + } + break; + case 'EXECUTE_EVAL': + { + let result; + try { + // biome-ignore lint/security/noGlobalEval: yes + result = await eval(` + (${data.func})(this) + `); + } catch (e) { + result = e; + } + this.postMessage({ + type: 'EVAL_RESPONSE', + response: result, + workerId: workerData.workerId, + nonce: data.nonce, + } satisfies WorkerSendEvalResponse); + } + break; + case 'EVAL_RESPONSE': + { + const evalResponse = this.promises.get(data.nonce); + if (!evalResponse) return; + this.promises.delete(data.nonce); + clearTimeout(evalResponse.timeout); + evalResponse.resolve(data.response); + } + break; + } + } + + private generateNonce(large = true): string { + const uuid = randomUUID(); + const nonce = large ? uuid : uuid.split('-')[0]; + if (this.promises.has(nonce)) return this.generateNonce(large); + return nonce; + } + + private generateSendPromise(nonce: string, message = 'Timeout'): Promise { + return new Promise((res, rej) => { + const timeout = setTimeout(() => { + this.promises.delete(nonce); + rej(new Error(message)); + }, 60e3); + this.promises.set(nonce, { resolve: res, timeout }); + }); + } + + tellWorker(workerId: number, func: (_: this) => {}) { + const nonce = this.generateNonce(); + this.postMessage({ + type: 'EVAL', + func: func.toString(), + toWorkerId: workerId, + workerId: workerData.workerId, + nonce, + } satisfies WorkerSendEval); + return this.generateSendPromise(nonce); + } + + protected async onPacket(packet: GatewayDispatchPayload, shardId: number) { + await this.events?.execute('RAW', packet, this as WorkerClient, shardId); + await this.events?.execute(packet.t, packet, this, shardId); + switch (packet.t) { + case 'READY': + for (const g of packet.d.guilds) { + this.__handleGuilds?.add(g.id); + } + this.botId = packet.d.user.id; + this.applicationId = packet.d.application.id; + this.me = new ClientUser(this, packet.d.user, packet.d.application) as never; + if ( + !(this.__handleGuilds?.size && (workerData.intents & GatewayIntentBits.Guilds) === GatewayIntentBits.Guilds) + ) { + if ([...this.shards.values()].every(shard => shard.data.session_id)) { + this.postMessage({ + type: 'WORKER_READY', + workerId: this.workerId, + } as WorkerReady); + await this.events?.runEvent('WORKER_READY', this, this.me, -1); + } + delete this.__handleGuilds; + } + this.debugger?.debug(`#${shardId} [${packet.d.user.username}](${this.botId}) is online...`); + break; + case 'INTERACTION_CREATE': + await onInteractionCreate(this, packet.d, shardId); + break; + case 'MESSAGE_CREATE': + await onMessageCreate(this, packet.d, shardId); + break; + case 'GUILD_CREATE': { + if (this.__handleGuilds?.has(packet.d.id)) { + this.__handleGuilds.delete(packet.d.id); + if (!this.__handleGuilds.size && [...this.shards.values()].every(shard => shard.data.session_id)) { + this.postMessage({ + type: 'WORKER_READY', + workerId: this.workerId, + } as WorkerReady); + await this.events?.runEvent('WORKER_READY', this, this.me, -1); + } + if (!this.__handleGuilds.size) delete this.__handleGuilds; + return; + } + } + } + } +} + +export function generateShardInfo(shard: Shard): WorkerShardInfo { + return { + open: shard.isOpen, + shardId: shard.id, + latency: shard.latency, + resumable: shard.resumable, + }; +} + +interface WorkerClientOptions extends BaseClientOptions { + disabledCache: Cache['disabledCache']; + commands?: NonNullable['commands']; + handlePayload?: ShardManagerOptions['handlePayload']; +} diff --git a/src/events/handler.ts b/src/events/handler.ts index 577a791..06141ba 100644 --- a/src/events/handler.ts +++ b/src/events/handler.ts @@ -1,129 +1,136 @@ -import type { - GatewayDispatchPayload, - GatewayMessageCreateDispatch, - GatewayMessageDeleteBulkDispatch, - GatewayMessageDeleteDispatch, -} from 'discord-api-types/v10'; -import type { Client, WorkerClient } from '../client'; -import { BaseHandler, type Logger, ReplaceRegex, magicImport, type MakeRequired, type SnakeCase } from '../common'; -import type { ClientEvents } from '../events/hooks'; -import * as RawEvents from '../events/hooks'; -import type { ClientEvent, ClientNameEvents } from './event'; -import type { Collectors } from '../client/collectors'; - -export type EventValue = MakeRequired & { fired?: boolean }; - -export type GatewayEvents = Uppercase>; - -export class EventHandler extends BaseHandler { - constructor( - logger: Logger, - protected collectors: Collectors, - ) { - super(logger); - } - - onFail = (event: GatewayEvents, err: unknown) => this.logger.warn('.events.onFail', err, event); - protected filter = (path: string) => path.endsWith('.js') || (!path.endsWith('.d.ts') && path.endsWith('.ts')); - - values: Partial> = {}; - - async load(eventsDir: string, instances?: { file: ClientEvent; path: string }[]) { - for (const i of instances ?? (await this.loadFilesK(await this.getFiles(eventsDir)))) { - const instance = this.callback(i.file); - if (!instance) continue; - if (typeof instance?.run !== 'function') { - this.logger.warn( - i.path.split(process.cwd()).slice(1).join(process.cwd()), - 'Missing run function, use `export default {...}` syntax', - ); - continue; - } - instance.__filePath = i.path; - this.values[ReplaceRegex.snake(instance.data.name).toUpperCase() as GatewayEvents] = instance as EventValue; - } - } - - async execute(name: GatewayEvents, ...args: [GatewayDispatchPayload, Client | WorkerClient, number]) { - switch (name) { - case 'MESSAGE_CREATE': - { - const { d: data } = args[0] as GatewayMessageCreateDispatch; - if (args[1].components?.values.has(data.interaction_metadata?.id ?? data.id)) { - args[1].components.values.get(data.interaction_metadata?.id ?? data.id)!.messageId = data.id; - } - } - break; - case 'MESSAGE_DELETE': - { - const { d: data } = args[0] as GatewayMessageDeleteDispatch; - const value = [...(args[1].components?.values ?? [])].find(x => x[1].messageId === data.id); - if (value) { - args[1].components!.onMessageDelete(value[0]); - } - } - break; - case 'MESSAGE_DELETE_BULK': - { - const { d: data } = args[0] as GatewayMessageDeleteBulkDispatch; - const values = [...(args[1].components?.values ?? [])]; - data.ids.forEach(id => { - const value = values.find(x => x[1].messageId === id); - if (value) { - args[1].components!.onMessageDelete(value[0]); - } - }); - } - break; - } - - await this.runEvent(args[0].t, args[1], args[0].d, args[2]); - await this.collectors.run(args[0].t, args[0].d); - } - - async runEvent(name: GatewayEvents, client: Client | WorkerClient, packet: any, shardId: number) { - const Event = this.values[name]; - if (!Event) { - return; - } - try { - if (Event.data.once && Event.fired) { - return; - } - Event.fired = true; - const hook = await RawEvents[name]?.(client, packet as never); - await Event.run(...[hook, client, shardId]); - } catch (e) { - await this.onFail(name, e); - } - } - - async reload(name: ClientNameEvents) { - const eventName = ReplaceRegex.snake(name).toUpperCase() as GatewayEvents; - const event = this.values[eventName]; - if (!event?.__filePath) return null; - delete require.cache[event.__filePath]; - const imported = await magicImport(event.__filePath).then(x => x.default ?? x); - imported.__filePath = event.__filePath; - this.values[eventName] = imported; - return imported; - } - - async reloadAll(stopIfFail = true) { - for (const i in this.values) { - try { - await this.reload(ReplaceRegex.camel(i) as ClientNameEvents); - } catch (e) { - if (stopIfFail) { - throw e; - } - } - } - } - - setHandlers({ callback }: { callback: EventHandler['callback'] }) { - this.callback = callback; - } - - callback = (file: ClientEvent): ClientEvent | false => file; -} +import type { + GatewayDispatchPayload, + GatewayMessageCreateDispatch, + GatewayMessageDeleteBulkDispatch, + GatewayMessageDeleteDispatch, +} from 'discord-api-types/v10'; +import type { Client, WorkerClient } from '../client'; +import { BaseHandler, ReplaceRegex, magicImport, type MakeRequired, type SnakeCase } from '../common'; +import type { ClientEvents } from '../events/hooks'; +import * as RawEvents from '../events/hooks'; +import type { ClientEvent, ClientNameEvents } from './event'; + +export type EventValue = MakeRequired & { fired?: boolean }; + +export type GatewayEvents = Uppercase>; + +export class EventHandler extends BaseHandler { + constructor(protected client: Client | WorkerClient) { + super(client.logger); + } + + onFail = (event: GatewayEvents, err: unknown) => this.logger.warn('.events.onFail', err, event); + protected filter = (path: string) => path.endsWith('.js') || (!path.endsWith('.d.ts') && path.endsWith('.ts')); + + values: Partial> = {}; + + async load(eventsDir: string, instances?: { file: ClientEvent; path: string }[]) { + for (const i of instances ?? (await this.loadFilesK(await this.getFiles(eventsDir)))) { + const instance = this.callback(i.file); + if (!instance) continue; + if (typeof instance?.run !== 'function') { + this.logger.warn( + i.path.split(process.cwd()).slice(1).join(process.cwd()), + 'Missing run function, use `export default {...}` syntax', + ); + continue; + } + instance.__filePath = i.path; + this.values[ReplaceRegex.snake(instance.data.name).toUpperCase() as GatewayEvents] = instance as EventValue; + } + } + + async execute(name: GatewayEvents, ...args: [GatewayDispatchPayload, Client | WorkerClient, number]) { + switch (name) { + case 'MESSAGE_CREATE': + { + const { d: data } = args[0] as GatewayMessageCreateDispatch; + if (args[1].components?.values.has(data.interaction_metadata?.id ?? data.id)) { + args[1].components.values.get(data.interaction_metadata?.id ?? data.id)!.messageId = data.id; + } + } + break; + case 'MESSAGE_DELETE': + { + const { d: data } = args[0] as GatewayMessageDeleteDispatch; + const value = [...(args[1].components?.values ?? [])].find(x => x[1].messageId === data.id); + if (value) { + args[1].components!.onMessageDelete(value[0]); + } + } + break; + case 'MESSAGE_DELETE_BULK': + { + const { d: data } = args[0] as GatewayMessageDeleteBulkDispatch; + const values = [...(args[1].components?.values ?? [])]; + data.ids.forEach(id => { + const value = values.find(x => x[1].messageId === id); + if (value) { + args[1].components!.onMessageDelete(value[0]); + } + }); + } + break; + } + + await this.runEvent(args[0].t, args[1], args[0].d, args[2]); + await this.client.collectors.run(args[0].t, args[0].d); + } + + async runEvent(name: GatewayEvents, client: Client | WorkerClient, packet: any, shardId: number) { + const Event = this.values[name]; + if (!Event) { + return this.client.cache.onPacket({ + t: name, + d: packet, + } as GatewayDispatchPayload); + } + try { + if (Event.data.once && Event.fired) { + return this.client.cache.onPacket({ + t: name, + d: packet, + } as GatewayDispatchPayload); + } + Event.fired = true; + const hook = await RawEvents[name]?.(client, packet as never); + if (name !== 'RAW') + await this.client.cache.onPacket({ + t: name, + d: packet, + } as GatewayDispatchPayload); + await Event.run(...[hook, client, shardId]); + } catch (e) { + await this.onFail(name, e); + } + } + + async reload(name: ClientNameEvents) { + const eventName = ReplaceRegex.snake(name).toUpperCase() as GatewayEvents; + const event = this.values[eventName]; + if (!event?.__filePath) return null; + delete require.cache[event.__filePath]; + const imported = await magicImport(event.__filePath).then(x => x.default ?? x); + imported.__filePath = event.__filePath; + this.values[eventName] = imported; + return imported; + } + + async reloadAll(stopIfFail = true) { + for (const i in this.values) { + try { + await this.reload(ReplaceRegex.camel(i) as ClientNameEvents); + } catch (e) { + if (stopIfFail) { + throw e; + } + } + } + } + + setHandlers({ callback }: { callback: EventHandler['callback'] }) { + this.callback = callback; + } + + callback = (file: ClientEvent): ClientEvent | false => file; +}