feat: port to typed EventEmitter

This commit is contained in:
DjDeveloperr 2021-01-20 15:35:15 +05:30
parent 3f436b2b3f
commit e7b0804616
13 changed files with 318 additions and 198 deletions

View file

@ -1,4 +1,4 @@
export { EventEmitter } from 'https://deno.land/std@0.82.0/node/events.ts'
export { EventEmitter } from 'https://deno.land/x/event@0.2.1/mod.ts'
export { unzlib } from 'https://deno.land/x/denoflate@1.1/mod.ts'
export { fetchAuto } from 'https://raw.githubusercontent.com/DjDeveloperr/fetch-base64/main/mod.ts'
export { parse } from 'https://deno.land/x/mutil@0.1.2/mod.ts'

View file

@ -1,5 +1,9 @@
import { GatewayEventHandler } from '../index.ts'
import { GatewayEvents, TypingStartGuildData } from '../../types/gateway.ts'
import {
GatewayEvents,
MessageDeletePayload,
TypingStartGuildData
} from '../../types/gateway.ts'
import { channelCreate } from './channelCreate.ts'
import { channelDelete } from './channelDelete.ts'
import { channelUpdate } from './channelUpdate.ts'
@ -55,6 +59,10 @@ import {
} from '../../utils/getChannelByType.ts'
import { interactionCreate } from './interactionCreate.ts'
import { Interaction } from '../../structures/slash.ts'
import { CommandContext } from '../../models/command.ts'
import { RequestMethods } from '../../models/rest.ts'
import { PartialInvitePayload } from '../../types/invite.ts'
import { GuildChannel } from '../../managers/guildChannels.ts'
export const gatewayHandlers: {
[eventCode in GatewayEvents]: GatewayEventHandler | undefined
@ -105,7 +113,8 @@ export interface VoiceServerUpdateData {
guild: Guild
}
export interface ClientEvents {
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
export type ClientEvents = {
/** When Client has successfully connected to Discord */
ready: []
/** When a successful reconnect has been made */
@ -355,4 +364,40 @@ export interface ClientEvents {
* @param payload Payload JSON of the event
*/
raw: [evt: string, payload: any]
/**
* An uncached Message was deleted.
* @param payload Message Delete Payload
*/
messageDeleteUncached: [payload: MessageDeletePayload]
guildMembersChunk: [
guild: Guild,
info: {
chunkIndex: number
chunkCount: number
members: string[]
presences: string[] | undefined
}
]
guildMembersChunked: [guild: Guild, chunks: number]
rateLimit: [data: { method: RequestMethods; url: string; body: any }]
inviteDeleteUncached: [invite: PartialInvitePayload]
voiceStateRemoveUncached: [data: { guild: Guild; member: Member }]
userUpdateUncached: [user: User]
webhooksUpdateUncached: [guild: Guild, channelID: string]
guildRoleUpdateUncached: [role: Role]
guildMemberUpdateUncached: [member: Member]
guildMemberRemoveUncached: [member: Member]
channelUpdateUncached: [channel: GuildChannel]
commandOwnerOnly: [ctx: CommandContext]
commandGuildOnly: [ctx: CommandContext]
commandDmOnly: [ctx: CommandContext]
commandNSFW: [ctx: CommandContext]
commandBotMissingPermissions: [ctx: CommandContext, missing: string[]]
commandUserMissingPermissions: [ctx: CommandContext, missing: string[]]
commandMissingArgs: [ctx: CommandContext]
commandUsed: [ctx: CommandContext]
commandError: [ctx: CommandContext, err: Error]
}

View file

@ -7,6 +7,7 @@ export const ready: GatewayEventHandler = async (
gateway: Gateway,
d: Ready
) => {
gateway.client.upSince = new Date()
await gateway.client.guilds.flush()
await gateway.client.users.set(d.user.id, d.user)

View file

@ -8,7 +8,7 @@ export const resume: GatewayEventHandler = async (
d: Resume
) => {
gateway.debug(`Session Resumed!`)
gateway.client.emit('resume')
gateway.client.emit('resumed')
if (gateway.client.user === undefined)
gateway.client.user = new User(
gateway.client,

View file

@ -1,4 +1,4 @@
import { unzlib, EventEmitter } from '../../deps.ts'
import { unzlib } from '../../deps.ts'
import { Client } from '../models/client.ts'
import {
DISCORD_GATEWAY_URL,
@ -10,14 +10,15 @@ import {
GatewayIntents,
GatewayCloseCodes,
IdentityPayload,
StatusUpdatePayload
StatusUpdatePayload,
GatewayEvents
} from '../types/gateway.ts'
import { gatewayHandlers } from './handlers/index.ts'
import { GATEWAY_BOT } from '../types/endpoint.ts'
import { GatewayCache } from '../managers/gatewayCache.ts'
import { delay } from '../utils/delay.ts'
import { VoiceChannel } from '../structures/guildVoiceChannel.ts'
import { Guild } from '../structures/guild.ts'
import { HarmonyEventEmitter } from '../utils/events.ts'
export interface RequestMembersOptions {
limit?: number
@ -33,15 +34,31 @@ export interface VoiceStateOptions {
export const RECONNECT_REASON = 'harmony-reconnect'
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
export type GatewayTypedEvents = {
[name in GatewayEvents]: [any]
} & {
connect: []
ping: [number]
resume: []
reconnectRequired: []
close: [number, string]
error: [Error, ErrorEvent]
sentIdentify: []
sentResume: []
reconnecting: []
init: []
}
/**
* Handles Discord Gateway connection.
*
* You should not use this and rather use Client class.
*/
export class Gateway extends EventEmitter {
websocket: WebSocket
token: string
intents: GatewayIntents[]
export class Gateway extends HarmonyEventEmitter<GatewayTypedEvents> {
websocket?: WebSocket
token?: string
intents?: GatewayIntents[]
connected = false
initialized = false
heartbeatInterval = 0
@ -53,23 +70,13 @@ export class Gateway extends EventEmitter {
client: Client
cache: GatewayCache
private timedIdentify: number | null = null
shards?: number[]
constructor(client: Client, token: string, intents: GatewayIntents[]) {
constructor(client: Client, shards?: number[]) {
super()
this.token = token
this.intents = intents
this.client = client
this.cache = new GatewayCache(client)
this.websocket = new WebSocket(
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
`${DISCORD_GATEWAY_URL}/?v=${DISCORD_API_VERSION}&encoding=json`,
[]
)
this.websocket.binaryType = 'arraybuffer'
this.websocket.onopen = this.onopen.bind(this)
this.websocket.onmessage = this.onmessage.bind(this)
this.websocket.onclose = this.onclose.bind(this)
this.websocket.onerror = this.onerror.bind(this)
this.shards = shards
}
private onopen(): void {
@ -145,7 +152,7 @@ export class Gateway extends EventEmitter {
await this.cache.set('seq', s)
}
if (t !== null && t !== undefined) {
this.emit(t, d)
this.emit(t as any, d)
this.client.emit('raw', t, d)
const handler = gatewayHandlers[t]
@ -236,24 +243,40 @@ export class Gateway extends EventEmitter {
}
}
private onerror(event: Event | ErrorEvent): void {
const eventError = event as ErrorEvent
this.emit('error', eventError)
private async onerror(event: ErrorEvent): Promise<void> {
const error = new Error(
Deno.inspect({
message: event.message,
error: event.error,
type: event.type,
target: event.target
})
)
error.name = 'ErrorEvent'
console.log(error)
this.emit('error', error, event)
await this.reconnect()
}
private async sendIdentify(forceNewSession?: boolean): Promise<void> {
this.debug('Fetching /gateway/bot...')
const info = await this.client.rest.get(GATEWAY_BOT())
if (info.session_start_limit.remaining === 0)
throw new Error(
`Session Limit Reached. Retry After ${info.session_start_limit.reset_after}ms`
if (typeof this.token !== 'string') throw new Error('Token not specified')
if (typeof this.intents !== 'object')
throw new Error('Intents not specified')
if (this.client.fetchGatewayInfo === true) {
this.debug('Fetching /gateway/bot...')
const info = await this.client.rest.api.gateway.bot.get()
if (info.session_start_limit.remaining === 0)
throw new Error(
`Session Limit Reached. Retry After ${info.session_start_limit.reset_after}ms`
)
this.debug(`Recommended Shards: ${info.shards}`)
this.debug('=== Session Limit Info ===')
this.debug(
`Remaining: ${info.session_start_limit.remaining}/${info.session_start_limit.total}`
)
this.debug(`Recommended Shards: ${info.shards}`)
this.debug('=== Session Limit Info ===')
this.debug(
`Remaining: ${info.session_start_limit.remaining}/${info.session_start_limit.total}`
)
this.debug(`Reset After: ${info.session_start_limit.reset_after}ms`)
this.debug(`Reset After: ${info.session_start_limit.reset_after}ms`)
}
if (forceNewSession === undefined || !forceNewSession) {
const sessionIDCached = await this.cache.get('session_id')
@ -272,7 +295,10 @@ export class Gateway extends EventEmitter {
$device: this.client.clientProperties.device ?? 'harmony'
},
compress: true,
shard: [0, 1], // TODO: Make sharding possible
shard:
this.shards === undefined
? [0, 1]
: [this.shards[0] ?? 0, this.shards[1] ?? 1],
intents: this.intents.reduce(
(previous, current) => previous | current,
0
@ -289,6 +315,10 @@ export class Gateway extends EventEmitter {
}
private async sendResume(): Promise<void> {
if (typeof this.token !== 'string') throw new Error('Token not specified')
if (typeof this.intents !== 'object')
throw new Error('Intents not specified')
if (this.sessionID === undefined) {
this.sessionID = await this.cache.get('session_id')
if (this.sessionID === undefined) return await this.sendIdentify()
@ -380,22 +410,22 @@ export class Gateway extends EventEmitter {
this.websocket.onopen = this.onopen.bind(this)
this.websocket.onmessage = this.onmessage.bind(this)
this.websocket.onclose = this.onclose.bind(this)
this.websocket.onerror = this.onerror.bind(this)
this.websocket.onerror = this.onerror.bind(this) as any
}
close(code: number = 1000, reason?: string): void {
return this.websocket.close(code, reason)
return this.websocket?.close(code, reason)
}
send(data: GatewayResponse): boolean {
if (this.websocket.readyState !== this.websocket.OPEN) return false
if (this.websocket?.readyState !== this.websocket?.OPEN) return false
const packet = JSON.stringify({
op: data.op,
d: data.d,
s: typeof data.s === 'number' ? data.s : null,
t: data.t === undefined ? null : data.t
})
this.websocket.send(packet)
this.websocket?.send(packet)
return true
}

View file

@ -3,7 +3,6 @@ import { User } from '../structures/user.ts'
import { GatewayIntents } from '../types/gateway.ts'
import { Gateway } from '../gateway/index.ts'
import { RESTManager, RESTOptions, TokenType } from './rest.ts'
import { EventEmitter } from '../../deps.ts'
import { DefaultCacheAdapter, ICacheAdapter } from './cacheAdapter.ts'
import { UsersManager } from '../managers/users.ts'
import { GuildManager } from '../managers/guilds.ts'
@ -21,6 +20,7 @@ import { Invite } from '../structures/invite.ts'
import { INVITE } from '../types/endpoint.ts'
import { ClientEvents } from '../gateway/handlers/index.ts'
import type { Collector } from './collectors.ts'
import { HarmonyEventEmitter } from '../utils/events.ts'
/** OS related properties sent with Gateway Identify */
export interface ClientProperties {
@ -59,40 +59,20 @@ export interface ClientOptions {
disableEnvToken?: boolean
/** Override REST Options */
restOptions?: RESTOptions
}
export declare interface Client {
on<K extends keyof ClientEvents>(
event: K,
listener: (...args: ClientEvents[K]) => void
): this
on(event: string | symbol, listener: (...args: any[]) => void): this
once<K extends keyof ClientEvents>(
event: K,
listener: (...args: ClientEvents[K]) => void
): this
once(event: string | symbol, listener: (...args: any[]) => void): this
emit<K extends keyof ClientEvents>(
event: K,
...args: ClientEvents[K]
): boolean
emit(event: string | symbol, ...args: any[]): boolean
off<K extends keyof ClientEvents>(
event: K,
listener: (...args: ClientEvents[K]) => void
): this
off(event: string | symbol, listener: (...args: any[]) => void): this
/** Whether to fetch Gateway info or not */
fetchGatewayInfo?: boolean
/** ADVANCED: Shard ID to launch on */
shard?: number
/** Shard count. Set to 'auto' for automatic sharding */
shardCount?: number | 'auto'
}
/**
* Discord Client.
*/
export class Client extends EventEmitter {
export class Client extends HarmonyEventEmitter<ClientEvents> {
/** Gateway object */
gateway?: Gateway
gateway: Gateway
/** REST Manager - used to make all requests */
rest: RESTManager
/** User which Client logs in to, undefined until logs in */
@ -117,12 +97,21 @@ export class Client extends EventEmitter {
clientProperties: ClientProperties
/** Slash-Commands Management client */
slash: SlashClient
/** Whether to fetch Gateway info or not */
fetchGatewayInfo: boolean = true
/** Users Manager, containing all Users cached */
users: UsersManager = new UsersManager(this)
/** Guilds Manager, providing cache & API interface to Guilds */
guilds: GuildManager = new GuildManager(this)
/** Channels Manager, providing cache interface to Channels */
channels: ChannelsManager = new ChannelsManager(this)
/** Channels Manager, providing cache interface to Channels */
emojis: EmojisManager = new EmojisManager(this)
/** Last READY timestamp */
upSince?: Date
/** Client's presence. Startup one if set before connecting */
presence: ClientPresence = new ClientPresence()
_decoratedEvents?: {
@ -141,10 +130,23 @@ export class Client extends EventEmitter {
/** Shard on which this Client is */
shard: number = 0
/** Shard Count */
shardCount: number | 'auto' = 1
/** Shard Manager of this Client if Sharded */
shardManager?: ShardManager
shards?: ShardManager
/** Collectors set */
collectors: Set<Collector> = new Set()
/** Since when is Client online (ready). */
get uptime(): number {
if (this.upSince === undefined) return 0
else {
const dif = Date.now() - this.upSince.getTime()
if (dif < 0) return dif
else return dif
}
}
constructor(options: ClientOptions = {}) {
super()
this._id = options.id
@ -169,7 +171,7 @@ export class Client extends EventEmitter {
Object.keys(this._decoratedEvents).length !== 0
) {
Object.entries(this._decoratedEvents).forEach((entry) => {
this.on(entry[0], entry[1])
this.on(entry[0] as keyof ClientEvents, entry[1])
})
this._decoratedEvents = undefined
}
@ -183,12 +185,17 @@ export class Client extends EventEmitter {
}
: options.clientProperties
if (options.shard !== undefined) this.shard = options.shard
if (options.shardCount !== undefined) this.shardCount = options.shardCount
this.slash = new SlashClient({
id: () => this.getEstimatedID(),
client: this,
enabled: options.enableSlash
})
if (options.fetchGatewayInfo === true) this.fetchGatewayInfo = true
if (this.token === undefined) {
try {
const token = Deno.env.get('DISCORD_TOKEN')
@ -209,6 +216,7 @@ export class Client extends EventEmitter {
if (options.restOptions !== undefined)
Object.assign(restOptions, options.restOptions)
this.rest = new RESTManager(restOptions)
this.gateway = new Gateway(this)
}
/**
@ -232,6 +240,7 @@ export class Client extends EventEmitter {
/** Emits debug event */
debug(tag: string, msg: string): void {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.emit('debug', `[${tag}] ${msg}`)
}
@ -271,7 +280,7 @@ export class Client extends EventEmitter {
* @param token Your token. This is required if not given in ClientOptions.
* @param intents Gateway intents in array. This is required if not given in ClientOptions.
*/
connect(token?: string, intents?: GatewayIntents[]): void {
async connect(token?: string, intents?: GatewayIntents[]): Promise<Client> {
if (token === undefined && this.token !== undefined) token = this.token
else if (this.token === undefined && token !== undefined) {
this.token = token
@ -288,7 +297,30 @@ export class Client extends EventEmitter {
} else throw new Error('No Gateway Intents were provided')
this.rest.token = token
this.gateway = new Gateway(this, token, intents)
this.gateway.token = token
this.gateway.intents = intents
this.gateway.initWebsocket()
return this.waitFor('ready', () => true).then(() => this)
}
/** Destroy the Gateway connection */
async destroy(): Promise<Client> {
this.gateway.initialized = false
this.gateway.sequenceID = undefined
this.gateway.sessionID = undefined
await this.gateway.cache.delete('seq')
await this.gateway.cache.delete('session_id')
this.gateway.close()
this.user = undefined
this.upSince = undefined
return this
}
/** Attempt to Close current Gateway connection and Resume */
async reconnect(): Promise<Client> {
this.gateway.close()
this.gateway.initWebsocket()
return this.waitFor('ready', () => true).then(() => this)
}
/** Add a new Collector */
@ -309,7 +341,7 @@ export class Client extends EventEmitter {
}
}
emit(event: keyof ClientEvents, ...args: any[]): boolean {
async emit(event: keyof ClientEvents, ...args: any[]): Promise<void> {
const collectors: Collector[] = []
for (const collector of this.collectors.values()) {
if (collector.event === event) collectors.push(collector)
@ -317,33 +349,11 @@ export class Client extends EventEmitter {
if (collectors.length !== 0) {
this.collectors.forEach((collector) => collector._fire(...args))
}
// TODO(DjDeveloperr): Fix this ts-ignore
// eslint-disable-next-line @typescript-eslint/prefer-ts-expect-error
// @ts-ignore
return super.emit(event, ...args)
}
/** Wait for an Event (optionally satisfying an event) to occur */
async waitFor<K extends keyof ClientEvents>(
event: K,
checkFunction: (...args: ClientEvents[K]) => boolean,
timeout?: number
): Promise<ClientEvents[K] | []> {
return await new Promise((resolve) => {
let timeoutID: number | undefined
if (timeout !== undefined) {
timeoutID = setTimeout(() => {
this.off(event, eventFunc)
resolve([])
}, timeout)
}
const eventFunc = (...args: ClientEvents[K]): void => {
if (checkFunction(...args)) {
resolve(args)
this.off(event, eventFunc)
if (timeoutID !== undefined) clearTimeout(timeoutID)
}
}
this.on(event, eventFunc)
})
}
}
/** Event decorator to create an Event handler from function */

View file

@ -1,6 +1,6 @@
import { Collection } from '../utils/collection.ts'
import { EventEmitter } from '../../deps.ts'
import type { Client } from './client.ts'
import { HarmonyEventEmitter } from '../utils/events.ts'
export type CollectorFilter = (...args: any[]) => boolean | Promise<boolean>
@ -19,7 +19,14 @@ export interface CollectorOptions {
timeout?: number
}
export class Collector extends EventEmitter {
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
export type CollectorEvents = {
start: []
end: []
collect: any
}
export class Collector extends HarmonyEventEmitter<CollectorEvents> {
client?: Client
private _started: boolean = false
event: string
@ -146,14 +153,14 @@ export class Collector extends EventEmitter {
let done = false
const onend = (): void => {
done = true
this.removeListener('end', onend)
this.off('end', onend)
resolve(this)
}
this.on('end', onend)
setTimeout(() => {
if (!done) {
this.removeListener('end', onend)
this.off('end', onend)
reject(new Error('Timeout'))
}
}, timeout)

View file

@ -259,7 +259,7 @@ export class CommandClient extends Client implements CommandClientOptions {
: category.ownerOnly) === true &&
!this.owners.includes(msg.author.id)
)
return this.emit('commandOwnerOnly', ctx, command)
return this.emit('commandOwnerOnly', ctx)
// Checks if Command is only for Guild
if (
@ -268,7 +268,7 @@ export class CommandClient extends Client implements CommandClientOptions {
: category.guildOnly) === true &&
msg.guild === undefined
)
return this.emit('commandGuildOnly', ctx, command)
return this.emit('commandGuildOnly', ctx)
// Checks if Command is only for DMs
if (
@ -277,14 +277,14 @@ export class CommandClient extends Client implements CommandClientOptions {
: category.dmOnly) === true &&
msg.guild !== undefined
)
return this.emit('commandDmOnly', ctx, command)
return this.emit('commandDmOnly', ctx)
if (
command.nsfw === true &&
(msg.guild === undefined ||
((msg.channel as unknown) as GuildTextChannel).nsfw !== true)
)
return this.emit('commandNSFW', ctx, command)
return this.emit('commandNSFW', ctx)
const allPermissions =
command.permissions !== undefined
@ -316,12 +316,7 @@ export class CommandClient extends Client implements CommandClientOptions {
}
if (missing.length !== 0)
return this.emit(
'commandBotMissingPermissions',
ctx,
command,
missing
)
return this.emit('commandBotMissingPermissions', ctx, missing)
}
}
@ -349,27 +344,22 @@ export class CommandClient extends Client implements CommandClientOptions {
}
if (missing.length !== 0)
return this.emit(
'commandUserMissingPermissions',
command,
missing,
ctx
)
return this.emit('commandUserMissingPermissions', ctx, missing)
}
}
if (command.args !== undefined) {
if (typeof command.args === 'boolean' && parsed.args.length === 0)
return this.emit('commandMissingArgs', ctx, command)
return this.emit('commandMissingArgs', ctx)
else if (
typeof command.args === 'number' &&
parsed.args.length < command.args
)
this.emit('commandMissingArgs', ctx, command)
this.emit('commandMissingArgs', ctx)
}
try {
this.emit('commandUsed', ctx, command)
this.emit('commandUsed', ctx)
const beforeExecute = await awaitSync(command.beforeExecute(ctx))
if (beforeExecute === false) return
@ -377,7 +367,7 @@ export class CommandClient extends Client implements CommandClientOptions {
const result = await awaitSync(command.execute(ctx))
command.afterExecute(ctx, result)
} catch (e) {
this.emit('commandError', command, ctx, e)
this.emit('commandError', ctx, e)
}
}
}

View file

@ -1,3 +1,4 @@
import { ClientEvents } from '../../mod.ts'
import { Collection } from '../utils/collection.ts'
import { Command } from './command.ts'
import { CommandClient } from './commandClient.ts'
@ -90,14 +91,14 @@ export class Extension {
Object.keys(this._decoratedEvents).length !== 0
) {
Object.entries(this._decoratedEvents).forEach((entry) => {
this.listen(entry[0], entry[1])
this.listen(entry[0] as keyof ClientEvents, entry[1])
})
this._decoratedEvents = undefined
}
}
/** Listens for an Event through Extension. */
listen(event: string, cb: ExtensionEventCallback): boolean {
listen(event: keyof ClientEvents, cb: ExtensionEventCallback): boolean {
if (this.events[event] !== undefined) return false
else {
const fn = (...args: any[]): any => {
@ -152,7 +153,7 @@ export class ExtensionsManager {
if (extension === undefined) return false
extension.commands.deleteAll()
for (const [k, v] of Object.entries(extension.events)) {
this.client.removeListener(k, v)
this.client.off(k as keyof ClientEvents, v)
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete extension.events[k]
}

View file

@ -1,69 +1,75 @@
import { Collection } from '../utils/collection.ts'
import { Client, ClientOptions } from './client.ts'
import {EventEmitter} from '../../deps.ts'
import { Client } from './client.ts'
import { RESTManager } from './rest.ts'
// import { GATEWAY_BOT } from '../types/endpoint.ts'
// import { GatewayBotPayload } from '../types/gatewayBot.ts'
import { Gateway } from '../gateway/index.ts'
import { HarmonyEventEmitter } from '../utils/events.ts'
import { GatewayEvents } from '../types/gateway.ts'
// TODO(DjDeveloperr)
// I'm kinda confused; will continue on this later once
// Deno namespace in Web Worker is stable!
export interface ShardManagerOptions {
client: Client | typeof Client
token?: string
intents?: number[]
options?: ClientOptions
shards: number
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
export type ShardManagerEvents = {
launch: [number]
shardReady: [number]
shardDisconnect: [number, number | undefined, string | undefined]
shardError: [number, Error, ErrorEvent]
shardResume: [number]
}
export interface ShardManagerInitOptions {
file: string
token?: string
intents?: number[]
options?: ClientOptions
shards?: number
}
export class ShardManager extends EventEmitter {
workers: Collection<string, Worker> = new Collection()
token: string
intents: number[]
shardCount: number
private readonly __client: Client
export class ShardManager extends HarmonyEventEmitter<ShardManagerEvents> {
list: Collection<string, Gateway> = new Collection()
client: Client
cachedShardCount?: number
get rest(): RESTManager {
return this.__client.rest
return this.client.rest
}
constructor(options: ShardManagerOptions) {
constructor(client: Client) {
super()
this.__client =
options.client instanceof Client
? options.client
: // eslint-disable-next-line new-cap
new options.client(options.options)
if (this.__client.token === undefined || options.token === undefined)
throw new Error('Token should be provided when constructing ShardManager')
if (this.__client.intents === undefined || options.intents === undefined)
throw new Error(
'Intents should be provided when constructing ShardManager'
)
this.token = this.__client.token ?? options.token
this.intents = this.__client.intents ?? options.intents
this.shardCount = options.shards
this.client = client
}
// static async init(): Promise<ShardManager> {}
async getShardCount(): Promise<number> {
let shardCount: number
if (this.cachedShardCount !== undefined) shardCount = this.cachedShardCount
else {
if (this.client.shardCount === 'auto') {
const info = await this.client.rest.api.gateway.bot.get()
shardCount = info.shards as number
} else shardCount = this.client.shardCount ?? 1
}
this.cachedShardCount = shardCount
return this.cachedShardCount
}
// async start(): Promise<ShardManager> {
// const info = ((await this.rest.get(
// GATEWAY_BOT()
// )) as unknown) as GatewayBotPayload
/** Launches a new Shard */
async launch(id: number): Promise<ShardManager> {
if (this.list.has(id.toString()) === true)
throw new Error(`Shard ${id} already launched`)
// const totalShards = this.__shardCount ?? info.shards
const shardCount = await this.getShardCount()
// return this
// }
const gw = new Gateway(this.client, [Number(id), shardCount])
this.list.set(id.toString(), gw)
gw.initWebsocket()
this.emit('launch', id)
gw.on(GatewayEvents.Ready, () => this.emit('shardReady', id))
gw.on('error', (err: Error, evt: ErrorEvent) =>
this.emit('shardError', id, err, evt)
)
gw.on(GatewayEvents.Resumed, () => this.emit('shardResume', id))
gw.on('close', (code: number, reason: string) =>
this.emit('shardDisconnect', id, code, reason)
)
return gw.waitFor(GatewayEvents.Ready, () => true).then(() => this)
}
async start(): Promise<ShardManager> {
const shardCount = await this.getShardCount()
for (let i = 0; i <= shardCount; i++) {
await this.launch(i)
}
return this
}
}

View file

@ -292,14 +292,14 @@ export class Guild extends Base {
const listener = (guild: Guild): void => {
if (guild.id === this.id) {
chunked = true
this.client.removeListener('guildMembersChunked', listener)
this.client.off('guildMembersChunked', listener)
resolve(this)
}
}
this.client.on('guildMembersChunked', listener)
setTimeout(() => {
if (!chunked) {
this.client.removeListener('guildMembersChunked', listener)
this.client.off('guildMembersChunked', listener)
}
}, timeout)
}
@ -312,19 +312,19 @@ export class Guild extends Base {
*/
async awaitAvailability(timeout: number = 1000): Promise<Guild> {
return await new Promise((resolve, reject) => {
if(!this.unavailable) resolve(this);
if (!this.unavailable) resolve(this)
const listener = (guild: Guild): void => {
if (guild.id === this.id) {
this.client.removeListener('guildLoaded', listener);
resolve(this);
this.client.off('guildLoaded', listener)
resolve(this)
}
};
this.client.on('guildLoaded', listener);
}
this.client.on('guildLoaded', listener)
setTimeout(() => {
this.client.removeListener('guildLoaded', listener);
reject(Error("Timeout. Guild didn't arrive in time."));
}, timeout);
});
this.client.off('guildLoaded', listener)
reject(Error("Timeout. Guild didn't arrive in time."))
}, timeout)
})
}
}

View file

@ -44,7 +44,7 @@ export class VoiceChannel extends Channel {
const onVoiceStateAdd = (state: VoiceState): void => {
if (state.user.id !== this.client.user?.id) return
if (state.channel?.id !== this.id) return
this.client.removeListener('voiceStateAdd', onVoiceStateAdd)
this.client.off('voiceStateAdd', onVoiceStateAdd)
done++
if (done >= 2) resolve((vcdata as unknown) as VoiceServerUpdateData)
}
@ -52,7 +52,7 @@ export class VoiceChannel extends Channel {
const onVoiceServerUpdate = (data: VoiceServerUpdateData): void => {
if (data.guild.id !== this.guild.id) return
vcdata = data
this.client.removeListener('voiceServerUpdate', onVoiceServerUpdate)
this.client.off('voiceServerUpdate', onVoiceServerUpdate)
done++
if (done >= 2) resolve(vcdata)
}
@ -64,8 +64,8 @@ export class VoiceChannel extends Channel {
setTimeout(() => {
if (done < 2) {
this.client.removeListener('voiceServerUpdate', onVoiceServerUpdate)
this.client.removeListener('voiceStateAdd', onVoiceStateAdd)
this.client.off('voiceServerUpdate', onVoiceServerUpdate)
this.client.off('voiceStateAdd', onVoiceStateAdd)
reject(
new Error(
"Connection timed out - couldn't connect to Voice Channel"

30
src/utils/events.ts Normal file
View file

@ -0,0 +1,30 @@
import { EventEmitter } from '../../deps.ts'
export class HarmonyEventEmitter<
T extends Record<string, unknown[]>
> extends EventEmitter<T> {
/** Wait for an Event to fire with given condition. */
async waitFor<K extends keyof T>(
event: K,
checkFunction: (...args: T[K]) => boolean = () => true,
timeout?: number
): Promise<T[K] | []> {
return await new Promise((resolve) => {
let timeoutID: number | undefined
if (timeout !== undefined) {
timeoutID = setTimeout(() => {
this.off(event, eventFunc)
resolve([])
}, timeout)
}
const eventFunc = (...args: T[K]): void => {
if (checkFunction(...args)) {
resolve(args)
this.off(event, eventFunc)
if (timeoutID !== undefined) clearTimeout(timeoutID)
}
}
this.on(event, eventFunc)
})
}
}