Skip to content

Commit 73ba97a

Browse files
committed
refactor(core): allow easily supporting redis gateway
BREAKING CHANGE: the client now requires a shardCount prop
1 parent 87dee70 commit 73ba97a

File tree

11 files changed

+131
-177
lines changed

11 files changed

+131
-177
lines changed

packages/core/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ const gateway = new WebSocketManager({
4848
});
4949

5050
// Create a client to emit relevant events.
51-
const client = new Client({ rest, gateway });
51+
const client = new Client({ rest, gateway, shardCount: await gateway.getShardCount() });
5252

5353
// Listen for interactions
5454
// Each event contains an `api` prop along with the event data that allows you to interface with the Discord REST API

packages/core/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
"discord-api-types": "^0.37.41"
6363
},
6464
"devDependencies": {
65+
"@discordjs/brokers": "workspace:^",
6566
"@favware/cliff-jumper": "^2.0.0",
6667
"@microsoft/api-extractor": "^7.34.8",
6768
"@types/node": "18.16.5",

packages/core/src/client.ts

Lines changed: 29 additions & 143 deletions
Original file line numberDiff line numberDiff line change
@@ -4,75 +4,16 @@ import { calculateShardId } from '@discordjs/util';
44
import { WebSocketShardEvents } from '@discordjs/ws';
55
import { DiscordSnowflake } from '@sapphire/snowflake';
66
import { AsyncEventEmitter } from '@vladfrangu/async_event_emitter';
7-
import {
8-
GatewayDispatchEvents,
9-
GatewayOpcodes,
10-
type APIGuildMember,
11-
type GatewayAutoModerationActionExecutionDispatchData,
12-
type GatewayAutoModerationRuleCreateDispatchData,
13-
type GatewayAutoModerationRuleDeleteDispatchData,
14-
type GatewayAutoModerationRuleUpdateDispatchData,
15-
type GatewayChannelCreateDispatchData,
16-
type GatewayChannelDeleteDispatchData,
17-
type GatewayChannelPinsUpdateDispatchData,
18-
type GatewayChannelUpdateDispatchData,
19-
type GatewayGuildAuditLogEntryCreateDispatchData,
20-
type GatewayGuildBanAddDispatchData,
21-
type GatewayGuildBanRemoveDispatchData,
22-
type GatewayGuildCreateDispatchData,
23-
type GatewayGuildDeleteDispatchData,
24-
type GatewayGuildEmojisUpdateDispatchData,
25-
type GatewayGuildIntegrationsUpdateDispatchData,
26-
type GatewayGuildMemberAddDispatchData,
27-
type GatewayGuildMemberRemoveDispatchData,
28-
type GatewayGuildMemberUpdateDispatchData,
29-
type GatewayGuildMembersChunkDispatchData,
30-
type GatewayGuildRoleCreateDispatchData,
31-
type GatewayGuildRoleDeleteDispatchData,
32-
type GatewayGuildRoleUpdateDispatchData,
33-
type GatewayGuildScheduledEventCreateDispatchData,
34-
type GatewayGuildScheduledEventDeleteDispatchData,
35-
type GatewayGuildScheduledEventUpdateDispatchData,
36-
type GatewayGuildScheduledEventUserAddDispatchData,
37-
type GatewayGuildScheduledEventUserRemoveDispatchData,
38-
type GatewayGuildStickersUpdateDispatchData,
39-
type GatewayGuildUpdateDispatchData,
40-
type GatewayIntegrationCreateDispatchData,
41-
type GatewayIntegrationDeleteDispatchData,
42-
type GatewayIntegrationUpdateDispatchData,
43-
type GatewayInteractionCreateDispatchData,
44-
type GatewayInviteCreateDispatchData,
45-
type GatewayInviteDeleteDispatchData,
46-
type GatewayMessageCreateDispatchData,
47-
type GatewayMessageDeleteBulkDispatchData,
48-
type GatewayMessageDeleteDispatchData,
49-
type GatewayMessageReactionAddDispatchData,
50-
type GatewayMessageReactionRemoveAllDispatchData,
51-
type GatewayMessageReactionRemoveDispatchData,
52-
type GatewayMessageReactionRemoveEmojiDispatchData,
53-
type GatewayMessageUpdateDispatchData,
54-
type GatewayPresenceUpdateData,
55-
type GatewayPresenceUpdateDispatchData,
56-
type GatewayReadyDispatchData,
57-
type GatewayRequestGuildMembersData,
58-
type GatewayStageInstanceCreateDispatchData,
59-
type GatewayStageInstanceDeleteDispatchData,
60-
type GatewayStageInstanceUpdateDispatchData,
61-
type GatewayThreadCreateDispatchData,
62-
type GatewayThreadDeleteDispatchData,
63-
type GatewayThreadListSyncDispatchData,
64-
type GatewayThreadMemberUpdateDispatchData,
65-
type GatewayThreadMembersUpdateDispatchData,
66-
type GatewayThreadUpdateDispatchData,
67-
type GatewayTypingStartDispatchData,
68-
type GatewayUserUpdateDispatchData,
69-
type GatewayVoiceServerUpdateDispatchData,
70-
type GatewayVoiceStateUpdateData,
71-
type GatewayVoiceStateUpdateDispatchData,
72-
type GatewayWebhooksUpdateDispatchData,
7+
import { GatewayDispatchEvents, GatewayOpcodes } from 'discord-api-types/v10';
8+
import type {
9+
GatewayDispatchPayload,
10+
APIGuildMember,
11+
GatewayRequestGuildMembersData,
12+
GatewayPresenceUpdateData,
13+
GatewayVoiceStateUpdateData,
7314
} from 'discord-api-types/v10';
74-
import type { Gateway } from './Gateway.js';
7515
import { API } from './api/index.js';
16+
import type { Gateway } from './gateway/Gateway.js';
7617

7718
export interface IntrinsicProps {
7819
/**
@@ -89,98 +30,43 @@ export interface WithIntrinsicProps<T> extends IntrinsicProps {
8930
data: T;
9031
}
9132

92-
export interface MappedEvents {
93-
[GatewayDispatchEvents.AutoModerationActionExecution]: [
94-
WithIntrinsicProps<GatewayAutoModerationActionExecutionDispatchData>,
95-
];
96-
[GatewayDispatchEvents.AutoModerationRuleCreate]: [WithIntrinsicProps<GatewayAutoModerationRuleCreateDispatchData>];
97-
[GatewayDispatchEvents.AutoModerationRuleDelete]: [WithIntrinsicProps<GatewayAutoModerationRuleDeleteDispatchData>];
98-
[GatewayDispatchEvents.AutoModerationRuleUpdate]: [WithIntrinsicProps<GatewayAutoModerationRuleUpdateDispatchData>];
99-
[GatewayDispatchEvents.ChannelCreate]: [WithIntrinsicProps<GatewayChannelCreateDispatchData>];
100-
[GatewayDispatchEvents.ChannelDelete]: [WithIntrinsicProps<GatewayChannelDeleteDispatchData>];
101-
[GatewayDispatchEvents.ChannelPinsUpdate]: [WithIntrinsicProps<GatewayChannelPinsUpdateDispatchData>];
102-
[GatewayDispatchEvents.ChannelUpdate]: [WithIntrinsicProps<GatewayChannelUpdateDispatchData>];
103-
[GatewayDispatchEvents.GuildAuditLogEntryCreate]: [WithIntrinsicProps<GatewayGuildAuditLogEntryCreateDispatchData>];
104-
[GatewayDispatchEvents.GuildBanAdd]: [WithIntrinsicProps<GatewayGuildBanAddDispatchData>];
105-
[GatewayDispatchEvents.GuildBanRemove]: [WithIntrinsicProps<GatewayGuildBanRemoveDispatchData>];
106-
[GatewayDispatchEvents.GuildCreate]: [WithIntrinsicProps<GatewayGuildCreateDispatchData>];
107-
[GatewayDispatchEvents.GuildDelete]: [WithIntrinsicProps<GatewayGuildDeleteDispatchData>];
108-
[GatewayDispatchEvents.GuildEmojisUpdate]: [WithIntrinsicProps<GatewayGuildEmojisUpdateDispatchData>];
109-
[GatewayDispatchEvents.GuildIntegrationsUpdate]: [WithIntrinsicProps<GatewayGuildIntegrationsUpdateDispatchData>];
110-
[GatewayDispatchEvents.GuildMemberAdd]: [WithIntrinsicProps<GatewayGuildMemberAddDispatchData>];
111-
[GatewayDispatchEvents.GuildMemberRemove]: [WithIntrinsicProps<GatewayGuildMemberRemoveDispatchData>];
112-
[GatewayDispatchEvents.GuildMemberUpdate]: [WithIntrinsicProps<GatewayGuildMemberUpdateDispatchData>];
113-
[GatewayDispatchEvents.GuildMembersChunk]: [WithIntrinsicProps<GatewayGuildMembersChunkDispatchData>];
114-
[GatewayDispatchEvents.GuildRoleCreate]: [WithIntrinsicProps<GatewayGuildRoleCreateDispatchData>];
115-
[GatewayDispatchEvents.GuildRoleDelete]: [WithIntrinsicProps<GatewayGuildRoleDeleteDispatchData>];
116-
[GatewayDispatchEvents.GuildRoleUpdate]: [WithIntrinsicProps<GatewayGuildRoleUpdateDispatchData>];
117-
[GatewayDispatchEvents.GuildScheduledEventCreate]: [WithIntrinsicProps<GatewayGuildScheduledEventCreateDispatchData>];
118-
[GatewayDispatchEvents.GuildScheduledEventDelete]: [WithIntrinsicProps<GatewayGuildScheduledEventDeleteDispatchData>];
119-
[GatewayDispatchEvents.GuildScheduledEventUpdate]: [WithIntrinsicProps<GatewayGuildScheduledEventUpdateDispatchData>];
120-
[GatewayDispatchEvents.GuildScheduledEventUserAdd]: [
121-
WithIntrinsicProps<GatewayGuildScheduledEventUserAddDispatchData>,
122-
];
123-
[GatewayDispatchEvents.GuildScheduledEventUserRemove]: [
124-
WithIntrinsicProps<GatewayGuildScheduledEventUserRemoveDispatchData>,
125-
];
126-
[GatewayDispatchEvents.GuildStickersUpdate]: [WithIntrinsicProps<GatewayGuildStickersUpdateDispatchData>];
127-
[GatewayDispatchEvents.GuildUpdate]: [WithIntrinsicProps<GatewayGuildUpdateDispatchData>];
128-
[GatewayDispatchEvents.IntegrationCreate]: [WithIntrinsicProps<GatewayIntegrationCreateDispatchData>];
129-
[GatewayDispatchEvents.IntegrationDelete]: [WithIntrinsicProps<GatewayIntegrationDeleteDispatchData>];
130-
[GatewayDispatchEvents.IntegrationUpdate]: [WithIntrinsicProps<GatewayIntegrationUpdateDispatchData>];
131-
[GatewayDispatchEvents.InteractionCreate]: [WithIntrinsicProps<GatewayInteractionCreateDispatchData>];
132-
[GatewayDispatchEvents.InviteCreate]: [WithIntrinsicProps<GatewayInviteCreateDispatchData>];
133-
[GatewayDispatchEvents.InviteDelete]: [WithIntrinsicProps<GatewayInviteDeleteDispatchData>];
134-
[GatewayDispatchEvents.MessageCreate]: [WithIntrinsicProps<GatewayMessageCreateDispatchData>];
135-
[GatewayDispatchEvents.MessageDelete]: [WithIntrinsicProps<GatewayMessageDeleteDispatchData>];
136-
[GatewayDispatchEvents.MessageDeleteBulk]: [WithIntrinsicProps<GatewayMessageDeleteBulkDispatchData>];
137-
[GatewayDispatchEvents.MessageReactionAdd]: [WithIntrinsicProps<GatewayMessageReactionAddDispatchData>];
138-
[GatewayDispatchEvents.MessageReactionRemove]: [WithIntrinsicProps<GatewayMessageReactionRemoveDispatchData>];
139-
[GatewayDispatchEvents.MessageReactionRemoveAll]: [WithIntrinsicProps<GatewayMessageReactionRemoveAllDispatchData>];
140-
[GatewayDispatchEvents.MessageReactionRemoveEmoji]: [
141-
WithIntrinsicProps<GatewayMessageReactionRemoveEmojiDispatchData>,
142-
];
143-
[GatewayDispatchEvents.MessageUpdate]: [WithIntrinsicProps<GatewayMessageUpdateDispatchData>];
144-
[GatewayDispatchEvents.PresenceUpdate]: [WithIntrinsicProps<GatewayPresenceUpdateDispatchData>];
145-
[GatewayDispatchEvents.Ready]: [WithIntrinsicProps<GatewayReadyDispatchData>];
146-
[GatewayDispatchEvents.Resumed]: [WithIntrinsicProps<never>];
147-
[GatewayDispatchEvents.StageInstanceCreate]: [WithIntrinsicProps<GatewayStageInstanceCreateDispatchData>];
148-
[GatewayDispatchEvents.StageInstanceDelete]: [WithIntrinsicProps<GatewayStageInstanceDeleteDispatchData>];
149-
[GatewayDispatchEvents.StageInstanceUpdate]: [WithIntrinsicProps<GatewayStageInstanceUpdateDispatchData>];
150-
[GatewayDispatchEvents.ThreadCreate]: [WithIntrinsicProps<GatewayThreadCreateDispatchData>];
151-
[GatewayDispatchEvents.ThreadDelete]: [WithIntrinsicProps<GatewayThreadDeleteDispatchData>];
152-
[GatewayDispatchEvents.ThreadListSync]: [WithIntrinsicProps<GatewayThreadListSyncDispatchData>];
153-
[GatewayDispatchEvents.ThreadMemberUpdate]: [WithIntrinsicProps<GatewayThreadMemberUpdateDispatchData>];
154-
[GatewayDispatchEvents.ThreadMembersUpdate]: [WithIntrinsicProps<GatewayThreadMembersUpdateDispatchData>];
155-
[GatewayDispatchEvents.ThreadUpdate]: [WithIntrinsicProps<GatewayThreadUpdateDispatchData>];
156-
[GatewayDispatchEvents.TypingStart]: [WithIntrinsicProps<GatewayTypingStartDispatchData>];
157-
[GatewayDispatchEvents.UserUpdate]: [WithIntrinsicProps<GatewayUserUpdateDispatchData>];
158-
[GatewayDispatchEvents.VoiceServerUpdate]: [WithIntrinsicProps<GatewayVoiceServerUpdateDispatchData>];
159-
[GatewayDispatchEvents.VoiceStateUpdate]: [WithIntrinsicProps<GatewayVoiceStateUpdateDispatchData>];
160-
[GatewayDispatchEvents.WebhooksUpdate]: [WithIntrinsicProps<GatewayWebhooksUpdateDispatchData>];
161-
}
33+
// need this to be its own type for some reason, the compiler doesn't behave the same way if we in-line it
34+
type _DiscordEvents = {
35+
[K in GatewayDispatchEvents]: GatewayDispatchPayload & {
36+
t: K;
37+
};
38+
};
16239

163-
export type ManagerShardEventsMap = {
164-
[K in keyof MappedEvents]: MappedEvents[K];
40+
export type DiscordEvents = {
41+
// @ts-expect-error - unclear why this is an error, this behaves as expected
42+
[K in keyof _DiscordEvents]: _DiscordEvents[K]['d'];
43+
};
44+
45+
export type MappedEvents = {
46+
[K in keyof DiscordEvents]: [WithIntrinsicProps<DiscordEvents[K]>];
16547
};
16648

16749
export interface ClientOptions {
16850
gateway: Gateway;
16951
rest: REST;
52+
shardCount: number;
17053
}
17154

172-
export class Client extends AsyncEventEmitter<ManagerShardEventsMap> {
55+
export class Client extends AsyncEventEmitter<MappedEvents> {
17356
public readonly rest: REST;
17457

17558
public readonly gateway: Gateway;
17659

17760
public readonly api: API;
17861

179-
public constructor({ rest, gateway }: ClientOptions) {
62+
public readonly shardCount: number;
63+
64+
public constructor({ rest, gateway, shardCount }: ClientOptions) {
18065
super();
18166
this.rest = rest;
18267
this.gateway = gateway;
18368
this.api = new API(rest);
69+
this.shardCount = shardCount;
18470

18571
this.gateway.on(WebSocketShardEvents.Dispatch, ({ data: dispatch, shardId }) => {
18672
// @ts-expect-error event props can't be resolved properly, but they are correct
@@ -196,7 +82,7 @@ export class Client extends AsyncEventEmitter<ManagerShardEventsMap> {
19682
* @param timeout - The timeout for waiting for each guild members chunk event
19783
*/
19884
public async requestGuildMembers(options: GatewayRequestGuildMembersData, timeout = 10_000) {
199-
const shardId = calculateShardId(options.guild_id, await this.gateway.getShardCount());
85+
const shardId = calculateShardId(options.guild_id, this.shardCount);
20086
const nonce = options.nonce ?? DiscordSnowflake.generate().toString();
20187

20288
const promise = new Promise<APIGuildMember[]>((resolve, reject) => {
@@ -241,7 +127,7 @@ export class Client extends AsyncEventEmitter<ManagerShardEventsMap> {
241127
* @param options - The options for updating the voice state
242128
*/
243129
public async updateVoiceState(options: GatewayVoiceStateUpdateData) {
244-
const shardId = calculateShardId(options.guild_id, await this.gateway.getShardCount());
130+
const shardId = calculateShardId(options.guild_id, this.shardCount);
245131

246132
await this.gateway.send(shardId, {
247133
op: GatewayOpcodes.VoiceStateUpdate,

packages/core/src/Gateway.ts renamed to packages/core/src/gateway/Gateway.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,6 @@ import type { GatewaySendPayload } from 'discord-api-types/v10';
66
* A Discord gateway-like interface that can be used to send & recieve events.
77
*/
88
export interface Gateway {
9-
/**
10-
* Gets how many shards your bot is running.
11-
*/
12-
getShardCount(): Awaitable<number>;
139
on(
1410
event: WebSocketShardEvents.Dispatch,
1511
listener: (...params: ManagerShardEventsMap[WebSocketShardEvents.Dispatch]) => Awaitable<void>,
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import type { PubSubRedisBroker } from '@discordjs/brokers';
2+
import type { ManagerShardEventsMap, WebSocketShardEvents } from '@discordjs/ws';
3+
import { AsyncEventEmitter } from '@vladfrangu/async_event_emitter';
4+
import type { GatewaySendPayload, GatewayDispatchEvents } from 'discord-api-types/v10';
5+
import type { DiscordEvents } from '../client.js';
6+
import type { Gateway } from './Gateway.js';
7+
8+
interface BrokerIntrinsicProps {
9+
shardId: number;
10+
}
11+
12+
interface Events extends DiscordEvents {
13+
gateway_send: GatewaySendPayload;
14+
}
15+
16+
export type RedisBrokerDiscordEvents = {
17+
[K in keyof Events]: BrokerIntrinsicProps & { payload: Events[K] };
18+
};
19+
20+
export class RedisGateway
21+
extends AsyncEventEmitter<{ dispatch: ManagerShardEventsMap[WebSocketShardEvents.Dispatch] }>
22+
implements Gateway
23+
{
24+
public constructor(private readonly broker: PubSubRedisBroker<RedisBrokerDiscordEvents>) {
25+
super();
26+
}
27+
28+
public async send(shardId: number, payload: GatewaySendPayload): Promise<void> {
29+
await this.broker.publish('gateway_send', { payload, shardId });
30+
}
31+
32+
public async init(group: string, events: GatewayDispatchEvents[]) {
33+
for (const event of events) {
34+
this.broker.on(event, ({ data: { payload, shardId }, ack }) => {
35+
// @ts-expect-error - Union shenanigans
36+
this.emit('dispatch', { shardId, data: payload });
37+
void ack();
38+
});
39+
}
40+
41+
await this.broker.subscribe(group, events);
42+
}
43+
}

packages/core/src/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
export * from './api/index.js';
2-
export * from './client.js';
2+
export * from './gateway/Gateway.js';
3+
export * from './gateway/RedisGateway.js';
34
export * from './util/index.js';
5+
export * from './client.js';
46

57
export * from 'discord-api-types/v10';
68

packages/redis-gateway/README.md

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -70,14 +70,54 @@ await broker.publish('gateway_send', {
7070
status: PresenceUpdateStatus.Online,
7171
},
7272
});
73+
74+
// if you were to start this process multiple times (e.g. multiple apps using 'work_balancing_group'),
75+
// they would automatically work balance those interaction create events
76+
await broker.subscribe('work_balancing_group', [GatewayDispatchEvents.InteractionCreate]);
7377
```
7478

75-
For TypeScript usage, you can pass in a gereric type to the `PubSubRedisBroker` to map out all the events,
76-
refer to [this container's implementation](https://github.com/discordjs/discord.js/tree/main/packages/redis-gateway/src/index.ts#L15) for reference.
79+
For TypeScript usage, you can pass in a gereric type to the `PubSubRedisBroker` to map out all the events, a mapped
80+
interface is available in `@discordjs/core` as `RedisBrokerDiscordEvents`.
81+
82+
If you wish, you can also just use `@discordjs/core`:
83+
84+
```ts
85+
import { REST } from '@discordjs/rest';
86+
import Redis from 'ioredis';
87+
import { PubSubRedisBroker } from '@discordjs/brokers';
88+
import {
89+
GatewayDispatchEvents,
90+
GatewayIntentBits,
91+
InteractionType,
92+
MessageFlags,
93+
Client,
94+
RedisGateway,
95+
} from '@discordjs/core';
96+
97+
const rest = new REST({ version: '10' }).setToken(token);
98+
99+
const redis = new Redis();
100+
const broker = new PubSubRedisBroker({ redisClient: redis });
101+
const gateway = new RedisGateway(broker);
102+
103+
const client = new Client({
104+
rest,
105+
gateway,
106+
// you can get this however you want, it's used for some calculations and should be your bot's TOTAL shard count
107+
// across "clusters" or anything else.
108+
shardCount: Number(process.env.SHARD_COUNT!),
109+
});
110+
111+
client.on(GatewayDispatchEvents.InteractionCreate, async ({ data: interaction, api }) => {
112+
if (interaction.type !== InteractionType.ApplicationCommand || interaction.data.name !== 'ping') {
113+
return;
114+
}
115+
116+
await api.interactions.reply(interaction.id, interaction.token, { content: 'Pong!', flags: MessageFlags.Ephemeral });
117+
});
77118

78-
Also note that [core](https://github.com/discordjs/discord.js/tree/main/packages/core) supports an
79-
abstract `gateway` property that can be easily implemented, making this pretty comfortable to
80-
use in conjunction. Refer to the [Gateway documentation](https://discord.js.org/docs/packages/core/main/Gateway:Interface).
119+
await gateway.init('work_balancing_group', [GatewayDispatchEvents.InteractionCreate]);
120+
```
81121

82122
## Links
83123

packages/redis-gateway/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
"undici": "^5.22.0"
5454
},
5555
"devDependencies": {
56+
"@discordjs/core": "workspace:^",
5657
"@types/node": "16.18.25",
5758
"cross-env": "^7.0.3",
5859
"eslint": "^8.39.0",

packages/redis-gateway/src/discordEvents.ts

Lines changed: 0 additions & 15 deletions
This file was deleted.

0 commit comments

Comments
 (0)