Skip to content

Commit cc74ecb

Browse files
committed
feat: redis gateway
1 parent ddafcc4 commit cc74ecb

File tree

6 files changed

+165
-8
lines changed

6 files changed

+165
-8
lines changed

packages/redis-gateway/README.md

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,18 +22,52 @@
2222

2323
## Usage
2424

25-
Quickly spin up an instance:
25+
Set up an `.env` file:
26+
27+
```
28+
REDIS_URL=redis://localhost:6379
29+
DISCORD_TOKEN=your-token-here
30+
DISCORD_PROXY_URL=htt://localhost:8080 # if you want to use an HTTP proxy for DAPI calls (optional)
31+
INTENTS=0 # intents to use (optional, defaults to none)
32+
SHARD_COUNT=1 # number of total shards your bot should be running (optional, defaults to Discord recommended count)
33+
SHARD_IDS=0 # comma-separated list of shard IDs to run (optional, defaults to all shards)
34+
SHARDS_PER_WORKER=2 # number of shards per worker_thread or "all" (optional, if not specified, all shards will be run in the main thread)
35+
```
2636

27-
<!-- TODO: args -->
37+
Quickly spin up an instance:
2838

29-
`docker run -d --restart unless-stopped --name gateway discordjs/redis-gateway`
39+
`docker run -d --restart unless-stopped --env-file .env --name gateway discordjs/redis-gateway`
3040

3141
Use it:
3242

