Files
discordeno/apps/site/tutorial/big-bot-guide/gateway.md
Skillz4Killz 2714e1e7d1 BREAKING: node migration + major improvements and style changes (#2783)
* Setup turborepo (#2610)

* chore: BREAKING move to monorepo structure

* chore: setup turborepo

* setup more eslint and tsconfig (#2612)

* fix packages setting lcoation

* add dist to ignore

* style

* fix: .vscode set prettier and remove deno configs (#2611)

* fix: rewrite to process.env

* chore: add node types

* style: run eslint --fix

* fix: tests import ext

* chore: fix formatter

* chore: add build script

* chore: remove site from turborepo

* chore: move to seperate packages

* chore: seperate util

* chore: rename to index

* fix: utils

* chore: bump typescript

* fix: in process of fixing rest

* fix: logger

* style

* chore: fix turbo script

* fix: types

* fix: types

* fix: shard

* fix: gateway

* fixing: bot

* fix: at least it can run now

* chore: fix turbo script

* chore: move plugins

* chore: add type and utils export

* chore: working bot type and cache plugin

* Add git hooks (#2618)

* CI: Setup ci (#2669)

* ci: fix test

* ci: fix typo

* fix: turbo script

* fixes: yarn and linter errors in embeds pkg

* chore: fix yarn gitignore

* Node migration - devcontainer (#2672)

*  - feat: devcontainer -> node

* 👷‍♂ - ci: devcontainer - add tabnine, prettier

* fix: ignore .env and debug.ts

* fix: couple of linter errors

* fix: linter error

* fix: gateway linter errors

* style: update style

* style: fix bot style

* fix: type

* ci: move all old workflow

* chore: close #2619

* chore: close #2671

* test: add mocha

* chore: add typescript plugin

* test: add mocha

* test: add test to utils

* test/ci: update ci and coverage

* chore: change script naming

* ci: update include test

* test: add coverage

* ci: fix cache

* ci: fix ci and codecov

* Discordeno Documentation  (#2673)

* Add git hooks

* Add documentation generatation

* Change Documentation Engine

* Add documentation

* Remove autogenerated docs

* combine lint staged action into one

* style: fix

Co-authored-by: H01001000 <heiheiho000@gmail.com>

* chore: new package client

* test: add rest test

* ci: enable codecov

* fix: type

* test: add test to all packages

* ci: add release to gh per commit

* fix: ci syntax

* fix: package version

* fix: publish script

* fix: remove private from gateway

* ci: add filter for changes

* fix: ci syntex

* ci: try fix path filter

* ci: try fix path filter

* test: add test

* ci: fix string and  boolean

* fix: package and ci

* chore: fix turbo type cache

* ci: also publish to npm

* ci: change to public

* fix: not publish to npm

* fix: dependencies

* chore: fix fmt script

* fix: better rest typecheck Closes #2621

* fix: run yarn install

* feat: add transformers to rest

* feat: add helpers to rest

* test: move bot utils test

* reverse change to release.yml

* chore: add clean build

* refactor: discordeno

* chore: add import type

* chore: remove bot

* fix: change deps from bot to dd

* chore: update yarn lock

* test: temp remove test from logger

* refactor: remove transformers in helpers/channel

* type: close #2622

* ci(fix): explicitly define coverage file

* refactor: remove transformers in helpers/emoji

* type: fix discord guild type

* feat: DiscordEditAutomoderationRule type

* fix: remove unused type

* feat: DiscordCreateGuildEmoji DiscordModifyGuildEmoji & DiscordModifyCHannel types

* feat: DiscordCreateChannel DiscordBuDeleteMessages DiscordCreateMessage DiscordEditMessage

* feat: DiscordCreateScheduledEvent EditScheduledEvent DiscordCreateInvite

* fix: types for guild stuff for rest

* feat: thread discord types for rest

* feat: channel rest types

* feat: member rest types

* feat: more discord rest types

* fix: type errors

* fix: docs bot param name should be rest

* type: fix type error

* ci(fix): codecov

* chore(client) :add export transformer

* fix: verifySignature

* test: fix types

* test: add test prevent #2683 #2678

* fix: export transformer twice

* ci: reuse cache

* test: add test:unit-noTextCoverage

* feat: add transform and constant package

* fix: half fix #2683 #2688

* fix: #2688 fix all transformer.spec.ts

* fix: transformer name

* ci: update style

* fix: dependencies

* fix: naming

* fix: yarn lock

* fix: remove validations from helpers. Closes #2700

* fix: rest routes as a constants pkg

* fix: esm import with .js

* test: add exception case

* feat: adding transformer

* chore: change script name

* chore: add path fixing to coverage file

* fix: camelize tuple bug

* fix: reverse transformers

* fix: transformers folder to camel

* fix: transformers as an object

* fix: linter error

* ci: run test with deno close #2701

* fix: test depends on build

* fix: deno import node:crypto

* fix: rest improvements

* fix: channel type

* fix: export user

* chore: move to unit dir

* test: moving bench

* fix: remove transformers from rest package

* fix: move toggle transformers to bot pkg

* fix: move out gw helpers to gw package

* test: add rest e2e test

* fix: syntex

* ci: add discord token

* fix: ci not passing secret

* test: add role test

* ci: fix secret inherit

* fix: role helpers transformer

* test: increase timeout to 10s

* fix: member helpers transformer

* test: add member test

* fix: name and type

* fix: guild ban

* test: fix test

* fix: test add await

* test: skip some test

* test: increase timeout

* chore: add transformer to import map

* fix: test

* test: add why-is-node-still-running

* test: add debug hook

* ci: add timeout incase any async running

* test: try fix

* test: turn on rest debug

* fix: if undefined

* test: reduce timeout

* fix: queue not running after some request

* fix: increase remaining after request without ratelimit header

* fix: partial webhook

* fix: fetch hooks not working if debug defined later

* fix: nickname null to undefined

* test: finish adding webhook test

* refactor(test): move rest to utils

* fix: add await

* fix: sticker

* feat: add message embed component transformer

* fix: test not done

* fix: arg type to bigString

* fix: sendMessage

* fix: add allowedMentions, interactionResponse

* test: add emoji e2e test

* test: add guild e2e test

* refactor(test): remove extra rest call

* fix: create emoji BINARY_TYPE_INVALID_DATA_URI

* fix: create guild rate limit

* fix: automode rule helpers and test

* fix: test run in only

* test: add some queue bucket test

* test: remove empty test

* refactor(test): use new guild

* test: use new guild for other test

* fix: guild not defind

* reactor(test): remove duplicated creat channel

* test: increase timeout to 30s

* test: add thread test

* fix: more transformers

* fix: gateway helpers use transformers

* fix: types belong in types pkg

* fix: helpers use transformers

* BREAKING: v19 rewrite to node + major improvements (#2703)

* fix: move all to old folder

* fix: cleanup types

* fix: more cleanup

* fix: more base cleanup

* fix: token dotenv

* fix: add base transformer

* fix: partial error handling

* fix: handle 429 rate limit

* test(rest): fix unit test

* fix(script): transform extension

* test: fix error and buffer

* feat: camelizer util

* fix: cleanup

* fix: rate limit queues and headers processing

* fix: rest exports

* fix: no more transformers

* fix: queue header null bug

* fix: add gateway package base

* fix: lint error

* fix: add prettier file

* fix: prettier is default fmtr

* fix: fmt shard file

* fix: fmt

* fix: types issue

* fix: remove unused consts

* fix: all import issues

* fix: import error

* fix: import ending with .js

* fix: remove transformers package

* Fix eslint (#2710)

* fix: typing of button component label to be optional (#2708)

* feat: add guild_connections to role tags and toggles (#2706)

Co-authored-by: Skillz4Killz <23035000+Skillz4Killz@users.noreply.github.com>

* Add lint checks and autofixing workflow (#2702)

* Add lint checks and autofixing workflow

* Update lint.yml

* Fix: use yarn instead npm

* fix: add ts to eslint_extensions

* fix: update dir

* fix: lint.yml format

Co-authored-by: Jonathan Ho <heiheiho000@gmail.com>

* fix: unused deps

* ci: fix e2e test not running

* chore: run yarn install

* test: this should run test

* feat: getCHannel

* feat: createEMji helper

* feat: collection class in util package

* fix: gateway bugs

* why on earth is this needed change

* fix: cleanup docs on collection

* Emoji rest methods (node-migration-clean) (#2713)

* feat: all emoji rest methods

* Fix code style issues with ESLint

Co-authored-by: Lint Action <lint-action@samuelmeuli.com>

* fix: stuff

* fix: bot types

* fix: remove logs

* fix: camelize all gateway payloads

* fix: remove todos

* fix: start deris

* fix: lint/ts errors except shard file

* thats 1 way to fix type errors

* yes (#2714)

Co-authored-by: Skillz4Killz <23035000+Skillz4Killz@users.noreply.github.com>

* feat: rest channel helpers

* Update release.yml

* Update release.yml

* ci: add build filter

* ci: fail fast false

* fix: complete webhook related helpers

* fix: follow announcement helper

* add forum helper

* add stage helpers

* add thread related helpers

* alphabetize

* cleanup webhook routes

* automod helpers

* scheduled events

* integrations helpers

* invite stuff incomplete

* dm channel and avatar url

* chore: move ts-node into package

* fix: importing esm

* fix: tris message helpers

* test(rest): add simplifyUrl test

* test(rest): add checkRateLimits test

* test(rest): add processRateLimitedPaths test

* test(rest): fix missing beforeEach

* test(utils): enable old test

* interaction helpers

* perf(utils): optimize snake to camel case conversion (#2717)

* perf(utils): optimize snake to camel case conversion

* fmt

* wont change much but still faster

* actually this was a stupid idea

* shh

* Fix(client): Fix typings. (#2716)

Co-authored-by: Skillz4Killz <23035000+Skillz4Killz@users.noreply.github.com>

* test: fix rest and utils test (#2723)

* test(utils): fix  cant import collection

* test(rest): await expect

* fix(utils): deno compactability

* test(utils): typing

* fix(utils): add return type

* Add rest helpers for templates (#2727)

* Add rest helpers for templates

* Fix code style issues with ESLint

Co-authored-by: Lint Action <lint-action@samuelmeuli.com>

* fix: remove frozenAt

* fix: cleanup templates

* ci: release to npm (#2725)

* ci: release to npm

* Update release.yml

* 🐛 - fix: types - slashcommands - add nsfw prop (#2731)

* feat(rest helpers): Add template and member helpers. (#2728)

* feat(rest helpers): Add template and member helpers.

* format code

* feat(rest): add template routes

* fix(rest): routes and runMethod

* fix(rest): try to fix most of type

Co-authored-by: H01001000 <heiheiho000@gmail.com>
Co-authored-by: Skillz4Killz <23035000+Skillz4Killz@users.noreply.github.com>

* ci: fix release (#2732)

* fix: rest type errors

* fix: v19 begin

* fix: yarn lock

* chore: fix deps and script (#2733)

* fix: bug camelizer deleting letters

* fix: falsy token check

* fix: add frozenat check for queue

* fix: max stack trace error do to infinite loop

* fix: type error

* test(rest): fix missing import (#2734)

Co-authored-by: Skillz4Killz <23035000+Skillz4Killz@users.noreply.github.com>

* fix: file paths for imports

* fix: lastShardId should default to 0

* fix: use isomorphic ws

* test(rest): fix "TypeError: [Function] is not a thenable" (#2736)

* test(utils): add utils tests (#2737)

* test(utils): add urltobase64 test

* test(utils): add token test

* test(utils): fix missing import buffer

* test(utils): add casting test

* test(utils): add casting test

* test(utils): fix use correct function

* chore: make eslint error if missing .js extension (#2735)

* chore: eslint error if missing .js extension

* chore

* lint: fix missing .js error

Co-authored-by: Skillz4Killz <23035000+Skillz4Killz@users.noreply.github.com>

* fix: readme runtime list (#2739)

* Bot pkg (#2740)

* fix: bot pkg test

* Fix code style issues with ESLint

* Update Guild.ts

* Update Guild.ts

* Fix code style issues with ESLint

Co-authored-by: Lint Action <lint-action@samuelmeuli.com>
Co-authored-by: Jonathan Ho <heiheiho000@gmail.com>

* Update tsdoc.json (#2741)

Updates tsdoc.json to reflect the current packages.

* Interaction types - remove member,channel,role from value type (#2743)

https://discord.com/channels/785384884197392384/1067265182776176690/1068189883073572924

* Add missing types (#2742)

* Revert "fix: use isomorphic ws" (#2744)

This reverts commit ad306b0d0a.

* fix: interaction requests that sent without full url

* fix: lint issues

* fix: remote gateway test file

* fix: interaction response bug with body being invalid (#2746)

* fix: readme runtime list

* Fix code style issues with ESLint

* node 18

* fix: websocket import type

* fix: body for interaction requests

---------

Co-authored-by: Lint Action <lint-action@samuelmeuli.com>

* fix: interaction followup type (#2747)

* fix: readme runtime list

* Fix code style issues with ESLint

* node 18

* fix: websocket import type

* fix: body for interaction requests

* fix: perma fix for type error in ci

* fix: followupmessage option type

---------

Co-authored-by: Lint Action <lint-action@samuelmeuli.com>

* fix: e2e exit bug (#2748)

* fix: readme runtime list

* Fix code style issues with ESLint

* node 18

* fix: websocket import type

* fix: body for interaction requests

* fix: perma fix for type error in ci

* fix: followupmessage option type

* fix: e2e tests exit bug

* fix: color console logger

---------

Co-authored-by: Lint Action <lint-action@samuelmeuli.com>

* fix: rest sending attachments files (#2749)

* fix: readme runtime list

* Fix code style issues with ESLint

* node 18

* fix: websocket import type

* fix: body for interaction requests

* fix: perma fix for type error in ci

* fix: followupmessage option type

* fix: e2e tests exit bug

* fix: color console logger

* fix: image file sending

* Fix code style issues with ESLint

---------

Co-authored-by: Lint Action <lint-action@samuelmeuli.com>

* fix: guild and role methods (#2751)

* fix: readme runtime list

* Fix code style issues with ESLint

* node 18

* fix: websocket import type

* fix: body for interaction requests

* fix: perma fix for type error in ci

* fix: followupmessage option type

* fix: e2e tests exit bug

* fix: color console logger

* fix: image file sending

* Fix code style issues with ESLint

* guild and role methods

* Fix code style issues with ESLint

---------

Co-authored-by: Lint Action <lint-action@samuelmeuli.com>

* ci: add bot package (#2752)

* ci: add bot package

* ci: fix test

* e2e test stuff (#2754)

* fix: readme runtime list

* Fix code style issues with ESLint

* node 18

* fix: websocket import type

* fix: body for interaction requests

* fix: perma fix for type error in ci

* fix: followupmessage option type

* fix: e2e tests exit bug

* fix: color console logger

* fix: image file sending

* Fix code style issues with ESLint

* guild and role methods

* Fix code style issues with ESLint

* fix: dont send heartbeat if socket is not open

* fix: remove logs

* fox: remove more logs

* fix some bugs in role tests

* Switch to after hook style

* hoti's speed snaker

* auto convert objects for discord

* Fix code style issues with ESLint

* fix: remove dup imports

* fix: i hate linters

* speeder

---------

Co-authored-by: Lint Action <lint-action@samuelmeuli.com>

* fix: delete guilds test (#2758)

* fix: readme runtime list

* Fix code style issues with ESLint

* node 18

* fix: websocket import type

* fix: body for interaction requests

* fix: perma fix for type error in ci

* fix: followupmessage option type

* fix: e2e tests exit bug

* fix: color console logger

* fix: image file sending

* Fix code style issues with ESLint

* guild and role methods

* Fix code style issues with ESLint

* fix: dont send heartbeat if socket is not open

* fix: remove logs

* fox: remove more logs

* fix some bugs in role tests

* Switch to after hook style

* hoti's speed snaker

* auto convert objects for discord

* Fix code style issues with ESLint

* fix: remove dup imports

* fix: i hate linters

* speeder

* fix: tests delete guilds

* Fix code style issues with ESLint

---------

Co-authored-by: Lint Action <lint-action@samuelmeuli.com>

* chore: fix deps (#2757)

* ci/test: fix bot pkg e2e test (#2759)

* chore: fix script and update import map (#2761)

* chore: fix version script

* chore: update import map

* chore: fix ws import map

* chore: fix deno import map

* test: fix

* fix: more rest e2e tests (#2763)

* fix: readme runtime list

* Fix code style issues with ESLint

* node 18

* fix: websocket import type

* fix: body for interaction requests

* fix: perma fix for type error in ci

* fix: followupmessage option type

* fix: e2e tests exit bug

* fix: color console logger

* fix: image file sending

* Fix code style issues with ESLint

* guild and role methods

* Fix code style issues with ESLint

* fix: dont send heartbeat if socket is not open

* fix: remove logs

* fox: remove more logs

* fix some bugs in role tests

* Switch to after hook style

* hoti's speed snaker

* auto convert objects for discord

* Fix code style issues with ESLint

* fix: remove dup imports

* fix: i hate linters

* speeder

* fix: tests delete guilds

* Fix code style issues with ESLint

* fix: easier to provide custom intents in bot

* fix: shutdown bot after test

* fix: add getGuild

* fix: multiple guild delete attempts

* fix: add emoji e2e tests

* fix: remaining old e2e rest tests

* Fix code style issues with ESLint

---------

Co-authored-by: Lint Action <lint-action@samuelmeuli.com>

* test: add gateway integration test (#2756)

* test: add gateway integration test

* test(gateway): fix connection test

* test(gateway): add heartbeat test

* ci: add integration test

* fix: add uWebSockets.js

* ci: add timeout

* test(utils): remove old test

* Revert "test(utils): remove old test"

This reverts commit 04fb6dd4b5.

* test(gateway): fix uws server

* test(gateway): fix type

* chore: update codecov flag

* test(gateway): remove dev code

---------

Co-authored-by: Skillz4Killz <23035000+Skillz4Killz@users.noreply.github.com>

* Update release.yml (#2768)

* fix: bot logger (#2769)

* fix: readme runtime list

* Fix code style issues with ESLint

* node 18

* fix: websocket import type

* fix: body for interaction requests

* fix: perma fix for type error in ci

* fix: followupmessage option type

* fix: e2e tests exit bug

* fix: color console logger

* fix: image file sending

* Fix code style issues with ESLint

* guild and role methods

* Fix code style issues with ESLint

* fix: dont send heartbeat if socket is not open

* fix: remove logs

* fox: remove more logs

* fix some bugs in role tests

* Switch to after hook style

* hoti's speed snaker

* auto convert objects for discord

* Fix code style issues with ESLint

* fix: remove dup imports

* fix: i hate linters

* speeder

* fix: tests delete guilds

* Fix code style issues with ESLint

* fix: easier to provide custom intents in bot

* fix: shutdown bot after test

* fix: add getGuild

* fix: multiple guild delete attempts

* fix: add emoji e2e tests

* fix: remaining old e2e rest tests

* Fix code style issues with ESLint

* fix: add bot.logger

* fix: make logger name capital

---------

Co-authored-by: Lint Action <lint-action@samuelmeuli.com>

* chore: update readme (#2772)

* chore: add coverage per pkg

* chore: add npm version

* chore: add test status

* chore: fix ci

* chore: fix ci

* ci: fix ci needs

* chore: add only push event

* style: remove import logger

* 📚 - docs: fix README package links (#2773)

* test(all): add test importing index (#2774)

* test(all): add test importing index

* chore: remove old benchmark dir

* chore: disable coverage status fail

* test(client): add import test with try catch

* test(rest): fix narrow import scope

* test(utils): add test (#2764)

* test(utils): remove old test

* test(utils): add color test

* test(utils): fix import mocha

* test(utils): fix test type error

* test(utils): remove dev code

* fix(utils): bucket not export all function

* test(utils): add some test for bucket

* fix(utils): close #2775

* test(utils): add test for permissions.ts

* test(utils): fix missing mocha import

* fix(utils): better fix for #2775

* feat: addReaction & addReactions

* feat: connectToVoice

* fix: linters issues

* fix: remove aliases and add createGuildFromTemplate

* feat: deleteMessages

* fix: reaction related helpers

* mfa level

* voie states editing

* image urls

* fix: typos

* get message typeguards

* fix: more helpers

* fix: remaining helpers

* fix: add logs to gateway manager

* fix: rest resolve sends status and body

* fix: lots of errors

* fix: client errors

* fix: remove old pkg

* snaker

* fix: broken util import for image url

* fix: cleanup shard and circular deps

* fix: remove ThreadChannel from GuildChannel

* fix: generate interaction usage

* fix: more bugs

* fix: use node:events to import

* fix(rest): add interface RestRequestRejection (#2782)

* fix: remove invalid todo

* fix: timeout bug

---------

Co-authored-by: Skillz <skillz@discord.gg/ddeno>
Co-authored-by: Jonathan Ho <heiheiho000@gmail.com>
Co-authored-by: deepsarda <92147339+deepsarda@users.noreply.github.com>
Co-authored-by: Awesome Stickz <awesome@stickz.dev>
Co-authored-by: Lint Action <lint-action@samuelmeuli.com>
Co-authored-by: Lars_und_so <46791248+Larsundso@users.noreply.github.com>
Co-authored-by: ITOH <to@itoh.at>
Co-authored-by: Yaikava <83710104+Yaikava@users.noreply.github.com>
Co-authored-by: Andreas Fink <mail@afink.dev>

---------

Co-authored-by: Jonathan Ho <48591478+H01001000@users.noreply.github.com>
Co-authored-by: H01001000 <heiheiho000@gmail.com>
Co-authored-by: deepsarda <92147339+deepsarda@users.noreply.github.com>
Co-authored-by: Yaikava <83710104+Yaikava@users.noreply.github.com>
Co-authored-by: Skillz <skillz@discord.gg/ddeno>
Co-authored-by: Awesome Stickz <awesome@stickz.dev>
Co-authored-by: Lint Action <lint-action@samuelmeuli.com>
Co-authored-by: Lars_und_so <46791248+Larsundso@users.noreply.github.com>
Co-authored-by: ITOH <to@itoh.at>
Co-authored-by: Andreas Fink <mail@afink.dev>
2023-02-20 15:00:00 -06:00

32 KiB

sidebar_position, sidebar_label
sidebar_position sidebar_label
3 Step 2 - Gateway

Step 2: Creating A Standalone Gateway Process

If you are reading this, you should have your REST process completed. We are going to need it here. This process will be connecting to discord's websockets which will send you all the events.

Before, we dive into how, here is a quick summary of why you will want a standalone gateway process.

Why Use Standalone REST Process?

  • Zero Downtime Updates:

    • Your bot can be updated in a matter of seconds. With normal sharding, you have to restart which also has to process identifying all your shards with a 1/~5s rate limit. With WS handling moved to a proxy process, this allows you to instantly get the bot code restarted without any concerns of delays. If you have a bot on 200,000 servers normally this would mean a 20 minute delay to restart your bot if you made a small change and restarted.
  • Zero Downtime Resharding:

    • Discord stops letting your bot get added to new servers at certain points in time. For example, suppose you had 150,000 servers running 150 shards. The maximum amount of servers your shards could hold is 150 * 2500 = 375,000. If your bot reaches this, it can no longer join new servers until it re-shards.
    • DD proxy provides 2 types of re-sharding. Automated and manual. You can also have both.
      • Automated: This system will automatically begin a Zero-downtime resharding process behind the scenes when you reach 80% of your maximum servers allowed by your shards. For example, since 375,000 was the max, at 300,000 we would begin re-sharding behind the scenes with ZERO DOWNTIME.
        • 80% of maximum servers reached (The % of 80% is customizable.)
        • Identify limits have room to allow re-sharding. (Also customizable)
      • Manual: You can also trigger this manually should you choose.
  • Horizontal Scaling:

    • The proxy system allows you to scale the bot horizontally. When you reach a huge size, you can either keep spending more money to keep beefing up your server or you can buy several cheaper servers and scale horizontally. The proxy means you can have WS handling on a completely separate system.
  • No Loss Restarts:

    • When you restart a bot without the proxy system, normally you would lose many events. Users may be using commands or messages are sent that will not be filtered. As your bot's grow this number rises dramatically. Users may join who wont get the auto-roles or any other actions your bot should take. With the proxy system, you can keep restarting your bot and never lose any events. Events will be put into a queue while your bot is down(max size of queue is customizable), once the bot is available the queue will begin processing all events.
  • Controllers:

    • The controller aspect gives you full control over everything inside the proxy. You can provide a function to simply override the handler. For example, if you would like a certain function to do something different, instead of having to fork and maintain your fork, you can just provide a function to override.
  • Clustering With Workers:

    • Take full advantage of all your CPU cores by using workers to spread the load. Control how many shards per worker and how many workers to maximize efficiency!

Creating Gateway Manager

Create a file under some path like src/gateway/mod.ts.

import { DISCORD_TOKEN, REST_AUTHORIZATION, REST_PORT } from "../../configs.ts";
import { BASE_URL, createRestManager } from "../../deps.ts";

const rest = createRestManager({
  token: DISCORD_TOKEN,
  secretKey: REST_AUTHORIZATION,
  customUrl: `http://localhost:${REST_PORT}`,
});

Throw another rest manager here which will be responsible for calling the main REST process we created in Step 1. This will allow your gateway to communicate to the other process. Remember this is just to communicate outwards, this file should not have the http listener.

Feel free to refactor and optimize this should you wish to move const rest... to a separate file and reuse in both steps.

Getting Gateway Bot Data

Now we need to use this rest manager to call the api to get information about how to connect to discord's gateway for your bot.

import { routes } from "../../deps.ts";

const rest = createRestManager({
  token: DISCORD_TOKEN,
  secretKey: REST_AUTHORIZATION,
  customUrl: `http://localhost:${REST_PORT}`,
});

// CALL THE REST PROCESS TO GET GATEWAY DATA
const gatewayBot = await rest.runMethod(rest, "GET", routes.GATEWAY_BOT()).then((res) => ({
  url: res.url,
  shards: res.shards,
  sessionStartLimit: {
    total: res.session_start_limit.total,
    remaining: res.session_start_limit.remaining,
    resetAfter: res.session_start_limit.reset_after,
    maxConcurrency: res.session_start_limit.max_concurrency,
  },
}));

With this info, we can now create our gateway manager.

Understanding Gateway Manager

import { INTENTS, SHARDS_PER_WORKER, TOTAL_WORKERS } from "../../configs.ts";

const gateway = createGatewayManager({
  gatewayBot,
  gatewayConfig: {
    token: DISCORD_TOKEN,
    intents: INTENTS,
  },
  totalShards: gatewayBot.shards,
  shardsPerWorker: SHARDS_PER_WORKER,
  totalWorkers: TOTAL_WORKERS,
  // debug: console.log,
  // THIS WILL BE USED LATER IN WORKER SO LEAVE IT HERE
  handleDiscordPayload: () => {},
});

Basic Keys

  • EVENT_HANDLER_SECRET_KEY is from your configs that will be used to make sure requests sent to your event handler process are indeed from you.
  • DISCORD_TOKEN if you can't figure this out, this guide isn't for you. Please find another.
  • INTENTS pass in a number or a string of intents. Autocomplete/type-safety is provided for strings :)

Powerful Keys

If your bot is going to be run on one process, you can re-use the data that discord gave you to connect.

  • totalShards: is the maximum number of shards you want to use for connecting your bot. Should you think Discord is not smart enough to recommend a good amount, use this to override their choice. Highly recommend just using theirs.
  • lastShardId: is the last shard you want to connect in this process.
    • Using a combination of lastShardId & firstShardId, you can create several processes or even several servers to handle different amounts of shards should your bot get that big to require horizontal scaling. You can control how many shards each gateway manager will be responsible for.
  • spawnShardDelay: The delay in milliseconds to wait before spawning next shard.
  • shardsPerWorker: The amount of shards to load per worker. Discussed in detail below.
  • totalWorkers: The maximum amount of workers to use for your bot.

Gateway Cache

There is a few things that we cache in the gateway process directly, because sending them across the network is not ideal. This is done to support custom cache functionality.

  • guildIds: Used for determining what type of GUILD_CREATE event is received.
  • loadingGuildIds: Used for determining if all guilds have arrived when initially connecting.
  • editedMessages: Used to prevent spam of events across the network. MESSAGE_UPDATE are an extremely heavy event. Any embed or link that is in a message will unfurl triggerring a message update event. This is undesired behavior for 99% of bots out there. If someone sends a message with 5 urls, in there you will get a MESSAGE_CREATE and 5 MESSAGE_UPDATE events. If that user edits a single letter on it you now get 6 MESSAGE_UPDATE events, 1 for the content change and 5 more for each url being unfurled. The editedMessages cache checks if the content of the message changed or not before sending the event downstream. Override this behavior if you need different behavior.

Gateway Method Overriding

One of the benefits of Discordeno is that you can override/customize anything from the library. Should you desire to change the logic in any method it is as simple as:

// TYPINGS WILL BE AUTOMATICALLY PROVIDED
gateway.heartbeat = function (gateway, shardId, interval) {
  // YOUR CUSTOM HANDLING CODE HERE
};

Workers

Now, we should take a minute here to talk about workers. Workers are just Clusters in Node.js

When you have a big bot and you are processing millions of events, you need to speed up that processing. Keeping it in 1 thread is not very nice since JavaScript is single threaded. This means it can only process 1 event at a time. With workers, you can make it process several events at the same time. We mentioned the shardsPerWorker earlier. This option was added to allow you to choose how many shards should be managed by each worker.

When shards are spawned, they are triggered by a method on gateway: tellWorkerToIdentify, so we'll have to modify it to create workers and send message:

gateway.tellWorkerToIdentify = async (_gateway, workerId, shardId, _bucketId) => {
  let worker = workers.get(workerId);
  if (!worker) {
    worker = createWorker(workerId);
    workers.set(workerId, worker);
  }

  // TYPE TYPE WorkerMessage IS FROM WORKER FILE, DISCUSSED IN DETAIL BELOW
  const identify: WorkerMessage = {
    type: "IDENTIFY_SHARD",
    shardId,
  };

  worker.postMessage(identify);
};

You can choose to replace the handler with any desired functionality you like. For example, should should you want to create a new worker for each new workerId that appears and have that worker trigger the identify functionaly. How you choose to handler workers is left in your care.

Now that we've setup our initial gateway manager and added tellWorkerToIdentify to gateway, we need to do the rest of the work: creating workers, spawning shards etc.

import { EVENT_HANDLER_SECRET_KEY, EVENT_HANDLER_URL } from "../../configs.ts";
import { Worker } from "worker_threads";
import { WorkerCreateData, WorkerGetShardInfo, WorkerMessage, WorkerShardInfo, WorkerShardPayload } from "./worker.js";

// A COLLECTION OF WORKERS
const workers = new Collection<number, Worker>();
const nonces = new Collection<string, (data: any) => void>();

function createWorker(workerId: number) {
  const workerData: WorkerCreateData = {
    intents: gateway.manager.gatewayConfig.intents ?? 0,
    token: DISCORD_TOKEN,
    // TODO: PUT THIS SEPARATELY. CAN USE MULTIPLE URLS IF YOU HAVE MULTIPLE BOT PROCESSES HANDLING DIFFERENT SHARDS' EVENTS
    handlerUrls: [EVENT_HANDLER_URL],
    handlerAuthorization: EVENT_HANDLER_SECRET_KEY,
    path: "./worker.ts",
    totalShards: gateway.manager.totalShards,
    workerId,
  };

  const worker = new Worker("./worker.js", {
    workerData,
  });

  worker.on("message", async (data: ManagerMessage) => {
    switch (data.type) {
      case "REQUEST_IDENTIFY": {
        await gateway.manager.requestIdentify(data.shardId);

        const allowIdentify: WorkerMessage = {
          type: "ALLOW_IDENTIFY",
          shardId: data.shardId,
        };

        worker.postMessage(allowIdentify);

        break;
      }
      case "NONCE_REPLY": {
        nonces.get(data.nonce)?.(data.data);
      }
    }
  });

  return worker;
}

// TYPES WE USE
export type ManagerMessage = ManagerRequestIdentify | ManagerNonceReply<WorkerShardInfo[]>;

export type ManagerRequestIdentify = {
  type: "REQUEST_IDENTIFY";
  shardId: number;
};

export type ManagerNonceReply<T> = {
  type: "NONCE_REPLY";
  nonce: string;
  data: T;
};

Spawning Shards

Once you are ready and the gateway has been created as you desired, we can begin spawning the shards.

gateway.spawnShards(gateway);

This code now handles creating gateway manager, creating workers, spawning shards and sending the info to each workers. Next is creating a worker file to receive these info, connecting to gateway and sending the events to bot process.

Here's the full code of src/gateway/mod.ts:

import {
  DISCORD_TOKEN,
  EVENT_HANDLER_SECRET_KEY,
  EVENT_HANDLER_URL,
  INTENTS,
  REST_AUTHORIZATION,
  REST_PORT,
  SHARDS_PER_WORKER,
  TOTAL_WORKERS,
} from "../../configs.ts";
import { BASE_URL, createRestManager, routes } from "../../deps.ts";
import { Worker } from "worker_threads";
import { WorkerCreateData, WorkerGetShardInfo, WorkerMessage, WorkerShardInfo, WorkerShardPayload } from "./worker.ts";

const rest = createRestManager({
  token: DISCORD_TOKEN,
  secretKey: REST_AUTHORIZATION,
  customUrl: `http://localhost:${REST_PORT}`,
});

const rest = createRestManager({
  token: DISCORD_TOKEN,
  secretKey: REST_AUTHORIZATION,
  customUrl: `http://localhost:${REST_PORT}`,
});

// CALL THE REST PROCESS TO GET GATEWAY DATA
const gatewayBot = await rest.runMethod(rest, "GET", routes.GATEWAY_BOT()).then((res) => ({
  url: res.url,
  shards: res.shards,
  sessionStartLimit: {
    total: res.session_start_limit.total,
    remaining: res.session_start_limit.remaining,
    resetAfter: res.session_start_limit.reset_after,
    maxConcurrency: res.session_start_limit.max_concurrency,
  },
}));

const gateway = createGatewayManager({
  gatewayBot,
  gatewayConfig: {
    token: DISCORD_TOKEN,
    intents: INTENTS,
  },
  totalShards: gatewayBot.shards,
  shardsPerWorker: SHARDS_PER_WORKER,
  totalWorkers: TOTAL_WORKERS,
  // debug: console.log,
  handleDiscordPayload: () => {},
  tellWorkerToIdentify: async (_gateway, workerId, shardId, _bucketId) => {
    let worker = workers.get(workerId);
    if (!worker) {
      worker = createWorker(workerId);
      workers.set(workerId, worker);
    }

    // TYPE TYPE WorkerMessage IS FROM WORKER FILE, DISCUSSED IN DETAIL BELOW
    const identify: WorkerMessage = {
      type: "IDENTIFY_SHARD",
      shardId,
    };

    worker.postMessage(identify);
  },
});

// A COLLECTION OF WORKERS
const workers = new Collection<number, Worker>();
const nonces = new Collection<string, (data: any) => void>();

function createWorker(workerId: number) {
  const workerData: WorkerCreateData = {
    intents: gateway.manager.gatewayConfig.intents ?? 0,
    token: DISCORD_TOKEN,
    handlerUrls: [EVENT_HANDLER_URL],
    handlerAuthorization: EVENT_HANDLER_SECRET_KEY,
    path: "./worker.ts",
    totalShards: gateway.manager.totalShards,
    workerId,
  };

  const worker = new Worker("./worker.ts", {
    workerData,
  });

  worker.on("message", async (data: ManagerMessage) => {
    switch (data.type) {
      case "REQUEST_IDENTIFY": {
        await gateway.manager.requestIdentify(data.shardId);

        const allowIdentify: WorkerMessage = {
          type: "ALLOW_IDENTIFY",
          shardId: data.shardId,
        };

        worker.postMessage(allowIdentify);

        break;
      }
      case "NONCE_REPLY": {
        nonces.get(data.nonce)?.(data.data);
      }
    }
  });

  return worker;
}

// TYPES WE USE
export type ManagerMessage = ManagerRequestIdentify | ManagerNonceReply<WorkerShardInfo[]>;

export type ManagerRequestIdentify = {
  type: "REQUEST_IDENTIFY";
  shardId: number;
};

export type ManagerNonceReply<T> = {
  type: "NONCE_REPLY";
  nonce: string;
  data: T;
};

// SPAWN SHARDS INTO WORKERS
gateway.spawnShards();

Worker File

Now that we've handled creating gateway, workers, we need to create a worker file to identify, receive gateway events and send them to bot process.

Create a file in a path like src/gateway/worker.ts.

Now we'll have to create a Shard Manager, this is what will handle identifying, receiving events.

import { createShardManager } from "discordeno";
import { parentPort, workerData } from "worker_threads";

if (!parentPort) {
  throw new Error("Parent port is null");
}

// THE DATA WE GET FROM GATEWAY FILE
const script: WorkerCreateData = workerData;

const identifyPromises = new Map<number, () => void>();

const manager = createShardManager({
  gatewayConfig: {
    intents: script.intents,
    token: script.token,
  },
  shardIds: [],
  totalShards: script.totalShards,
  // WE WILL COVER THESE TWO FUNCTIONS IN LATER PART OF THE GUIDE, FOR NOW, LEAVE IT THIS WAY
  handleMessage: () => {},
  requestIdentify: async () => {},
});

The above code only creates a shard manager, we now have 3 more things to do:

  • Listening to gateway process, sending events received to respective shards in the manager.
  • Handling Discord Payloads.
  • Requesting Identify.

Sending events from Gateway to Shards in Manager

In order for the shards to receive events and send to bot process, we need to receive the event payloads from gateway first, we can do this by using message event in parentPort like shown below:

import { Shard } from "discordeno";

function buildShardInfo(shard: Shard): WorkerShardInfo {
  return {
    workerId: script.workerId,
    shardId: shard.id,
    rtt: shard.heart.rtt || -1,
    state: shard.state,
  };
}

parentPort.on("message", async (data: WorkerMessage) => {
  switch (data.type) {
    // Gateway sends IDENTIFY_SHARD in gateway.tellWorkerToIdentify
    case "IDENTIFY_SHARD": {
      await manager.identify(data.shardId);

      break;
    }
    // Gateway sends ALLOW_IDENTIFY when worker requests to identify
    case "ALLOW_IDENTIFY": {
      identifyPromises.get(data.shardId)?.();
      identifyPromises.delete(data.shardId);

      break;
    }
    // Gateway sends SHARD_PAYLOAD for every events it receives from Discord
    case "SHARD_PAYLOAD": {
      manager.shards.get(data.shardId)?.send(data.data);

      break;
    }
    // Send shard info if gateway sends GET_SHARD_INFO
    case "GET_SHARD_INFO": {
      const infos = manager.shards.map(buildShardInfo);

      parentPort?.postMessage({ type: "NONCE_REPLY", nonce: data.nonce, data: infos });
    }
  }
});

Now TypeScript will error because of missing types, add these to your code:

import { ShardSocketRequest, ShardState } from "discordeno";

export type WorkerMessage = WorkerIdentifyShard | WorkerAllowIdentify | WorkerShardPayload | WorkerGetShardInfo;

export type WorkerIdentifyShard = {
  type: "IDENTIFY_SHARD";
  shardId: number;
};

export type WorkerAllowIdentify = {
  type: "ALLOW_IDENTIFY";
  shardId: number;
};

export type WorkerShardPayload = {
  type: "SHARD_PAYLOAD";
  shardId: number;
  data: ShardSocketRequest;
};

export type WorkerGetShardInfo = {
  type: "GET_SHARD_INFO";
  nonce: string;
};

export type WorkerCreateData = {
  intents: number;
  token: string;
  handlerUrls: string[];
  handlerAuthorization: string;
  path: string;
  totalShards: number;
  workerId: number;
};

export type WorkerShardInfo = {
  workerId: number;
  shardId: number;
  rtt: number;
  state: ShardState;
};

Handling Discord Payloads

One of the big things we didn't cover yet is the handler for discord payloads. This is the main sauce of your worker process here. This is going to take the events that the gateway manager sent and send it to your event handler. How you wish to communicate with your event handler is up to you. For this guide, we will use http, but you can replace that with anything you like.

manager.createShardOptions.handleMessage = async (shard, message) => {
  const url = script.handlerUrls[shard.id % script.handlerUrls.length];
  if (!url) return console.error("ERROR: NO URL FOUND TO SEND MESSAGE");

  await fetch(url, {
    method: "POST",
    body: JSON.stringify({ message, shardId: shard.id }),
    headers: { "Content-Type": "application/json", Authorization: script.handlerAuthorization },
  }).catch((error) => console.error(error));
};

You can change this function to use a WS or any form of communication you prefer to use to send this to your event handler.

This is also the place where you make use of the Gateway Cache we mentioned earlier (guildIds, loadingGuildIds, editedMessages).

Gateway Queue

One thing we can add on here, which you will find already done in the template if you are using it. However, it is still good to read this to learn and understand the logic behind it. When you need a downtime for whatever reason, you can create a queue like system to avoid any missed events. Let's create a simple queue. If it errors, assuming something like the bot event listener process is down for whatever reason, the .catch in fetch will run adding this event to the queue to try again in one second by calling the handleQueue function.

.catch(() => {
  // IF FAILED TRY TO QUEUE MAYBE LISTENER IS DOWN
  if (message.t === "INTERACTION_CREATE") handleInteractionQueueing(message, shard.id);
  else queue.events.push({ shardId: shard.id, message });

  setTimeout(handleQueue, 1000);
});

Now TypeScript will probably throw some errors at your face, so let's fix those real quick. Create an object that will hold the queue of events for our gateway.

import { DiscordGatewayPayload } from "discordeno";

const queue: GatewayQueue = {
  processing: false,
  events: [],
};

export interface QueuedEvent {
  message: DiscordGatewayPayload;
  shardId: number;
}

export interface GatewayQueue {
  processing: boolean;
  events: QueuedEvent[];
}

async function handleQueue() {
  // PLACEHOLDER FUNCTION THAT WILL HANDLE PROCESSING THE QUEUE
}

async function handleInteractionQueueing(message: DiscordGatewayPayload, shardId: number) {
  // PLACEHOLDER FUNCTION
}

Alrighty, since TypeScript stopped being annoying, let's continue. Next, we should make sure to avoid fetching when the queue is already processing or has events queued up. This will help us preserve the order of events in the queue.

handleMessage: async function (shard, message) {
// IF QUEUE IS RUNNING JUST ADD TO QUEUE
if (queue.processing) {
  if (message.t === "INTERACTION_CREATE") return handleInteractionQueueing(message, shard.id);

  return queue.events.push({ shardId: shard.id, message });
}

await fetch(EVENT_HANDLER_URL, {

Typescript must be at it again so let's shut it up again. Keep in mind that we are handling interaction events separately because they require a response within 3 seconds or they will become invalid. In this function first we automatically respond to the ones that can not be deferred. For the interactions that can be deferred, we will simply defer them and add this event to the queue.

import { DiscordInteraction, InteractionResponseTypes, InteractionTypes, routes } from "discordeno";
import { BOT_SERVER_INVITE_CODE } from "../../configs.ts";

async function handleInteractionQueueing(message: DiscordGatewayPayload, shardId: number) {
  if (message.t !== "INTERACTION_CREATE") return;

  const interaction = message.d as DiscordInteraction;
  // IF THIS INTERACTION IS NOT DEFERABLE
  if ([InteractionTypes.ModalSubmit, InteractionTypes.ApplicationCommandAutocomplete].includes(interaction.type)) {
    return await rest.runMethod(
      rest,
      "POST",
      routes.INTERACTION_ID_TOKEN(BigInt(interaction.id), interaction.token),
      {
        type: InteractionResponseTypes.ChannelMessageWithSource,
        data: {
          content:
            `The bot is having a temporary issue, please try again or contact us at https://discord.gg/${BOT_SERVER_INVITE_CODE}`,
        },
      },
    );
  }

  await rest.runMethod(rest, "POST", endpoints.INTERACTION_ID_TOKEN(BigInt(interaction.id), interaction.token), {
    // MESSAGE COMPONENTS NEED SPECIAL DEFER
    type: InteractionTypes.MessageComponent === interaction.type
      ? InteractionResponseTypes.DeferredUpdateMessage
      : InteractionResponseTypes.DeferredChannelMessageWithSource,
  });

  // ADD EVENT TO QUEUE
  queue.events.push({ shardId, message });
}

Oh no, TypeScript is at it again. We need to make a REST manager so that our gateway proxy can communicate with our REST proxy. We then can make use of it to send a POST request.

const rest = createRestManager({
  token: DISCORD_TOKEN,
  secretKey: REST_AUTHORIZATION,
  customUrl: REST_URL,
});

So now there is only one thing left the handleQueue function. First we get the first item from the queue using .shift(). Then we check to see if that item exists. If it does not exist, we mark the queue as no longer processing and cancel out. However, if it does exist, we send a fetch request to the bot event handler process. In the .catch() we will add this event back in to the start of the queue in case the bot is still down. Finally we call this function again to run the next item in the queue.

async function handleQueue() {
  const event = queue.events.shift();
  // QUEUE IS EMPTY
  if (!event) {
    console.log("GATEWAY QUEUE ENDING");
    queue.processing = false;
    return;
  }

  await fetch(EVENT_HANDLER_URL, {
    headers: {
      Authorization: EVENT_HANDLER_SECRET_KEY,
      "Content-Type": "application/json",
    },
    method: "POST",
    body: JSON.stringify({
      shardId: event.shardId,
      message: event.message,
    }),
  })
    .then((res) => {
      res.text();
      handleQueue();
    })
    .catch(() => {
      // EVENT HANDLER STILL NOT ACCEPTING REQUEST. SO ADD BACK TO QUEUE
      queue.events.unshift(event);
      setTimeout(handleQueue, 1000);
    });
}

Requesting Identify

We need to request identify in order to trigger initial handshake with the gateway, we'll use manager.requestIdentify to do this.

import { ManagerMessage } from "./mod.ts";

manager.requestIdentify = async function (shardId: number): Promise<void> {
  return await new Promise((resolve) => {
    identifyPromises.set(shardId, resolve);

    const identifyRequest: ManagerMessage = {
      type: "REQUEST_IDENTIFY",
      shardId,
    };

    parentPort?.postMessage(identifyRequest);
  });
};

That's all, you've now setup your gateway and worker. Here's the full code of src/gateway/worker.ts:

import {
  createRestManager,
  createShardManager,
  DiscordGatewayPayload,
  DiscordInteraction,
  InteractionResponseTypes,
  InteractionTypes,
  routes,
  Shard,
  ShardSocketRequest,
  ShardState,
} from "discordeno";
import { parentPort, workerData } from "worker_threads";
import { ManagerMessage } from "./mod";
import {
  BOT_SERVER_INVITE_CODE,
  DISCORD_TOKEN,
  EVENT_HANDLER_SECRET_KEY,
  EVENT_HANDLER_URL,
  REST_AUTHORIZATION,
  REST_URL,
} from "../../configs.ts";

if (!parentPort) {
  throw new Error("Parent port is null");
}

// THE DATA WE GET FROM GATEWAY FILE
const script: WorkerCreateData = workerData;

const identifyPromises = new Map<number, () => void>();

const rest = createRestManager({
  token: DISCORD_TOKEN,
  secretKey: REST_AUTHORIZATION,
  customUrl: REST_URL,
});

const manager = createShardManager({
  gatewayConfig: {
    intents: script.intents,
    token: script.token,
  },
  shardIds: [],
  totalShards: script.totalShards,
  handleMessage: async (shard, message) => {
    const url = script.handlerUrls[shard.id % script.handlerUrls.length];
    if (!url) return console.error("ERROR: NO URL FOUND TO SEND MESSAGE");

    await fetch(url, {
      method: "POST",
      body: JSON.stringify({ message, shardId: shard.id }),
      headers: { "Content-Type": "application/json", Authorization: script.handlerAuthorization },
    }).catch(() => {
      // IF FAILED TRY TO QUEUE MAYBE LISTENER IS DOWN
      if (message.t === "INTERACTION_CREATE") handleInteractionQueueing(message, shard.id);
      else queue.events.push({ shardId: shard.id, message });

      setTimeout(handleQueue, 1000);
    });
  },
  requestIdentify: async function (shardId: number): Promise<void> {
    return await new Promise((resolve) => {
      identifyPromises.set(shardId, resolve);

      const identifyRequest: ManagerMessage = {
        type: "REQUEST_IDENTIFY",
        shardId,
      };

      parentPort?.postMessage(identifyRequest);
    });
  },
});

function buildShardInfo(shard: Shard): WorkerShardInfo {
  return {
    workerId: script.workerId,
    shardId: shard.id,
    rtt: shard.heart.rtt || -1,
    state: shard.state,
  };
}

parentPort.on("message", async (data: WorkerMessage) => {
  switch (data.type) {
    // Gateway sends IDENTIFY_SHARD in gateway.tellWorkerToIdentify
    case "IDENTIFY_SHARD": {
      await manager.identify(data.shardId);

      break;
    }
    // Gateway sends ALLOW_IDENTIFY when worker requests to identify
    case "ALLOW_IDENTIFY": {
      identifyPromises.get(data.shardId)?.();
      identifyPromises.delete(data.shardId);

      break;
    }
    // Gateway sends SHARD_PAYLOAD for every events it receives from Discord
    case "SHARD_PAYLOAD": {
      manager.shards.get(data.shardId)?.send(data.data);

      break;
    }
    // Send shard info if gateway sends GET_SHARD_INFO
    case "GET_SHARD_INFO": {
      const infos = manager.shards.map(buildShardInfo);

      parentPort?.postMessage({ type: "NONCE_REPLY", nonce: data.nonce, data: infos });
    }
  }
});

const queue: GatewayQueue = {
  processing: false,
  events: [],
};

async function handleQueue() {
  const event = queue.events.shift();
  // QUEUE IS EMPTY
  if (!event) {
    console.log("GATEWAY QUEUE ENDING");
    queue.processing = false;
    return;
  }

  await fetch(EVENT_HANDLER_URL, {
    headers: {
      Authorization: EVENT_HANDLER_SECRET_KEY,
      "Content-Type": "application/json",
    },
    method: "POST",
    body: JSON.stringify({
      shardId: event.shardId,
      message: event.message,
    }),
  })
    .then((res) => {
      res.text();
      handleQueue();
    })
    .catch(() => {
      // EVENT HANDLER STILL NOT ACCEPTING REQUEST. SO ADD BACK TO QUEUE
      queue.events.unshift(event);
      setTimeout(handleQueue, 1000);
    });
}

async function handleInteractionQueueing(message: DiscordGatewayPayload, shardId: number) {
  if (message.t !== "INTERACTION_CREATE") return;

  const interaction = message.d as DiscordInteraction;
  // IF THIS INTERACTION IS NOT DEFERABLE
  if ([InteractionTypes.ModalSubmit, InteractionTypes.ApplicationCommandAutocomplete].includes(interaction.type)) {
    return await rest.runMethod(rest, "POST", routes.INTERACTION_ID_TOKEN(BigInt(interaction.id), interaction.token), {
      type: InteractionResponseTypes.ChannelMessageWithSource,
      data: {
        content:
          `The bot is having a temporary issue, please try again or contact us at https://discord.gg/${BOT_SERVER_INVITE_CODE}`,
      },
    });
  }

  await rest.runMethod(rest, "POST", routes.INTERACTION_ID_TOKEN(BigInt(interaction.id), interaction.token), {
    // MESSAGE COMPONENTS NEED SPECIAL DEFER
    type: InteractionTypes.MessageComponent === interaction.type
      ? InteractionResponseTypes.DeferredUpdateMessage
      : InteractionResponseTypes.DeferredChannelMessageWithSource,
  });

  // ADD EVENT TO QUEUE
  queue.events.push({ shardId, message });
}

export type WorkerMessage = WorkerIdentifyShard | WorkerAllowIdentify | WorkerShardPayload | WorkerGetShardInfo;

export type WorkerIdentifyShard = {
  type: "IDENTIFY_SHARD";
  shardId: number;
};

export type WorkerAllowIdentify = {
  type: "ALLOW_IDENTIFY";
  shardId: number;
};

export type WorkerShardPayload = {
  type: "SHARD_PAYLOAD";
  shardId: number;
  data: ShardSocketRequest;
};

export type WorkerGetShardInfo = {
  type: "GET_SHARD_INFO";
  nonce: string;
};

export type WorkerCreateData = {
  intents: number;
  token: string;
  handlerUrls: string[];
  handlerAuthorization: string;
  path: string;
  totalShards: number;
  workerId: number;
};

export type WorkerShardInfo = {
  workerId: number;
  shardId: number;
  rtt: number;
  state: ShardState;
};

export interface QueuedEvent {
  message: DiscordGatewayPayload;
  shardId: number;
}

export interface GatewayQueue {
  processing: boolean;
  events: QueuedEvent[];
}

Note, you can take this concept and expand on it as much as you like. You can swap out the fetch() with websockets or any other system you like to communicate between your processes. I highly recommend you take some time to add checks in place to prevent adding to queue when the queue reaches a certain size. You don't want this to become a memory leak of infinite size and crash your gateway. So take the time to do this right in your setup.

If you have any questions, please contact us on our discord server.