flarecord 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +156 -0
  3. package/dist/core/bot-base.d.ts +26 -0
  4. package/dist/core/bot-base.d.ts.map +1 -0
  5. package/dist/core/bot-base.js +60 -0
  6. package/dist/core/bot-base.js.map +1 -0
  7. package/dist/core/client.d.ts +26 -0
  8. package/dist/core/client.d.ts.map +1 -0
  9. package/dist/core/client.js +60 -0
  10. package/dist/core/client.js.map +1 -0
  11. package/dist/core/connection.d.ts +52 -0
  12. package/dist/core/connection.d.ts.map +1 -0
  13. package/dist/core/connection.js +156 -0
  14. package/dist/core/connection.js.map +1 -0
  15. package/dist/core/gateway.d.ts +15 -0
  16. package/dist/core/gateway.d.ts.map +1 -0
  17. package/dist/core/gateway.js +50 -0
  18. package/dist/core/gateway.js.map +1 -0
  19. package/dist/core/handlers.d.ts +25 -0
  20. package/dist/core/handlers.d.ts.map +1 -0
  21. package/dist/core/handlers.js +120 -0
  22. package/dist/core/handlers.js.map +1 -0
  23. package/dist/core/heartbeat.d.ts +19 -0
  24. package/dist/core/heartbeat.d.ts.map +1 -0
  25. package/dist/core/heartbeat.js +55 -0
  26. package/dist/core/heartbeat.js.map +1 -0
  27. package/dist/core/message-handler.d.ts +23 -0
  28. package/dist/core/message-handler.d.ts.map +1 -0
  29. package/dist/core/message-handler.js +92 -0
  30. package/dist/core/message-handler.js.map +1 -0
  31. package/dist/index.d.ts +14 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +13 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/types/constants.d.ts +90 -0
  36. package/dist/types/constants.d.ts.map +1 -0
  37. package/dist/types/constants.js +99 -0
  38. package/dist/types/constants.js.map +1 -0
  39. package/dist/types/index.d.ts +54 -0
  40. package/dist/types/index.d.ts.map +1 -0
  41. package/dist/types/index.js +6 -0
  42. package/dist/types/index.js.map +1 -0
  43. package/dist/utils/logger.d.ts +23 -0
  44. package/dist/utils/logger.d.ts.map +1 -0
  45. package/dist/utils/logger.js +44 -0
  46. package/dist/utils/logger.js.map +1 -0
  47. package/dist/utils/message-helper.d.ts +11 -0
  48. package/dist/utils/message-helper.d.ts.map +1 -0
  49. package/dist/utils/message-helper.js +61 -0
  50. package/dist/utils/message-helper.js.map +1 -0
  51. package/dist/utils/reconnect.d.ts +19 -0
  52. package/dist/utils/reconnect.d.ts.map +1 -0
  53. package/dist/utils/reconnect.js +69 -0
  54. package/dist/utils/reconnect.js.map +1 -0
  55. package/dist/utils/session.d.ts +23 -0
  56. package/dist/utils/session.d.ts.map +1 -0
  57. package/dist/utils/session.js +58 -0
  58. package/dist/utils/session.js.map +1 -0
  59. package/dist/utils/websocket.d.ts +12 -0
  60. package/dist/utils/websocket.d.ts.map +1 -0
  61. package/dist/utils/websocket.js +64 -0
  62. package/dist/utils/websocket.js.map +1 -0
  63. package/package.json +48 -0
  64. package/src/core/bot-base.ts +90 -0
  65. package/src/core/client.ts +90 -0
  66. package/src/core/connection.ts +238 -0
  67. package/src/core/gateway.ts +78 -0
  68. package/src/core/handlers.ts +136 -0
  69. package/src/core/heartbeat.ts +65 -0
  70. package/src/core/message-handler.ts +96 -0
  71. package/src/index.ts +96 -0
  72. package/src/types/constants.ts +103 -0
  73. package/src/types/index.ts +65 -0
  74. package/src/utils/logger.ts +56 -0
  75. package/src/utils/message-helper.ts +100 -0
  76. package/src/utils/reconnect.ts +108 -0
  77. package/src/utils/session.ts +64 -0
  78. package/src/utils/websocket.ts +90 -0
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "flarecord",
3
+ "version": "0.0.1",
4
+ "description": "discord gateway client for cloudflare workers. built with typescript and optimized for durable objects.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "src"
17
+ ],
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/vaishnav-mk/flarecord.git"
21
+ },
22
+ "keywords": [
23
+ "discord",
24
+ "discord-bot",
25
+ "discord-gateway",
26
+ "cloudflare",
27
+ "cloudflare-workers",
28
+ "durable-objects",
29
+ "websocket",
30
+ "typescript"
31
+ ],
32
+ "author": "vaishnav-mk",
33
+ "license": "MIT",
34
+ "scripts": {
35
+ "build": "tsc",
36
+ "dev": "tsc --watch",
37
+ "clean": "rm -rf dist",
38
+ "typecheck": "tsc --noEmit"
39
+ },
40
+ "devDependencies": {
41
+ "@cloudflare/workers-types": "^4.20241106.0",
42
+ "typescript": "^5.5.2"
43
+ },
44
+ "dependencies": {
45
+ "discord-api-types": "^0.38.37",
46
+ "discord.js": "^14.25.1"
47
+ }
48
+ }
@@ -0,0 +1,90 @@
1
+ import { DiscordGateway } from "./gateway";
2
+ import type { DurableObjectState } from "@cloudflare/workers-types";
3
+ import type { ReadyData, MessageData } from "../types";
4
+
5
+ export interface DiscordClientOptions {
6
+ token: string;
7
+ intents: number;
8
+ storageKey?: string;
9
+ onReady?: (data: ReadyData) => void;
10
+ onMessage?: (data: MessageData) => void;
11
+ onError?: (error: Error) => void;
12
+ onDispatch?: (event: string, data: unknown) => void;
13
+ }
14
+
15
+ export class DiscordClient {
16
+ private ctx: DurableObjectState;
17
+ private gateway: DiscordGateway | null = null;
18
+ private initialized = false;
19
+ private options: DiscordClientOptions;
20
+
21
+ constructor(ctx: DurableObjectState, options: DiscordClientOptions) {
22
+ this.ctx = ctx;
23
+ this.options = options;
24
+ }
25
+
26
+ getGateway(): DiscordGateway | null {
27
+ return this.gateway;
28
+ }
29
+
30
+ isConnected(): boolean {
31
+ return this.gateway?.isConnected ?? false;
32
+ }
33
+
34
+ getToken(): string {
35
+ return this.options.token;
36
+ }
37
+
38
+ async fetch(_request: Request): Promise<Response> {
39
+ if (!this.initialized) {
40
+ await new Promise(resolve => setTimeout(resolve, 100));
41
+ await this.initialize();
42
+ this.initialized = true;
43
+ }
44
+ return new Response("Bot is running!", {
45
+ headers: { "Content-Type": "text/plain" },
46
+ });
47
+ }
48
+
49
+ async alarm(): Promise<void> {
50
+ if (this.gateway?.isConnected) {
51
+ await this.gateway.sendHeartbeat();
52
+ }
53
+ }
54
+
55
+ private async initialize(): Promise<void> {
56
+ await new Promise(resolve => setTimeout(resolve, 200));
57
+
58
+ this.gateway = new DiscordGateway(
59
+ this.options.token,
60
+ this.options.intents,
61
+ this.ctx,
62
+ {
63
+ storageKey: this.options.storageKey || "gatewayState",
64
+ onDispatch: (event, data) => {
65
+ this.options.onDispatch?.(event, data);
66
+ },
67
+ }
68
+ );
69
+
70
+ this.gateway.onReady((data) => {
71
+ this.options.onReady?.(data);
72
+ });
73
+
74
+ this.gateway.onMessage((data) => {
75
+ this.options.onMessage?.(data);
76
+ });
77
+
78
+ this.gateway.onError((error) => {
79
+ this.options.onError?.(error);
80
+ });
81
+
82
+ try {
83
+ await this.gateway.connect();
84
+ } catch (error) {
85
+ this.options.onError?.(
86
+ error instanceof Error ? error : new Error("Failed to connect")
87
+ );
88
+ }
89
+ }
90
+ }
@@ -0,0 +1,90 @@
1
+ import { DiscordGateway } from "./gateway";
2
+ import type { DurableObjectState } from "@cloudflare/workers-types";
3
+ import type { ReadyData, MessageData } from "../types";
4
+
5
+ export interface DiscordClientOptions {
6
+ token: string;
7
+ intents: number;
8
+ storageKey?: string;
9
+ onReady?: (data: ReadyData) => void;
10
+ onMessage?: (data: MessageData) => void;
11
+ onError?: (error: Error) => void;
12
+ onDispatch?: (event: string, data: unknown) => void;
13
+ }
14
+
15
+ export class DiscordClient {
16
+ private ctx: DurableObjectState;
17
+ private gateway: DiscordGateway | null = null;
18
+ private initialized = false;
19
+ private options: DiscordClientOptions;
20
+
21
+ constructor(ctx: DurableObjectState, options: DiscordClientOptions) {
22
+ this.ctx = ctx;
23
+ this.options = options;
24
+ }
25
+
26
+ getGateway(): DiscordGateway | null {
27
+ return this.gateway;
28
+ }
29
+
30
+ isConnected(): boolean {
31
+ return this.gateway?.isConnected ?? false;
32
+ }
33
+
34
+ getToken(): string {
35
+ return this.options.token;
36
+ }
37
+
38
+ async fetch(_request: Request): Promise<Response> {
39
+ if (!this.initialized) {
40
+ await new Promise(resolve => setTimeout(resolve, 100));
41
+ await this.initialize();
42
+ this.initialized = true;
43
+ }
44
+ return new Response("Bot is running!", {
45
+ headers: { "Content-Type": "text/plain" },
46
+ });
47
+ }
48
+
49
+ async alarm(): Promise<void> {
50
+ if (this.gateway?.isConnected) {
51
+ await this.gateway.sendHeartbeat();
52
+ }
53
+ }
54
+
55
+ private async initialize(): Promise<void> {
56
+ await new Promise(resolve => setTimeout(resolve, 200));
57
+
58
+ this.gateway = new DiscordGateway(
59
+ this.options.token,
60
+ this.options.intents,
61
+ this.ctx,
62
+ {
63
+ storageKey: this.options.storageKey || "gatewayState",
64
+ onDispatch: (event, data) => {
65
+ this.options.onDispatch?.(event, data);
66
+ },
67
+ }
68
+ );
69
+
70
+ this.gateway.onReady((data) => {
71
+ this.options.onReady?.(data);
72
+ });
73
+
74
+ this.gateway.onMessage((data) => {
75
+ this.options.onMessage?.(data);
76
+ });
77
+
78
+ this.gateway.onError((error) => {
79
+ this.options.onError?.(error);
80
+ });
81
+
82
+ try {
83
+ await this.gateway.connect();
84
+ } catch (error) {
85
+ this.options.onError?.(
86
+ error instanceof Error ? error : new Error("Failed to connect")
87
+ );
88
+ }
89
+ }
90
+ }
@@ -0,0 +1,238 @@
1
+ import { GATEWAY_BASE_URL, WEBSOCKET_READY_STATE_OPEN } from "../types/constants";
2
+ import { GatewayPayload, ReadyData, MessageData } from "../types";
3
+ import { WebSocketManager } from "../utils/websocket";
4
+ import { GatewayHandlers } from "./handlers";
5
+ import { ReconnectManager } from "../utils/reconnect";
6
+ import { SessionManager } from "../utils/session";
7
+ import { HeartbeatManager } from "./heartbeat";
8
+ import { MessageHandler } from "./message-handler";
9
+ import { Logger, LogLevel, createLogger } from "../utils/logger";
10
+ import type { DurableObjectState, WebSocket } from "@cloudflare/workers-types";
11
+
12
+ export class GatewayConnection {
13
+ private wsManager: WebSocketManager;
14
+ private token: string;
15
+ private ctx: DurableObjectState;
16
+ private storageKey: string;
17
+ private sequence: { value: number | null } = { value: null };
18
+ private handlers: GatewayHandlers;
19
+ private reconnectManager: ReconnectManager;
20
+ private sessionManager: SessionManager;
21
+ private heartbeatManager: HeartbeatManager;
22
+ private messageHandler: MessageHandler;
23
+ private onDispatch?: (event: string, data: unknown) => void;
24
+ private logger: Logger;
25
+ private isShuttingDown = false;
26
+
27
+ private onReadyCallback: ((data: ReadyData) => void) | null = null;
28
+ private onMessageCallback: ((data: MessageData) => void) | null = null;
29
+ private onErrorCallback: ((error: Error) => void) | null = null;
30
+
31
+ constructor(
32
+ token: string,
33
+ intents: number,
34
+ ctx: DurableObjectState,
35
+ config: {
36
+ storageKey: string;
37
+ identifyProperties: { os: string; browser: string; device: string };
38
+ maxReconnectAttempts?: number;
39
+ reconnectDelays?: {
40
+ initial: number;
41
+ max: number;
42
+ backoffMultiplier: number;
43
+ };
44
+ onDispatch?: (event: string, data: unknown) => void;
45
+ logger?: Logger;
46
+ debug?: boolean;
47
+ }
48
+ ) {
49
+ this.token = token;
50
+ this.ctx = ctx;
51
+ this.storageKey = config.storageKey;
52
+ this.onDispatch = config.onDispatch;
53
+ this.logger = config.logger || createLogger({
54
+ level: config.debug ? LogLevel.DEBUG : LogLevel.INFO,
55
+ });
56
+
57
+ this.wsManager = new WebSocketManager();
58
+ this.reconnectManager = new ReconnectManager(
59
+ config.maxReconnectAttempts,
60
+ config.reconnectDelays
61
+ );
62
+ this.sessionManager = new SessionManager(
63
+ token,
64
+ ctx,
65
+ this.storageKey,
66
+ config.identifyProperties,
67
+ (p: GatewayPayload) => this.wsManager.send(p)
68
+ );
69
+ this.heartbeatManager = new HeartbeatManager(
70
+ ctx,
71
+ this.storageKey,
72
+ this.sequence,
73
+ (p: GatewayPayload) => this.wsManager.send(p)
74
+ );
75
+ this.handlers = new GatewayHandlers(
76
+ ctx,
77
+ this.storageKey,
78
+ this.sequence,
79
+ (data) => this.onReadyCallback?.(data),
80
+ (data) => this.onMessageCallback?.(data),
81
+ this.onDispatch,
82
+ this.logger
83
+ );
84
+ this.messageHandler = new MessageHandler(
85
+ this.handlers,
86
+ this.sessionManager,
87
+ this.heartbeatManager,
88
+ (shouldResume) => this.reconnect(shouldResume),
89
+ ctx,
90
+ this.storageKey,
91
+ intents,
92
+ this.logger
93
+ );
94
+ }
95
+
96
+ get ws(): WebSocket | null {
97
+ return this.wsManager.ws;
98
+ }
99
+
100
+ get isConnected(): boolean {
101
+ return this.wsManager.ws?.readyState === WEBSOCKET_READY_STATE_OPEN;
102
+ }
103
+
104
+ async connect(): Promise<void> {
105
+ if (this.isShuttingDown) {
106
+ this.logger.warn("Connection attempt blocked: shutdown in progress");
107
+ return;
108
+ }
109
+
110
+ try {
111
+ this.logger.debug("Fetching gateway URL");
112
+ const gatewayUrl = await this.getGatewayUrl();
113
+ this.logger.debug("Connecting to gateway", { url: gatewayUrl });
114
+ await this.wsManager.connect(gatewayUrl);
115
+ this.wsManager.setupEventListeners(
116
+ (data: string | ArrayBuffer | ArrayBufferView) => this.handleMessage(data),
117
+ async (code: number, reason: string) => await this.handleClose(code, reason),
118
+ (error: Error) => this.handleWebSocketError(error)
119
+ );
120
+ this.reconnectManager.reset();
121
+ this.logger.info("Gateway connection established");
122
+ } catch (error) {
123
+ const err = error instanceof Error ? error : new Error("Connection failed");
124
+ this.logger.error("Failed to connect to gateway", err);
125
+ this.onErrorCallback?.(err);
126
+ if (!this.isShuttingDown) {
127
+ await this.reconnectManager.scheduleReconnect(() => this.connect());
128
+ }
129
+ }
130
+ }
131
+
132
+ private async getGatewayUrl(): Promise<string> {
133
+ const response = await fetch(GATEWAY_BASE_URL, {
134
+ headers: { Authorization: `Bot ${this.token}` },
135
+ });
136
+
137
+ if (!response.ok) {
138
+ throw new Error(`Failed to get Gateway URL: ${response.status}`);
139
+ }
140
+
141
+ const data = (await response.json()) as { url: string };
142
+ const wsUrl = data.url.replace(/^wss:/, "https:");
143
+ return `${wsUrl}?v=10&encoding=json`;
144
+ }
145
+
146
+ private async handleMessage(
147
+ data: string | ArrayBuffer | ArrayBufferView
148
+ ): Promise<void> {
149
+ try {
150
+ const text = this.wsManager.decodeMessage(data);
151
+ const payload: GatewayPayload = JSON.parse(text);
152
+ this.logger.debug("Received gateway payload", { op: payload.op, t: payload.t });
153
+ await this.messageHandler.handle(payload);
154
+ } catch (error) {
155
+ const err = error instanceof Error ? error : new Error("Message handling failed");
156
+ this.logger.error("Error handling gateway message", err);
157
+ this.onErrorCallback?.(err);
158
+ }
159
+ }
160
+
161
+ private async handleClose(code: number, reason: string): Promise<void> {
162
+ this.logger.warn("WebSocket closed", { code, reason });
163
+ await this.heartbeatManager.cancel();
164
+
165
+ if (this.isShuttingDown) {
166
+ this.logger.debug("Shutdown in progress, not reconnecting");
167
+ return;
168
+ }
169
+
170
+ const shouldResume = this.messageHandler.shouldResume(code);
171
+ this.logger.debug("Determining reconnect strategy", { shouldResume, code });
172
+ await this.reconnect(shouldResume);
173
+ }
174
+
175
+ private handleWebSocketError(error: Error): void {
176
+ this.logger.error("WebSocket error", error);
177
+ this.onErrorCallback?.(error);
178
+
179
+ if (!this.isShuttingDown && !this.isConnected) {
180
+ this.logger.debug("Scheduling reconnect due to WebSocket error");
181
+ this.reconnectManager.scheduleReconnect(() => this.connect()).catch((err) => {
182
+ this.logger.error("Failed to schedule reconnect", err);
183
+ });
184
+ }
185
+ }
186
+
187
+ private async reconnect(shouldResume: boolean): Promise<void> {
188
+ if (this.isShuttingDown) {
189
+ this.logger.debug("Reconnect blocked: shutdown in progress");
190
+ return;
191
+ }
192
+
193
+ try {
194
+ await this.reconnectManager.reconnect(
195
+ shouldResume,
196
+ this.ctx,
197
+ this.storageKey,
198
+ () => this.connect(),
199
+ () => {
200
+ this.logger.debug("Closing WebSocket connection");
201
+ this.wsManager.close();
202
+ }
203
+ );
204
+ } catch (error) {
205
+ const err = error instanceof Error ? error : new Error("Reconnect failed");
206
+ this.logger.error("Reconnection failed", err);
207
+ this.onErrorCallback?.(err);
208
+ }
209
+ }
210
+
211
+ async disconnect(): Promise<void> {
212
+ this.logger.info("Disconnecting from gateway");
213
+ this.isShuttingDown = true;
214
+ await this.heartbeatManager.cancel();
215
+ this.wsManager.close();
216
+ this.reconnectManager.reset();
217
+ }
218
+
219
+ onReady(callback: (data: ReadyData) => void): void {
220
+ this.onReadyCallback = callback;
221
+ }
222
+
223
+ onMessage(callback: (data: MessageData) => void): void {
224
+ this.onMessageCallback = callback;
225
+ }
226
+
227
+ onError(callback: (error: Error) => void): void {
228
+ this.onErrorCallback = callback;
229
+ }
230
+
231
+ async sendHeartbeat(): Promise<void> {
232
+ if (!this.isConnected) {
233
+ await this.heartbeatManager.cancel();
234
+ return;
235
+ }
236
+ await this.heartbeatManager.send();
237
+ }
238
+ }
@@ -0,0 +1,78 @@
1
+ import { GatewayConnection } from "./connection";
2
+ import {
3
+ ReadyData,
4
+ GatewayConfig,
5
+ DEFAULT_IDENTIFY_PROPERTIES,
6
+ MessageData,
7
+ } from "../types";
8
+ import {
9
+ DEFAULT_RECONNECT_DELAYS,
10
+ DEFAULT_STORAGE_KEY,
11
+ DEFAULT_MAX_RECONNECT_ATTEMPTS,
12
+ } from "../types/constants";
13
+ import { createLogger, LogLevel } from "../utils/logger";
14
+ import type { DurableObjectState, WebSocket } from "@cloudflare/workers-types";
15
+
16
+ export class DiscordGateway {
17
+ private connection: GatewayConnection;
18
+
19
+ constructor(
20
+ token: string,
21
+ intents: number,
22
+ ctx: DurableObjectState,
23
+ config?: Partial<GatewayConfig>
24
+ ) {
25
+ const storageKey = config?.storageKey || DEFAULT_STORAGE_KEY;
26
+ const identifyProperties =
27
+ config?.identifyProperties || DEFAULT_IDENTIFY_PROPERTIES;
28
+ const maxReconnectAttempts =
29
+ config?.maxReconnectAttempts || DEFAULT_MAX_RECONNECT_ATTEMPTS;
30
+ const reconnectDelays =
31
+ config?.reconnectDelays || DEFAULT_RECONNECT_DELAYS;
32
+ const logger = config?.logger || createLogger({
33
+ level: config?.debug ? LogLevel.DEBUG : LogLevel.INFO,
34
+ });
35
+
36
+ this.connection = new GatewayConnection(token, intents, ctx, {
37
+ storageKey,
38
+ identifyProperties,
39
+ maxReconnectAttempts,
40
+ reconnectDelays,
41
+ onDispatch: config?.onDispatch,
42
+ logger,
43
+ debug: config?.debug,
44
+ });
45
+ }
46
+
47
+ get ws(): WebSocket | null {
48
+ return this.connection.ws;
49
+ }
50
+
51
+ get isConnected(): boolean {
52
+ return this.connection.isConnected;
53
+ }
54
+
55
+ async connect(): Promise<void> {
56
+ await this.connection.connect();
57
+ }
58
+
59
+ async disconnect(): Promise<void> {
60
+ await this.connection.disconnect();
61
+ }
62
+
63
+ async sendHeartbeat(): Promise<void> {
64
+ await this.connection.sendHeartbeat();
65
+ }
66
+
67
+ onReady(callback: (data: ReadyData) => void): void {
68
+ this.connection.onReady(callback);
69
+ }
70
+
71
+ onMessage(callback: (data: MessageData) => void): void {
72
+ this.connection.onMessage(callback);
73
+ }
74
+
75
+ onError(callback: (error: Error) => void): void {
76
+ this.connection.onError(callback);
77
+ }
78
+ }
@@ -0,0 +1,136 @@
1
+ import { GatewayOpcode, GatewayEvent } from "../types/constants";
2
+ import {
3
+ GatewayPayload,
4
+ HelloData,
5
+ ReadyData,
6
+ GatewayState,
7
+ MessageData,
8
+ } from "../types";
9
+ import type { DurableObjectState } from "@cloudflare/workers-types";
10
+ import type { GatewayMessageCreateDispatchData } from "discord-api-types/v10";
11
+ import type { Logger } from "../utils/logger";
12
+
13
+ export class GatewayHandlers {
14
+ constructor(
15
+ private ctx: DurableObjectState,
16
+ private storageKey: string,
17
+ private sequence: { value: number | null },
18
+ private onReady: (data: ReadyData) => void,
19
+ private onMessage: (data: MessageData) => void,
20
+ private onDispatch: ((event: string, data: unknown) => void) | undefined,
21
+ private logger: Logger
22
+ ) {}
23
+
24
+ async handleHello(data: HelloData): Promise<void> {
25
+ this.logger.debug("Handling HELLO", { heartbeatInterval: data.heartbeat_interval });
26
+ const state = await this.getState();
27
+ await this.ctx.storage.put(this.storageKey, {
28
+ heartbeatInterval: data.heartbeat_interval,
29
+ sequence: this.sequence.value,
30
+ sessionId: state?.sessionId ?? null,
31
+ reconnectAttempts: 0,
32
+ shouldResume: state?.sessionId !== null && state?.sessionId !== undefined,
33
+ } satisfies GatewayState);
34
+ }
35
+
36
+ async handleReady(data: ReadyData): Promise<void> {
37
+ this.logger.info("Bot ready", { userId: data.user.id, username: data.user.username });
38
+ const state = await this.getState();
39
+ if (state) {
40
+ await this.ctx.storage.put(this.storageKey, {
41
+ ...state,
42
+ sessionId: data.session_id,
43
+ shouldResume: true,
44
+ reconnectAttempts: 0,
45
+ } satisfies GatewayState);
46
+ }
47
+ this.onReady(data);
48
+ }
49
+
50
+ handleResumed(): void {
51
+ this.logger.info("Session resumed");
52
+ this.ctx.storage.get(this.storageKey).then((state) => {
53
+ const gatewayState = this.validateState(state);
54
+ if (gatewayState) {
55
+ this.ctx.storage.put(this.storageKey, {
56
+ ...gatewayState,
57
+ reconnectAttempts: 0,
58
+ } satisfies GatewayState);
59
+ }
60
+ });
61
+ }
62
+
63
+ handleDispatch(payload: GatewayPayload): void {
64
+ if (payload.s !== null && payload.s !== undefined) {
65
+ this.sequence.value = payload.s;
66
+ this.updateSequenceInStorage();
67
+ }
68
+
69
+ if (!payload.t) {
70
+ return;
71
+ }
72
+
73
+ const event = payload.t;
74
+ if (event === GatewayEvent.READY) {
75
+ this.handleReady(payload.d as ReadyData);
76
+ } else if (event === GatewayEvent.MESSAGE_CREATE) {
77
+ this.handleMessageCreate(payload);
78
+ } else if (event === GatewayEvent.RESUMED) {
79
+ this.handleResumed();
80
+ }
81
+
82
+ if (this.onDispatch) {
83
+ this.onDispatch(event, payload.d);
84
+ }
85
+ }
86
+
87
+ private handleMessageCreate(payload: GatewayPayload): void {
88
+ const messageData = payload.d as GatewayMessageCreateDispatchData;
89
+ const opcodeName = this.getOpcodeName(payload.op);
90
+
91
+ const enrichedMessage: MessageData = {
92
+ ...messageData,
93
+ _gatewayMetadata: {
94
+ opcode: payload.op,
95
+ opcodeName,
96
+ event: payload.t ?? "UNKNOWN",
97
+ sequence: payload.s ?? null,
98
+ },
99
+ };
100
+ this.onMessage(enrichedMessage);
101
+ }
102
+
103
+ private getOpcodeName(opcode: number): string {
104
+ const entry = Object.entries(GatewayOpcode).find(
105
+ ([, value]) => value === opcode
106
+ );
107
+ return entry?.[0] ?? "UNKNOWN";
108
+ }
109
+
110
+ private async getState(): Promise<GatewayState | null> {
111
+ const state = await this.ctx.storage.get(this.storageKey);
112
+ return this.validateState(state);
113
+ }
114
+
115
+ private validateState(state: unknown): GatewayState | null {
116
+ if (state === null || state === undefined) {
117
+ return null;
118
+ }
119
+ if (typeof state === "object" && "heartbeatInterval" in state) {
120
+ return state as GatewayState;
121
+ }
122
+ return null;
123
+ }
124
+
125
+ private updateSequenceInStorage(): void {
126
+ this.ctx.storage.get(this.storageKey).then((state) => {
127
+ const gatewayState = this.validateState(state);
128
+ if (gatewayState) {
129
+ this.ctx.storage.put(this.storageKey, {
130
+ ...gatewayState,
131
+ sequence: this.sequence.value,
132
+ } satisfies GatewayState);
133
+ }
134
+ });
135
+ }
136
+ }