From 9ad40b7516291e8c6105c523f5467825f0628e16 Mon Sep 17 00:00:00 2001 From: almeidx Date: Fri, 17 Jan 2025 00:18:16 +0000 Subject: [PATCH 1/2] refactor!: use `AsyncEventEmitter` instead of `EventEmitter` BREAKING CHANGE: The `BaseClient`, `Shard`, `ShardingManager`, and `Collector` classes now extend `AsyncEventEmitter` instead of `EventEmitter`. --- packages/discord.js/package.json | 1 + packages/discord.js/src/client/BaseClient.js | 8 +- packages/discord.js/src/sharding/Shard.js | 10 +- .../src/sharding/ShardClientUtil.js | 4 +- .../src/sharding/ShardingManager.js | 6 +- .../src/structures/interfaces/Collector.js | 6 +- packages/discord.js/typings/index.d.ts | 123 +++--------------- packages/discord.js/typings/index.test-d.ts | 4 +- pnpm-lock.yaml | 3 + 9 files changed, 41 insertions(+), 124 deletions(-) diff --git a/packages/discord.js/package.json b/packages/discord.js/package.json index 3e378d4f6c7c..4f33cf5a1cca 100644 --- a/packages/discord.js/package.json +++ b/packages/discord.js/package.json @@ -72,6 +72,7 @@ "@discordjs/util": "workspace:^", "@discordjs/ws": "workspace:^", "@sapphire/snowflake": "3.5.5", + "@vladfrangu/async_event_emitter": "^2.4.6", "discord-api-types": "^0.37.114", "fast-deep-equal": "3.1.3", "lodash.snakecase": "4.1.1", diff --git a/packages/discord.js/src/client/BaseClient.js b/packages/discord.js/src/client/BaseClient.js index 70397d9a073f..299727069a30 100644 --- a/packages/discord.js/src/client/BaseClient.js +++ b/packages/discord.js/src/client/BaseClient.js @@ -1,7 +1,7 @@ 'use strict'; -const EventEmitter = require('node:events'); const { REST } = require('@discordjs/rest'); +const { AsyncEventEmitter } = require('@vladfrangu/async_event_emitter'); const { Routes } = require('discord-api-types/v10'); const { DiscordjsTypeError, ErrorCodes } = require('../errors'); const { Options } = require('../util/Options'); @@ -9,11 +9,11 @@ const { flatten } = require('../util/Util'); /** * The base class for all clients. - * @extends {EventEmitter} + * @extends {AsyncEventEmitter} */ -class BaseClient extends EventEmitter { +class BaseClient extends AsyncEventEmitter { constructor(options = {}) { - super({ captureRejections: true }); + super(); if (typeof options !== 'object' || options === null) { throw new DiscordjsTypeError(ErrorCodes.InvalidType, 'options', 'object', true); diff --git a/packages/discord.js/src/sharding/Shard.js b/packages/discord.js/src/sharding/Shard.js index 0d52f3aadf71..f20f9c9aad50 100644 --- a/packages/discord.js/src/sharding/Shard.js +++ b/packages/discord.js/src/sharding/Shard.js @@ -1,11 +1,11 @@ 'use strict'; -const EventEmitter = require('node:events'); const path = require('node:path'); const process = require('node:process'); const { setTimeout, clearTimeout } = require('node:timers'); const { setTimeout: sleep } = require('node:timers/promises'); const { SHARE_ENV } = require('node:worker_threads'); +const { AsyncEventEmitter } = require('@vladfrangu/async_event_emitter'); const { DiscordjsError, ErrorCodes } = require('../errors'); const { ShardEvents } = require('../util/ShardEvents'); const { makeError, makePlainError } = require('../util/Util'); @@ -17,9 +17,9 @@ let Worker = null; * A self-contained shard created by the {@link ShardingManager}. Each one has a {@link ChildProcess} that contains * an instance of the bot and its {@link Client}. When its child process/worker exits for any reason, the shard will * spawn a new one to replace it as necessary. - * @extends {EventEmitter} + * @extends {AsyncEventEmitter} */ -class Shard extends EventEmitter { +class Shard extends AsyncEventEmitter { constructor(manager, id) { super(); @@ -445,7 +445,7 @@ class Shard extends EventEmitter { /** * Increments max listeners by one for a given emitter, if they are not zero. - * @param {EventEmitter|process} emitter The emitter that emits the events. + * @param {Worker|ChildProcess} emitter The emitter that emits the events. * @private */ incrementMaxListeners(emitter) { @@ -457,7 +457,7 @@ class Shard extends EventEmitter { /** * Decrements max listeners by one for a given emitter, if they are not zero. - * @param {EventEmitter|process} emitter The emitter that emits the events. + * @param {Worker|ChildProcess} emitter The emitter that emits the events. * @private */ decrementMaxListeners(emitter) { diff --git a/packages/discord.js/src/sharding/ShardClientUtil.js b/packages/discord.js/src/sharding/ShardClientUtil.js index 5aa5bc6698f7..18bd53a29782 100644 --- a/packages/discord.js/src/sharding/ShardClientUtil.js +++ b/packages/discord.js/src/sharding/ShardClientUtil.js @@ -242,7 +242,7 @@ class ShardClientUtil { /** * Increments max listeners by one for a given emitter, if they are not zero. - * @param {EventEmitter|process} emitter The emitter that emits the events. + * @param {Worker|ChildProcess} emitter The emitter that emits the events. * @private */ incrementMaxListeners(emitter) { @@ -254,7 +254,7 @@ class ShardClientUtil { /** * Decrements max listeners by one for a given emitter, if they are not zero. - * @param {EventEmitter|process} emitter The emitter that emits the events. + * @param {Worker|ChildProcess} emitter The emitter that emits the events. * @private */ decrementMaxListeners(emitter) { diff --git a/packages/discord.js/src/sharding/ShardingManager.js b/packages/discord.js/src/sharding/ShardingManager.js index ad6252e37265..964e7046ed5c 100644 --- a/packages/discord.js/src/sharding/ShardingManager.js +++ b/packages/discord.js/src/sharding/ShardingManager.js @@ -1,11 +1,11 @@ 'use strict'; -const EventEmitter = require('node:events'); const fs = require('node:fs'); const path = require('node:path'); const process = require('node:process'); const { setTimeout: sleep } = require('node:timers/promises'); const { Collection } = require('@discordjs/collection'); +const { AsyncEventEmitter } = require('@vladfrangu/async_event_emitter'); const { Shard } = require('./Shard'); const { DiscordjsError, DiscordjsTypeError, DiscordjsRangeError, ErrorCodes } = require('../errors'); const { fetchRecommendedShardCount } = require('../util/Util'); @@ -17,9 +17,9 @@ const { fetchRecommendedShardCount } = require('../util/Util'); * process, and there are several useful methods that utilize it in order to simplify tasks that are normally difficult * with sharding. It can spawn a specific number of shards or the amount that Discord suggests for the bot, and takes a * path to your main bot script to launch for each one. - * @extends {EventEmitter} + * @extends {AsyncEventEmitter} */ -class ShardingManager extends EventEmitter { +class ShardingManager extends AsyncEventEmitter { /** * The mode to spawn shards with for a {@link ShardingManager}. Can be either one of: * * 'process' to use child processes diff --git a/packages/discord.js/src/structures/interfaces/Collector.js b/packages/discord.js/src/structures/interfaces/Collector.js index ad5fb5f9691a..9be7c877fc1c 100644 --- a/packages/discord.js/src/structures/interfaces/Collector.js +++ b/packages/discord.js/src/structures/interfaces/Collector.js @@ -1,8 +1,8 @@ 'use strict'; -const EventEmitter = require('node:events'); const { setTimeout, clearTimeout } = require('node:timers'); const { Collection } = require('@discordjs/collection'); +const { AsyncEventEmitter } = require('@vladfrangu/async_event_emitter'); const { DiscordjsTypeError, ErrorCodes } = require('../../errors'); const { flatten } = require('../../util/Util'); @@ -25,10 +25,10 @@ const { flatten } = require('../../util/Util'); /** * Abstract class for defining a new Collector. - * @extends {EventEmitter} + * @extends {AsyncEventEmitter} * @abstract */ -class Collector extends EventEmitter { +class Collector extends AsyncEventEmitter { constructor(client, options = {}) { super(); diff --git a/packages/discord.js/typings/index.d.ts b/packages/discord.js/typings/index.d.ts index 3c1f8064e9a4..37242651c1d9 100644 --- a/packages/discord.js/typings/index.d.ts +++ b/packages/discord.js/typings/index.d.ts @@ -20,6 +20,7 @@ import { Awaitable, JSONEncodable } from '@discordjs/util'; import { Collection, ReadonlyCollection } from '@discordjs/collection'; import { BaseImageURLOptions, ImageURLOptions, RawFile, REST, RESTOptions } from '@discordjs/rest'; import { WebSocketManager, WebSocketManagerOptions } from '@discordjs/ws'; +import { AsyncEventEmitter } from '@vladfrangu/async_event_emitter'; import { APIActionRowComponent, APIApplicationCommandInteractionData, @@ -177,7 +178,6 @@ import { GatewayVoiceChannelEffectSendDispatchData, } from 'discord-api-types/v10'; import { ChildProcess } from 'node:child_process'; -import { EventEmitter } from 'node:events'; import { Stream } from 'node:stream'; import { MessagePort, Worker } from 'node:worker_threads'; import { @@ -515,7 +515,7 @@ export abstract class Base { public valueOf(): string; } -export class BaseClient extends EventEmitter implements AsyncDisposable { +export class BaseClient extends AsyncEventEmitter implements AsyncDisposable { public constructor(options?: ClientOptions | WebhookClientOptions); private decrementMaxListeners(): void; private incrementMaxListeners(): void; @@ -959,7 +959,7 @@ export type If = Value ex ? FalseResult : TrueResult | FalseResult; -export class Client extends BaseClient { +export class Client extends BaseClient { public constructor(options: ClientOptions); private actions: unknown; private expectedGuilds: Set; @@ -977,18 +977,6 @@ export class Client extends BaseClient { // This a technique used to brand the ready state. Or else we'll get `never` errors on typeguard checks. private readonly _ready: Ready; - // Override inherited static EventEmitter methods, with added type checks for Client events. - public static once( - eventEmitter: Emitter, - eventName: Emitter extends Client ? Event : string | symbol, - options?: { signal?: AbortSignal | undefined }, - ): Promise; - public static on( - eventEmitter: Emitter, - eventName: Emitter extends Client ? Event : string | symbol, - options?: { signal?: AbortSignal | undefined }, - ): AsyncIterableIterator; - public application: If; public channels: ChannelManager; public get emojis(): BaseGuildEmojiManager; @@ -1023,30 +1011,6 @@ export class Client extends BaseClient { public login(token?: string): Promise; public isReady(): this is Client; public toJSON(): unknown; - - public on(event: Event, listener: (...args: ClientEvents[Event]) => void): this; - public on( - event: Exclude, - listener: (...args: any[]) => void, - ): this; - - public once(event: Event, listener: (...args: ClientEvents[Event]) => void): this; - public once( - event: Exclude, - listener: (...args: any[]) => void, - ): this; - - public emit(event: Event, ...args: ClientEvents[Event]): boolean; - public emit(event: Exclude, ...args: unknown[]): boolean; - - public off(event: Event, listener: (...args: ClientEvents[Event]) => void): this; - public off( - event: Exclude, - listener: (...args: any[]) => void, - ): this; - - public removeAllListeners(event?: Event): this; - public removeAllListeners(event?: Exclude): this; } export interface StickerPackFetchOptions { @@ -1134,7 +1098,9 @@ export interface CollectorEventTypes end: [collected: ReadonlyCollection, reason: string]; } -export abstract class Collector extends EventEmitter { +export abstract class Collector extends AsyncEventEmitter< + CollectorEventTypes +> { protected constructor(client: Client, options?: CollectorOptions<[Value, ...Extras]>); private _timeout: NodeJS.Timeout | null; private _idletimeout: NodeJS.Timeout | null; @@ -1160,16 +1126,6 @@ export abstract class Collector exten protected listener: (...args: any[]) => void; public abstract collect(...args: unknown[]): Awaitable; public abstract dispose(...args: unknown[]): Key | null; - - public on>( - event: EventKey, - listener: (...args: CollectorEventTypes[EventKey]) => void, - ): this; - - public once>( - event: EventKey, - listener: (...args: CollectorEventTypes[EventKey]) => void, - ): this; } export class ChatInputCommandInteraction extends CommandInteraction { @@ -2031,19 +1987,6 @@ export class InteractionCollector exte public collect(interaction: Interaction): Snowflake; public empty(): void; public dispose(interaction: Interaction): Snowflake; - public on(event: 'collect' | 'dispose' | 'ignore', listener: (interaction: Interaction) => void): this; - public on( - event: 'end', - listener: (collected: ReadonlyCollection, reason: string) => void, - ): this; - public on(event: string, listener: (...args: any[]) => void): this; - - public once(event: 'collect' | 'dispose' | 'ignore', listener: (interaction: Interaction) => void): this; - public once( - event: 'end', - listener: (collected: ReadonlyCollection, reason: string) => void, - ): this; - public once(event: string, listener: (...args: any[]) => void): this; } // tslint:disable-next-line no-empty-interface @@ -2783,26 +2726,6 @@ export class ReactionCollector extends Collector void, - ): this; - public on( - event: 'end', - listener: (collected: ReadonlyCollection, reason: string) => void, - ): this; - public on(event: string, listener: (...args: any[]) => void): this; - - public once( - event: 'collect' | 'dispose' | 'remove' | 'ignore', - listener: (reaction: MessageReaction, user: User) => void, - ): this; - public once( - event: 'end', - listener: (collected: ReadonlyCollection, reason: string) => void, - ): this; - public once(event: string, listener: (...args: any[]) => void): this; } export class ReactionEmoji extends Emoji { @@ -2997,15 +2920,15 @@ export interface ShardEventTypes { spawn: [process: ChildProcess | Worker]; } -export class Shard extends EventEmitter { +export class Shard extends AsyncEventEmitter { private constructor(manager: ShardingManager, id: number); private _evals: Map>; private _exitListener: (...args: any[]) => void; private _fetches: Map>; private _handleExit(respawn?: boolean, timeout?: number): void; private _handleMessage(message: unknown): void; - private incrementMaxListeners(emitter: EventEmitter | ChildProcess): void; - private decrementMaxListeners(emitter: EventEmitter | ChildProcess): void; + private incrementMaxListeners(emitter: Worker | ChildProcess): void; + private decrementMaxListeners(emitter: Worker | ChildProcess): void; public args: string[]; public execArgv: string[]; @@ -3027,24 +2950,14 @@ export class Shard extends EventEmitter { public respawn(options?: { delay?: number; timeout?: number }): Promise; public send(message: unknown): Promise; public spawn(timeout?: number): Promise; - - public on( - event: Event, - listener: (...args: ShardEventTypes[Event]) => void, - ): this; - - public once( - event: Event, - listener: (...args: ShardEventTypes[Event]) => void, - ): this; } export class ShardClientUtil { private constructor(client: Client, mode: ShardingManagerMode); private _handleMessage(message: unknown): void; private _respond(type: string, message: unknown): void; - private incrementMaxListeners(emitter: EventEmitter | ChildProcess): void; - private decrementMaxListeners(emitter: EventEmitter | ChildProcess): void; + private incrementMaxListeners(emitter: Worker | ChildProcess): void; + private decrementMaxListeners(emitter: Worker | ChildProcess): void; public client: Client; public get count(): number; @@ -3073,7 +2986,11 @@ export class ShardClientUtil { public static shardIdForGuildId(guildId: Snowflake, shardCount: number): number; } -export class ShardingManager extends EventEmitter { +export interface ShardingManagerEvents { + shardCreate: [shard: Shard]; +} + +export class ShardingManager extends AsyncEventEmitter { public constructor(file: string, options?: ShardingManagerOptions); private _performOnShards(method: string, args: readonly unknown[]): Promise; private _performOnShards(method: string, args: readonly unknown[], shard: number): Promise; @@ -3105,10 +3022,6 @@ export class ShardingManager extends EventEmitter { public fetchClientValues(prop: string, shard: number): Promise; public respawnAll(options?: MultipleShardRespawnOptions): Promise>; public spawn(options?: MultipleShardSpawnOptions): Promise>; - - public on(event: 'shardCreate', listener: (shard: Shard) => void): this; - - public once(event: 'shardCreate', listener: (shard: Shard) => void): this; } export interface FetchRecommendedShardCountOptions { @@ -3692,8 +3605,8 @@ export class Webhook { } // tslint:disable-next-line no-empty-interface -export interface WebhookClient extends WebhookFields, BaseClient {} -export class WebhookClient extends BaseClient { +export interface WebhookClient extends WebhookFields, BaseClient<{}> {} +export class WebhookClient extends BaseClient<{}> { public constructor(data: WebhookClientData, options?: WebhookClientOptions); public readonly client: this; public options: WebhookClientOptions; diff --git a/packages/discord.js/typings/index.test-d.ts b/packages/discord.js/typings/index.test-d.ts index 29204e7131db..3b40d874e024 100644 --- a/packages/discord.js/typings/index.test-d.ts +++ b/packages/discord.js/typings/index.test-d.ts @@ -1306,9 +1306,9 @@ client.on('guildCreate', async g => { ); }); -// EventEmitter static method overrides +// Event emitter static method overrides expectType]>>(Client.once(client, 'clientReady')); -expectType]>>(Client.on(client, 'clientReady')); +expectAssignable]>>(Client.on(client, 'clientReady')); client.login('absolutely-valid-token'); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 24f9433c5676..06a94a2c5498 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -940,6 +940,9 @@ importers: '@sapphire/snowflake': specifier: 3.5.5 version: 3.5.5 + '@vladfrangu/async_event_emitter': + specifier: ^2.4.6 + version: 2.4.6 discord-api-types: specifier: ^0.37.114 version: 0.37.114 From abb7e259065073f799ac73a8d9111776f96cba9d Mon Sep 17 00:00:00 2001 From: almeidx Date: Sun, 19 Jan 2025 13:24:30 +0000 Subject: [PATCH 2/2] fix: requested changes --- packages/discord.js/typings/index.d.ts | 36 +++++++++++++-------- packages/discord.js/typings/index.test-d.ts | 21 ++++++++++++ 2 files changed, 43 insertions(+), 14 deletions(-) diff --git a/packages/discord.js/typings/index.d.ts b/packages/discord.js/typings/index.d.ts index 37242651c1d9..7bc046078d0a 100644 --- a/packages/discord.js/typings/index.d.ts +++ b/packages/discord.js/typings/index.d.ts @@ -959,7 +959,7 @@ export type If = Value ex ? FalseResult : TrueResult | FalseResult; -export class Client extends BaseClient { +export class Client extends BaseClient { public constructor(options: ClientOptions); private actions: unknown; private expectedGuilds: Set; @@ -1098,9 +1098,12 @@ export interface CollectorEventTypes end: [collected: ReadonlyCollection, reason: string]; } -export abstract class Collector extends AsyncEventEmitter< - CollectorEventTypes -> { +export abstract class Collector< + Key, + Value, + Extras extends unknown[] = [], + EventTypes extends {} = CollectorEventTypes, +> extends AsyncEventEmitter { protected constructor(client: Client, options?: CollectorOptions<[Value, ...Extras]>); private _timeout: NodeJS.Timeout | null; private _idletimeout: NodeJS.Timeout | null; @@ -1965,11 +1968,7 @@ export class InteractionCallbackResource { public type: InteractionResponseType; } -export class InteractionCollector extends Collector< - Snowflake, - Interaction, - [Collection] -> { +export class InteractionCollector extends Collector { public constructor(client: Client, options?: InteractionCollectorOptions); private _handleMessageDeletion(message: Message): void; private _handleChannelDeletion(channel: NonThreadGuildBasedChannel): void; @@ -2710,7 +2709,16 @@ export class PollAnswer extends Base { public fetchVoters(options?: BaseFetchPollAnswerVotersOptions): Promise>; } -export class ReactionCollector extends Collector { +export interface ReactionCollectorEventTypes extends CollectorEventTypes { + remove: [reaction: MessageReaction, user: User]; +} + +export class ReactionCollector extends Collector< + Snowflake | string, + MessageReaction, + [User], + ReactionCollectorEventTypes +> { public constructor(message: Message, options?: ReactionCollectorOptions); private _handleChannelDeletion(channel: NonThreadGuildBasedChannel): void; private _handleGuildDeletion(guild: Guild): void; @@ -2986,11 +2994,11 @@ export class ShardClientUtil { public static shardIdForGuildId(guildId: Snowflake, shardCount: number): number; } -export interface ShardingManagerEvents { +export interface ShardingManagerEventTypes { shardCreate: [shard: Shard]; } -export class ShardingManager extends AsyncEventEmitter { +export class ShardingManager extends AsyncEventEmitter { public constructor(file: string, options?: ShardingManagerOptions); private _performOnShards(method: string, args: readonly unknown[]): Promise; private _performOnShards(method: string, args: readonly unknown[], shard: number): Promise; @@ -5071,7 +5079,7 @@ export type OmitPartialGroupDMChannel = channel: Exclude; }; -export interface ClientEvents { +export interface ClientEventTypes { applicationCommandPermissionsUpdate: [data: ApplicationCommandPermissionsUpdateData]; autoModerationActionExecution: [autoModerationActionExecution: AutoModerationActionExecution]; autoModerationRuleCreate: [autoModerationRule: AutoModerationRule]; @@ -6085,7 +6093,7 @@ export type CollectedInteraction = export interface InteractionCollectorOptions< Interaction extends CollectedInteraction, Cached extends CacheType = CacheType, -> extends CollectorOptions<[Interaction, Collection]> { +> extends CollectorOptions<[Interaction]> { channel?: TextBasedChannelResolvable; componentType?: ComponentType; guild?: GuildResolvable; diff --git a/packages/discord.js/typings/index.test-d.ts b/packages/discord.js/typings/index.test-d.ts index 3b40d874e024..f76dfab01ce7 100644 --- a/packages/discord.js/typings/index.test-d.ts +++ b/packages/discord.js/typings/index.test-d.ts @@ -473,6 +473,10 @@ client.on('messageCreate', async message => { expectAssignable>(channel.awaitMessageComponent({ componentType: ComponentType.Button })); expectAssignable>(buttonCollector); + buttonCollector.on('collect', (...args) => expectType<[ButtonInteraction]>(args)); + buttonCollector.on('dispose', (...args) => expectType<[ButtonInteraction]>(args)); + buttonCollector.on('end', (...args) => expectType<[ReadonlyCollection, string]>(args)); + // Verify that select menus interaction are inferred. const selectMenuCollector = message.createMessageComponentCollector({ componentType: ComponentType.StringSelect }); expectAssignable>( @@ -483,12 +487,24 @@ client.on('messageCreate', async message => { ); expectAssignable>(selectMenuCollector); + selectMenuCollector.on('collect', (...args) => expectType<[SelectMenuInteraction]>(args)); + selectMenuCollector.on('dispose', (...args) => expectType<[SelectMenuInteraction]>(args)); + selectMenuCollector.on('end', (...args) => + expectType<[ReadonlyCollection, string]>(args), + ); + // Verify that message component interactions are default collected types. const defaultCollector = message.createMessageComponentCollector(); expectAssignable>(message.awaitMessageComponent()); expectAssignable>(channel.awaitMessageComponent()); expectAssignable>(defaultCollector); + defaultCollector.on('collect', (...args) => expectType<[MessageComponentInteraction]>(args)); + defaultCollector.on('dispose', (...args) => expectType<[MessageComponentInteraction]>(args)); + defaultCollector.on('end', (...args) => + expectType<[ReadonlyCollection, string]>(args), + ); + // Verify that additional options don't affect default collector types. const semiDefaultCollector = message.createMessageComponentCollector({ time: 10000 }); expectType>(semiDefaultCollector); @@ -1419,6 +1435,11 @@ reactionCollector.on('dispose', (...args) => { expectType<[MessageReaction, User]>(args); }); +reactionCollector.on('collect', (...args) => expectType<[MessageReaction, User]>(args)); +reactionCollector.on('dispose', (...args) => expectType<[MessageReaction, User]>(args)); +reactionCollector.on('remove', (...args) => expectType<[MessageReaction, User]>(args)); +reactionCollector.on('end', (...args) => expectType<[ReadonlyCollection, string]>(args)); + (async () => { for await (const value of reactionCollector) { expectType<[MessageReaction, User]>(value);