Skip to content

Commit e670888

Browse files
committed
feat(RedisBroker): discordjs/core gateway impl
1 parent 9d5c6b4 commit e670888

File tree

4 files changed

+149
-0
lines changed

4 files changed

+149
-0
lines changed

packages/brokers/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@
7373
},
7474
"devDependencies": {
7575
"@discordjs/api-extractor": "workspace:^",
76+
"@discordjs/core": "workspace:^",
77+
"@discordjs/ws": "workspace:^",
7678
"@discordjs/scripts": "workspace:^",
7779
"@favware/cliff-jumper": "^4.1.0",
7880
"@types/node": "^22.18.13",
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import type { Gateway, GatewayDispatchPayload, GatewayDispatchEvents, GatewaySendPayload } from '@discordjs/core';
2+
import type { ManagerShardEventsMap, WebSocketShardEvents } from '@discordjs/ws';
3+
import { AsyncEventEmitter } from '@vladfrangu/async_event_emitter';
4+
import type { PubSubRedisBroker } from '@discordjs/brokers';
5+
6+
// need this to be its own type for some reason, the compiler doesn't behave the same way if we in-line it
7+
type _DiscordEvents = {
8+
[K in GatewayDispatchEvents]: GatewayDispatchPayload & {
9+
t: K;
10+
};
11+
};
12+
13+
export type DiscordEvents = {
14+
[K in keyof _DiscordEvents]: _DiscordEvents[K]['d'];
15+
};
16+
17+
interface BrokerIntrinsicProps {
18+
shardId: number;
19+
}
20+
21+
interface Events extends DiscordEvents {
22+
// eslint-disable-next-line @typescript-eslint/no-use-before-define
23+
[RedisGateway.GatewaySendEvent]: GatewaySendPayload;
24+
}
25+
26+
export type RedisBrokerDiscordEvents = {
27+
[K in keyof Events]: BrokerIntrinsicProps & { payload: Events[K] };
28+
};
29+
30+
/**
31+
* RedisGateway is an implementation for core's Gateway interface built on top of our Redis brokers.
32+
*
33+
* Some important notes:
34+
* - Instances for this class are for your consumers/services that need the gateway. naturally, the events passed into
35+
* `init` are the only ones the core client will be able to emit
36+
* - You can also opt to use the class as-is without `@discordjs/core`, if you so desire. Events are properly typed
37+
* - You need to implement your own gateway service. Refer to the example below for how that would look like. This class
38+
* offers some static methods and properties that help in this errand. It is extremely important that you `publish`
39+
* events as the receiving service expects, and also that you handle GatewaySend events.
40+
*
41+
* @example
42+
* ```ts
43+
* // gateway-service/index.ts
44+
* import { RedisGateway, PubSubRedisBroker, kUseRandomGroupName } from '@discordjs/brokers';
45+
* import Redis from 'ioredis';
46+
*
47+
* // the `name` here probably should be env-determined if you need to scale this. see the main README for more information.
48+
* // also, we use a random group because we do NOT want work-balancing on gateway_send events.
49+
* const broker = new PubSubRedisBroker(new Redis(), { group: kUseRandomGroupName, name: 'send-consumer-1' });
50+
* const gateway = new WebSocketManager(gatewayOptionsHere); // see @discordjs/ws for examples.
51+
*
52+
* // emit events over the broker
53+
* gateway.on(WebSocketShardEvents.Dispatch, (...data) => void broker.publish(RedisGateway.toPublishArgs(data));
54+
*
55+
* // listen to payloads we should send to Discord
56+
* broker.on(RedisGateway.GatewaySendEvent, async ({ data: { payload, shardId }, ack }) => {
57+
* await gateway.send(shardId, payload);
58+
* await ack();
59+
* });
60+
* await broker.subscribe([RedisGateway.GatewaySendEvent]);
61+
* await gateway.connect();
62+
* ```
63+
*
64+
* ```ts
65+
* // other-service/index.ts
66+
* import { RedisGateway, PubSubRedisBroker, kUseRandomGroupName } from '@discordjs/brokers';
67+
* import Redis from 'ioredis';
68+
*
69+
* // the name here should absolutely be env-determined, see the main README for more information.
70+
* const broker = new PubSubRedisBroker(new Redis(), { group: 'my-service-name', name: 'service-name-instance-1' });
71+
* // unfortunately, we have to know the shard count. ideally this should be an env var
72+
* const gateway = new RedisGateway(broker, Number.parseInt(process.env.SHARD_COUNT, 10));
73+
*
74+
* const rest = new REST({ version: '10' }).setToken(process.env.DISCORD_TOKEN);
75+
* const client = new Client({ rest, gateway });
76+
*
77+
* // set up your client as you normally would with core
78+
*
79+
* // subscribe to the events that you want
80+
* await gateway.init([GatewayDispatchEvents.GuildCreate, GatewayDispatchEvents.MessageCreate]);
81+
* ```
82+
*/
83+
export class RedisGateway
84+
extends AsyncEventEmitter<{ dispatch: ManagerShardEventsMap[WebSocketShardEvents.Dispatch] }>
85+
implements Gateway
86+
{
87+
/**
88+
* Event used over the broker used to tell shards to send a payload to Discord.
89+
*/
90+
public static readonly GatewaySendEvent = 'gateway_send' as const;
91+
92+
/**
93+
* Converts a dispatch event from `@discordjs/ws` to arguments for a `broker.publish` call.
94+
*/
95+
public static toPublishArgs(
96+
data: [payload: GatewayDispatchPayload, shardId: number],
97+
): [GatewayDispatchEvents, BrokerIntrinsicProps & { payload: GatewayDispatchPayload['d'] }] {
98+
const [payload, shardId] = data;
99+
return [payload.t, { shardId, payload: payload.d }];
100+
}
101+
102+
public constructor(
103+
private readonly broker: PubSubRedisBroker<RedisBrokerDiscordEvents>,
104+
private readonly shardCount: number,
105+
) {
106+
super();
107+
}
108+
109+
public getShardCount(): number {
110+
return this.shardCount;
111+
}
112+
113+
public async send(shardId: number, payload: GatewaySendPayload): Promise<void> {
114+
await this.broker.publish('gateway_send', { payload, shardId });
115+
}
116+
117+
public async init(events: GatewayDispatchEvents[]) {
118+
for (const event of events) {
119+
// async_event_emitter nukes our types on this one.
120+
this.broker.on(
121+
event,
122+
({
123+
ack,
124+
data: { payload, shardId },
125+
}: {
126+
// eslint-disable-next-line @typescript-eslint/method-signature-style
127+
ack: () => Promise<void>;
128+
data: BrokerIntrinsicProps & { payload: any };
129+
}) => {
130+
// @ts-expect-error - Union shenanigans
131+
this.emit('dispatch', { shardId, data: payload });
132+
void ack();
133+
},
134+
);
135+
}
136+
137+
await this.broker.subscribe(events);
138+
}
139+
}

packages/brokers/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export * from './brokers/redis/BaseRedis.js';
22
export * from './brokers/redis/PubSubRedis.js';
3+
export * from './brokers/redis/RedisGateway.js';
34
export * from './brokers/redis/RPCRedis.js';
45

56
export * from './brokers/Broker.js';

pnpm-lock.yaml

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)