33-
```ts
34-
// TODO
43+
```js
44+
import Redis from 'ioredis';
45+
import { PubSubRedisBroker } from '@discordjs/brokers';
46+
import { GatewayDispatchEvents } from 'discord-api-types/v10';
47+
48+
const redis = new Redis();
49+
const broker = new PubSubRedisBroker({ redisClient: redis, encode, decode });
50+
51+
broker.on(GatewayDispatchEvents.InteractionCreate, async ({ data: interaction, ack }) => {
52+
if (interaction.type !== InteractionType.ApplicationCommand) {
53+
return;
54+
}
55+
56+
if (interaction.data.name === 'ping') {
57+
// reply with pong using your favorite Discord API library
58+
}
59+
60+
await ack();
61+
});
3562
```
3663

64+
For TypeScript usage, you can pass in a gereric type to the `PubSubRedisBroker` to map out all the events,
65+
refer to [this container's implementation](https://github.com/discordjs/discord.js/tree/main/packages/redis-gateway/src/index.ts#L15) for reference.
66+
67+
Also note that [core](https://github.com/discordjs/discord.js/tree/main/packages/core) supports an
68+
abstract `gateway` property that can be easily implemented, making this pretty comfortable to
69+
use in conjunction. Refer to the [Gateway documentation](https://discord.js.org/docs/packages/core/main/Gateway:Interface)
70+
3771
## Links
3872

3973
- [Website][website] ([source][website-source])

packages/redis-gateway/package.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,13 @@
4444
},
4545
"homepage": "https://discord.js.org",
4646
"dependencies": {
47-
"tslib": "^2.5.0"
47+
"@discordjs/brokers": "workspace:^",
48+
"@discordjs/rest": "workspace:^",
49+
"@discordjs/ws": "workspace:^",
50+
"discord-api-types": "^0.37.41",
51+
"ioredis": "^5.3.2",
52+
"tslib": "^2.5.0",
53+
"undici": "^5.22.0"
4854
},
4955
"devDependencies": {
5056
"@types/node": "16.18.25",
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import type { GatewayDispatchEvents, GatewayDispatchPayload, GatewaySendPayload } from 'discord-api-types/v10';
2+
3+
// need this to be its own type for some reason, the compiler doesn't behave the same way if we in-line it
4+
type _DiscordEvents = {
5+
[K in GatewayDispatchEvents]: GatewayDispatchPayload & {
6+
t: K;
7+
};
8+
};
9+
10+
export type DiscordEvents = {
11+
// @ts-expect-error - unclear why this ignore is needed, might be because dapi-types is missing some events from the union again
12+
[K in keyof _DiscordEvents]: _DiscordEvents[K]['d'];
13+
} & {
14+
gateway_send: GatewaySendPayload;
15+
};

packages/redis-gateway/src/env.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import process from 'node:process';
2+
import type { GatewayIntentBits } from 'discord-api-types/v10';
3+
4+
export class Env {
5+
public readonly redisUrl: string = process.env.REDIS_URL!;
6+
7+
public readonly discordToken: string = process.env.DISCORD_TOKEN!;
8+
9+
public readonly discordProxyURL: string | null = process.env.DISCORD_PROXY_URL ?? null;
10+
11+
public readonly intents: GatewayIntentBits | 0 = Number(process.env.INTENTS ?? 0);
12+
13+
public readonly shardCount: number | null = process.env.SHARD_COUNT ? Number(process.env.SHARD_COUNT) : null;
14+
15+
public readonly shardIds: number[] | null = process.env.SHARD_IDS
16+
? process.env.SHARD_IDS.split(',').map(Number)
17+
: null;
18+
19+
public readonly shardsPerWorker: number | 'all' | null =
20+
process.env.SHARDS_PER_WORKER === 'all'
21+
? 'all'
22+
: process.env.SHARDS_PER_WORKER
23+
? Number(process.env.SHARDS_PER_WORKER)
24+
: null;
25+
26+
private readonly REQUIRED_ENV_VARS = ['REDIS_URL', 'DISCORD_TOKEN'] as const;
27+
28+
public constructor() {
29+
for (const key of this.REQUIRED_ENV_VARS) {
30+
if (!(key in process.env)) {
31+
throw new Error(`Missing required environment variable: ${key}`);
32+
}
33+
}
34+
}
35+
}

packages/redis-gateway/src/index.ts

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,62 @@
1-
console.log('Hello, from @discordjs/redis-gateway');
1+
import { randomBytes } from 'node:crypto';
2+
import { PubSubRedisBroker } from '@discordjs/brokers';
3+
import type { RESTOptions } from '@discordjs/rest';
4+
import { REST } from '@discordjs/rest';
5+
import type { OptionalWebSocketManagerOptions, RequiredWebSocketManagerOptions } from '@discordjs/ws';
6+
import { WorkerShardingStrategy, CompressionMethod, WebSocketManager, WebSocketShardEvents } from '@discordjs/ws';
7+
import Redis from 'ioredis';
8+
import { ProxyAgent } from 'undici';
9+
import type { DiscordEvents } from './discordEvents.js';
10+
import { Env } from './env.js';
11+
12+
const env = new Env();
13+
14+
const redisClient = new Redis(env.redisUrl);
15+
const broker = new PubSubRedisBroker<DiscordEvents>({
16+
redisClient,
17+
});
18+
19+
const restOptions: Partial<RESTOptions> = {};
20+
if (env.discordProxyURL) {
21+
restOptions.api = `${env.discordProxyURL}/api`;
22+
}
23+
24+
const rest = new REST(restOptions).setToken(env.discordToken);
25+
if (env.discordProxyURL) {
26+
rest.setAgent(new ProxyAgent(env.discordProxyURL));
27+
}
28+
29+
const gatewayOptions: Partial<OptionalWebSocketManagerOptions> & RequiredWebSocketManagerOptions = {
30+
token: env.discordToken,
31+
rest,
32+
intents: env.intents,
33+
compression: CompressionMethod.ZlibStream,
34+
shardCount: env.shardCount,
35+
shardIds: env.shardIds,
36+
};
37+
if (env.shardsPerWorker) {
38+
gatewayOptions.buildStrategy = (manager) =>
39+
new WorkerShardingStrategy(manager, { shardsPerWorker: env.shardsPerWorker! });
40+
}
41+
42+
const gateway = new WebSocketManager(gatewayOptions);
43+
44+
gateway
45+
.on(WebSocketShardEvents.Debug, ({ message, shardId }) => console.log(`[WS Shard ${shardId}] [DEBUG]`, message))
46+
.on(WebSocketShardEvents.Hello, ({ shardId }) => console.log(`[WS Shard ${shardId}] [HELLO]`))
47+
.on(WebSocketShardEvents.Ready, ({ shardId }) => console.log(`[WS Shard ${shardId}] [READY]`))
48+
.on(WebSocketShardEvents.Resumed, ({ shardId }) => console.log(`[WS Shard ${shardId}] [RESUMED]`))
49+
.on(WebSocketShardEvents.Dispatch, ({ data }) => void broker.publish(data.t, data.d));
50+
51+
broker.on('gateway_send', async ({ data, ack }) => {
52+
for (const shardId of await gateway.getShardIds()) {
53+
await gateway.send(shardId, data);
54+
}
55+
56+
await ack();
57+
});
58+
59+
// we use a random group name because we don't want work-balancing,
60+
// we need this to be fanned out so all shards get the payload
61+
await broker.subscribe(randomBytes(16).toString('hex'), ['gateway_send']);
62+
await gateway.connect();

yarn.lock

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2019,7 +2019,7 @@ __metadata:
20192019
languageName: unknown
20202020
linkType: soft
20212021

2022-
"@discordjs/brokers@workspace:packages/brokers":
2022+
"@discordjs/brokers@workspace:^, @discordjs/brokers@workspace:packages/brokers":
20232023
version: 0.0.0-use.local
20242024
resolution: "@discordjs/brokers@workspace:packages/brokers"
20252025
dependencies:
@@ -2317,16 +2317,22 @@ __metadata:
23172317
version: 0.0.0-use.local
23182318
resolution: "@discordjs/redis-gateway@workspace:packages/redis-gateway"
23192319
dependencies:
2320+
"@discordjs/brokers": "workspace:^"
2321+
"@discordjs/rest": "workspace:^"
2322+
"@discordjs/ws": "workspace:^"
23202323
"@types/node": 16.18.25
23212324
cross-env: ^7.0.3
2325+
discord-api-types: ^0.37.41
23222326
eslint: ^8.39.0
23232327
eslint-config-neon: ^0.1.46
23242328
eslint-formatter-pretty: ^5.0.0
2329+
ioredis: ^5.3.2
23252330
prettier: ^2.8.8
23262331
tslib: ^2.5.0
23272332
tsup: ^6.7.0
23282333
turbo: ^1.9.4-canary.9
23292334
typescript: ^5.0.4
2335+
undici: ^5.22.0
23302336
languageName: unknown
23312337
linkType: soft
23322338

0 commit comments

Comments
 (0)